구조체와 클래스

구조체와 클래스

데이터를 묶어서 하나의 타입으로 만들고 싶을 때 흔히 쓰는 게 구조체(struct)와 클래스(class)예요. 둘 다 프로퍼티와 메서드를 정의해서 기능을 더할 수 있는 범용적인 구성 요소인데, Swift는 다른 언어와 달리 인터페이스와 구현을 별도 파일로 나눌 필요가 없어요. 한 파일에 정의하면 그 타입의 외부 인터페이스는 자동으로 다른 코드가 쓸 수 있게 공개돼요.

출처: 공식문서

본문

구조체와 클래스 비교하기

구조체와 클래스는 공통점이 많아요. 둘 다 값(프로퍼티)을 저장하고, 기능(메서드)을 제공하며, 서브스크립트로 값에 접근하도록 하고, 초기값을 설정하는 이니셜라이저를 정의하고, 확장하고, 프로토콜을 채택해 특정 종류의 표준 기능을 갖출 수 있어요.

클래스에는 구조체에 없는 추가 능력이 있어요. 상속(inheritance)으로 다른 클래스의 특성을 물려받고, 타입 캐스팅으로 실행 중에 인스턴스 타입을 확인하며, 디이니셜라이저로 자원을 해제하고, 참조 카운팅 덕분에 한 클래스 인스턴스를 여러 곳에서 참조할 수 있어요.

이 추가 능력은 복잡도 증가라는 대가를 수반해요. 일반적인 지침은 이래요. 구조체를 기본으로 쓰고, 클래스가 적절하거나 필요할 때만 클래스를 써요. 실제로 여러분이 정의하는 사용자 타입 대부분은 구조체와 열거형이 될 거예요.

정의 문법

구조체는 struct, 클래스는 class 키워드로 시작하고, 둘 다 전체 정의를 중괄호 안에 담아요.

struct SomeStructure {
    // structure definition goes here
}
class SomeClass {
    // class definition goes here
}

구조체 정의와 클래스 정의 예시를 볼게요. Resolution은 픽셀 기반 디스플레이 해상도를 나타내는 구조체예요. 두 저장 프로퍼티 widthheight를 갖고, 초깃값 0 덕분에 둘 다 Int 타입으로 추론돼요. VideoMode는 비디오 표시용 모드를 나타내는 클래스예요. 네 개의 변수 저장 프로퍼티를 갖는데, resolution은 새 Resolution 인스턴스로 초기화되죠.

struct Resolution {
    var width = 0
    var height = 0
}
class VideoMode {
    var resolution = Resolution()
    var interlaced = false
    var frameRate = 0.0
    var name: String?
}

나머지 세 프로퍼티 중 interlaced는 기본값 false, frameRate0.0, name은 옵셔널 String이라 자동으로 기본값 nil을 가져요.

인스턴스 만들기

Resolution 구조체 정의와 VideoMode 클래스 정의는 그저 '어떻게 생겼는지'를 서술할 뿐이에요. 실제 해상도나 비디오 모드를 쓰려면 인스턴스(instance)를 만들어야 해요. 구조체와 클래스 모두 이니셜라이저 문법을 쓰는데, 가장 간단한 형태는 타입 이름 뒤에 빈 괄호를 붙이는 거예요.

let someResolution = Resolution()
let someVideoMode = VideoMode()

프로퍼티 접근하기

인스턴스의 프로퍼티는 점 문법(dot syntax)으로 접근해요. 인스턴스 이름 바로 뒤에 점(.)을 붙이고 프로퍼티 이름을 쓰면 돼요. 공백은 없어요.

print("The width of someResolution is \(someResolution.width)")
// Prints "The width of someResolution is 0".

하위 프로퍼티로 쭉 파고들 수도 있어요. VideoModeresolution 프로퍼티 안에 있는 width 같은 경우가 그렇죠.

print("The width of someVideoMode is \(someVideoMode.resolution.width)")
// Prints "The width of someVideoMode is 0".

점 문법으로 변수 프로퍼티에 새 값을 할당하는 것도 가능해요.

someVideoMode.resolution.width = 1280
print("The width of someVideoMode is now \(someVideoMode.resolution.width)")
// Prints "The width of someVideoMode is now 1280".

구조체의 멤버와이즈 이니셜라이저

모든 구조체에는 멤버와이즈 이니셜라이저(memberwise initializer)가 자동 생성돼요. 새 인스턴스의 프로퍼티 초깃값을 이름으로 전달해서 초기화할 수 있어요.

let vga = Resolution(width: 640, height: 480)

구조체와 달리 클래스 인스턴스에는 기본 멤버와이즈 이니셜라이저가 주어지지 않아요.

구조체와 열거형은 값 타입

값 타입(value type)은 변수나 상수에 할당되거나 함수에 전달될 때 그 값이 복사되는 타입이에요. Swift의 모든 기본 타입 — 정수, 부동소수점 수, 불리언, 문자열, 배열, 딕셔너리 — 은 값 타입이고, 뒤에서는 구조체로 구현돼요. 구조체와 열거형 전부 값 타입이라, 인스턴스와 그 프로퍼티가 될 값 타입은 코드에서 전달될 때 항상 복사돼요.

Resolution 구조체를 예로 들어 볼게요.

let hd = Resolution(width: 1920, height: 1080)
var cinema = hd

상수 hd는 풀 HD 해상도(1920×1080)로 초기화된 Resolution 인스턴스를 가리켜요. 그다음 변수 cinema에 현재 hd 값을 넣었죠. Resolution이 구조체이므로 기존 인스턴스의 복사본이 만들어져 cinema에 할당돼요. 둘은 같은 너비·높이를 갖지만 뒤에서는 완전히 다른 두 인스턴스예요.

이제 cinemawidth를 2K 표준인 2048로 바꿔 볼게요.

cinema.width = 2048

확인해 보면 cinemawidth2048로 바뀌었지만, 원본 hdwidth는 여전히 1920이에요.

print("cinema is now \(cinema.width) pixels wide")
// Prints "cinema is now 2048 pixels wide".
print("hd is still \(hd.width) pixels wide")
// Prints "hd is still 1920 pixels wide".

cinemahd 값을 넘길 때 hd가 저장한 이 새 cinema 인스턴스로 복사된 거예요. 결국 같은 숫자를 담은 완전히 분리된 두 인스턴스가 생겼고, cinema의 width를 바꿔도 hd에는 영향을 주지 않죠.

열거형도 마찬가지 동작이에요.

enum CompassPoint {
    case north, south, east, west
    mutating func turnNorth() {
        self = .north
    }
}
var currentDirection = CompassPoint.west
let rememberedDirection = currentDirection
currentDirection.turnNorth()


print("The current direction is \(currentDirection)")
print("The remembered direction is \(rememberedDirection)")
// Prints "The current direction is north".
// Prints "The remembered direction is west".

rememberedDirectioncurrentDirection 값을 할당할 때 그 값의 복사본이 들어가요. 이후에 currentDirection을 바꿔도 rememberedDirection에 저장된 원본 복사본에는 영향을 주지 않죠.

클래스는 참조 타입

참조 타입(reference type)은 할당되거나 함수에 전달될 때 복사되지 않아요. 복사본 대신 같은 기존 인스턴스에 대한 참조가 쓰여요.

VideoMode 클래스를 예로 들어 볼게요.

let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0

상수 tenEighty를 새 VideoMode 인스턴스를 가리키도록 선언했어요. 이제 이걸 새 상수 alsoTenEighty에 할당하고 프레임 레이트를 바꿔 볼게요.

let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0

클래스는 참조 타입이라 tenEightyalsoTenEighty는 실제로 같은 VideoMode 인스턴스를 가리켜요. 둘은 같은 인스턴스의 다른 이름 두 개일 뿐이에요. 그래서 tenEightyframeRate를 확인하면 30.0으로 바뀌어 있어요.

print("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// Prints "The frameRate property of tenEighty is now 30.0".

이 예시는 참조 타입이 왜 추론하기 더 어려운지도 보여줘요. tenEightyalsoTenEighty가 코드에서 멀리 떨어져 있다면, 비디오 모드를 바꾸는 경로를 다 찾아내기 어려울 수 있죠. 값 타입은 같은 값을 다루는 코드가 소스 파일에서 가까이 모여 있어서 상대적으로 추론하기 쉬워요.

tenEightyalsoTenEighty는 상수로 선언됐는데도 프레임 레이트를 바꿀 수 있었죠? 이는 값이 아니라 참조가 상수이기 때문이에요. 상수 자체는 인스턴스를 '저장'하지 않고 그냥 가리킬 뿐이고, 실제로 바뀌는 건 뒤에 있는 VideoModeframeRate 프로퍼티예요.

동일성 연산자

클래스는 참조 타입이라 여러 상수·변수가 같은 인스턴스를 가리킬 수 있어요. 두 상수·변수가 정확히 같은 인스턴스를 가리키는지 확인하려면 동일성 연산자(identity operator)를 써요. ===(동일함)와 !==(동일하지 않음)가 있어요.

if tenEighty === alsoTenEighty {
    print("tenEighty and alsoTenEighty refer to the same VideoMode instance.")
}
// Prints "tenEighty and alsoTenEighty refer to the same VideoMode instance."

주의할 점이 있어요. ===(동일함)는 ==(같음)과 의미가 달라요. ===는 클래스 타입의 두 상수·변수가 정확히 같은 인스턴스를 가리키는지, ==는 두 인스턴스가 값으로 봤을 때 같다고 여겨지는지 보는 거예요. 두 인스턴스가 '같다'는 건 타입 설계자가 정하기 나름이에요.

더 알아보기