🔥 클래스는 참조 타입

599자
8분

Swift에서 클래스는 참조 타입(Reference Type)으로 정의됩니다. 이는 클래스의 인스턴스가 할당되거나 함수에 전달될 때, 복사되지 않고 동일한 인스턴스에 대한 참조가 사용된다는 것을 의미하죠.

VideoMode라는 클래스를 예로 들어볼까요?

class VideoMode {
    var resolution = Resolution()
    var interlaced = false
    var frameRate = 0.0
    var name: String?
}
swift

VideoMode 클래스는 비디오의 해상도, 비월 주사 여부, 프레임 속도, 이름 등의 속성을 가지고 있습니다. 이제 이 클래스의 인스턴스를 생성하고 속성 값을 설정해보겠습니다.

let tenEighty = VideoMode()             // tenEighty 상수에 VideoMode 인스턴스 할당
tenEighty.resolution = hd               // 해상도를 HD로 설정
tenEighty.interlaced = true             // 비월 주사로 설정
tenEighty.name = "1080i"                // 이름을 "1080i"로 설정
tenEighty.frameRate = 25.0              // 프레임 속도를 25.0으로 설정
swift

이렇게 tenEighty라는 상수를 선언하고 VideoMode의 새 인스턴스를 할당했습니다. 그리고 해상도를 HD(1920x1080)로, 비월 주사를 사용하도록, 이름을 "1080i"로, 프레임 속도를 25.0으로 각각 설정했죠.

그런 다음 tenEightyalsoTenEighty라는 새 상수에 할당하고, alsoTenEightyframeRate를 변경해보겠습니다.

let alsoTenEighty = tenEighty           // alsoTenEighty에 tenEighty 할당
alsoTenEighty.frameRate = 30.0          // alsoTenEighty의 프레임 속도를 30.0으로 변경
swift

클래스가 참조 타입이기 때문에 사실 tenEightyalsoTenEighty는 동일한 VideoMode 인스턴스를 가리키게 됩니다. 효과적으로 단일 인스턴스에 대해 서로 다른 두 개의 이름을 사용하는 셈이죠. 아래 그림과 같이 말이에요.

lecture image

tenEightyframeRate 속성을 확인해보면 VideoMode 인스턴스의 프레임 속도가 30.0으로 변경된 것을 알 수 있습니다.

print("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// "The frameRate property of tenEighty is now 30.0" 출력
swift

이 예제는 참조 타입으로 인해 코드를 이해하기 어려워질 수 있음을 보여주기도 합니다. 만약 tenEightyalsoTenEighty가 프로그램 코드에서 멀리 떨어져 있다면, 비디오 모드가 변경되는 모든 방식을 찾기가 어려울 수 있겠죠. tenEighty를 사용하는 곳에서는 alsoTenEighty를 사용하는 코드도 고려해야 하고, 그 반대의 경우도 마찬가지입니다. 반면에 값 타입은 동일한 값과 상호 작용하는 모든 코드가 소스 파일에서 가깝기 때문에 추론하기가 더 쉽습니다.

tenEightyalsoTenEighty가 상수로 선언되었음에 주목해주세요. 그럼에도 tenEighty.frameRatealsoTenEighty.frameRate를 변경할 수 있는데요, tenEightyalsoTenEighty 상수 자체의 값은 실제로 변경되지 않기 때문입니다. tenEightyalsoTenEightyVideoMode 인스턴스를 "저장"하지 않고, 내부적으로 VideoMode 인스턴스를 "참조"하는 거예요. 변경되는 것은 기본 VideoModeframeRate 속성이지, VideoMode를 참조하는 상수 값이 아닙니다.

식별 연산자

클래스가 참조 타입이기 때문에 여러 상수와 변수가 내부적으로 동일한 클래스 인스턴스를 참조하는 것이 가능합니다. (구조체와 열거형의 경우 상수나 변수에 할당되거나 함수에 전달될 때 항상 복사되므로 동일한 경우가 아닙니다.)

때로는 두 상수나 변수가 정확히 동일한 클래스 인스턴스를 참조하는지 확인하는 것이 유용할 수 있는데요, 이를 위해 Swift는 두 가지 식별 연산자를 제공합니다:

  • 같음 (===)
  • 같지 않음 (!==)

이 연산자들을 사용해 두 상수나 변수가 동일한 단일 인스턴스를 참조하는지 확인할 수 있습니다.

if tenEighty === alsoTenEighty {
    print("tenEighty와 alsoTenEighty는 같은 VideoMode 인스턴스를 참조합니다.")
}
// "tenEighty와 alsoTenEighty는 같은 VideoMode 인스턴스를 참조합니다." 출력
swift

같음 연산자(===)는 같다(==)와 의미가 다릅니다. 같음은 클래스 타입의 두 상수나 변수가 정확히 동일한 클래스 인스턴스를 참조함을 의미하죠. 반면 같다는 두 인스턴스가 타입 설계자가 정의한 적절한 의미의 같다에 대해 값이 동일하거나 동등함을 의미합니다.

사용자 정의 구조체와 클래스를 정의할 때는 두 인스턴스가 같다고 판단할 기준을 결정하는 것이 여러분의 책임입니다. ==!= 연산자의 구현을 직접 정의하는 과정은 동등 연산자에 설명되어 있어요.

포인터

C, C++, Objective-C 경험이 있다면 이러한 언어에서 메모리 주소를 참조하기 위해 포인터를 사용한다는 것을 알고 있을 거예요. Swift에서 참조 타입의 인스턴스를 참조하는 상수나 변수는 C언어의 포인터와 유사하지만, 메모리의 주소를 직접 가리키는 것은 아니며 참조를 생성한다는 것을 나타내기 위해 별표(*)를 쓸 필요가 없습니다. 대신 이러한 참조는 Swift의 다른 상수나 변수와 같은 방식으로 정의됩니다.

Swift 표준 라이브러리는 포인터와 직접 상호 작용해야 하는 경우 사용할 수 있는 포인터와 버퍼 타입을 제공하는데요, 자세한 내용은 수동 메모리 관리를 참고해주세요.

이렇게 클래스가 참조 타입으로 동작하는 방식과 식별 연산자의 사용, 그리고 포인터에 대해 살펴봤습니다. 참조 타입은 코드의 이해와 추론을 어렵게 만들 수 있지만, 클래스 인스턴스를 효율적으로 공유하고 전달할 수 있게 해주는 강력한 기능이기도 하죠. 값 타입과 참조 타입의 차이를 잘 이해하고 적절히 사용하는 것이 Swift 프로그래밍의 중요한 부분입니다!