8.1.6 클래스의 값 전달 방식 : 참조에 의한 전달

값의 복사에 의해 전달되는 구조체와는 달리, 클래스는 메모리 주소 참조에 의한 전달 방식을 사용합니다. 이를 참조 타입(Reference Type)이라고 합니다. 참조 타입은 변수나 상수에 할당될 때, 또는 함수의 인자값으로 전달될 때 값의 복사가 이루어지지 않습니다. 대신 현재 존재하는 인스턴스에 대한 참조가 전달됩니다.

여기서 참조란, 인스턴스가 저장된 메모리 주소 정보가 전달된다는 뜻입니다. 마치 보관함에 전달할 물건을 넣어두고 '보관함 XXX번에 물건 놔뒀으니까 꺼내서 가져가' 하는 방식이 참조에 의한 전달 방식입니다.

이와 유사한 개념으로 C에서의 포인터를 들 수 있습니다. C에서는 클래스가 존재하지 않으며 주고받는 자료형 대부분은 구조체로 작성됩니다. 구조게로 작성된 자료형을 직접 전달하는 대신 포인터 형식으로 메모리 주소값만 전달할 수도 있습니다.

스위프트에서는 이와 같이 포인터를 사용하여 객체와 메모리 주소를 구분하는 대신, 클래스 타입의 경우 항상 메모리 주소를 사용해 객체 자체를 전달합니다. 따라서 우리는 주고받는 타입이 클래스일 때는 '주소값을 전달해야 한다!'라는 고민을 하지 않아도 됩니다. 그런 고민 없이 단순히 값을 넘긴다고 생각해도 됩니다.

let video = VideoMode()
video.name = "Original Video Instance"

print("video 인스턴스의 name 값은 \(video.name!)입니다.")
// video 인스턴스의 name 값은 Original Video Instance입니다.

VideoMode 클래스를 초기화하여 인스턴스를 생성하여 video라는 상수에 할당했습니다. 그 다음 name이라는 프로퍼티에 값을 입력했습니다. 결과값을 출력하면 제대로 값이 설정되었음을 알 수 있습니다. 이제 이 인스턴스를 다른 상수에 할당해 보겠습니다.

let dvd = video
dvd.name = "DVD Video Instance"

print("video 인스턴스의 name 값은 \(video.name!)입니다")
// video 인스턴스의 name 값은 DVD Video Instance입니다

우리는 video의 속성값을 변경하지 않고 dvd의 속성값을 변경하였습니다. 그렇지만 video 상수의 프로퍼티에서도 값이 변경되었음을 확인할 수 있습니다.

이제 이 인스턴스의 값을 함수의 인자값으로 넣어 다시 수정해 보겠습니다.

func changeName(v: VideoMode) {
    v.name = "Function Video Instance"
}

changeName(v: Video)
print("video 인스턴스의 name 값은 \(video.name!)입니다")
// video 인스턴스의 name 값은 Function Video Instance입니다

changeName이라는 함수는 인자값으로 video인스턴스를 전달받아 프로퍼티의 값을 변경했습니다. 함수의 매개변수에 inout 키워드를 붙여주지 않았지만, 전달한 값이 클래스 타입이기 때문에 원본 인스턴스의 참조가 전달된 것입니다.

이처럼 클래스는 참조 타입이어서 한 곳에서 수정할 경우 다른 곳에도 적용되는 특징과 함께, 하나의 클래스 인스턴스를 여러 변수나 상수, 또는 함수의 인자값에서 동시에 참조할 수 있다는 특성도 가지고 있습니다. 여러 곳에 할당되면 그 개수만큼 하나의 클래스 인스턴스를 참조하는 곳이 늘어나는 것입니다.

이 때문에 클래스에서는 메모리에 대한 이슈 문제가 부각됩니다. 적절한 메모리 해제 시점을 계산해야 하기 때문입니다. 언제나 단일 참조가 보장되는 구조체 인스턴스는 인스턴스가 할당된 변수나 상수의 사용이 끝나면 곧바로 메모리에서 해제해도 되지만, 클래스 인스턴스는 여러 곳에서 동시에 참조가 가능하므로 한 곳에서의 참조가 완료되었다고 해도 마음대로 메모리에서 해제할 수 없습니다. 메모리에서 그냥 막 인스턴스를 해제해버리면 아직 인스턴스를 참조하고 있는 변수나 상수, 함수의 인자값 등은 잘못된 메모리 참조로 인한 오류가 발생합니다.

스위프트에서는 ARC가 객체의 메모리 해제를 담당합니다 ARC는 Auto Reference Counter의 약자로서 '지금 클래스 인스턴스를 참조하는 곳이 모두 몇 군데인지 자동으로 카운트해주는 객체'라고 할 수 있습니다. 이 객체는 인스턴스를 모니터링하며 변수나 상수, 함수의 인자값으로 할당되면 카운트를 1 증가시키고, 해당 변수나 상수들이 종료되면 카운트를 1 감소시키는 작업을 계속하면서 인스턴스의 참조 수를 계산합니다. 인스턴스의 참조 카운트가 0이 되면 메모리 해제 대상으로 간주하여 적절히 메모리에서 해제합니다.

클래스 인스턴스에서 단순한 값 비교는 불가능합니다. 대신 두 대상이 같은 메모리 공간을 참조하는 인스턴스인지 아닌지 비교해야 합니다. 이를 위해 클래스 인스턴스의 비교 연산자는 다음을 사용합니다.

  • 동일 인스턴스인지 비교 : ===

  • 동인 인스턴스가 아닌지 비교 : !==

if (video === dvd) {
    print("video와 dvd는 동일한 VideoMode 인스턴스를 참조하고 있군요")
} else {
    print("video와 dvd는 서로 다른 VideoMode 인스턴스를 참조하고 있군요")
}

// 실행 결과
video와 dvd는 동일한 VideoMode 인스턴스를 참조하고 있군요

VideoMode 클래스의 인스턴스가 생성된 다음 video 상수에 참조되었고, 이 값이 다시 dvd에 참조되었으므로 두 상수는 동일한 클래스 인스턴스를 참조합니다. 따라서 === 연산자의 결과는 true입니다. 만약 다음과 같이 인스턴스가 참조되었다면 두 상수는 서로 다른 인스턴스를 참조합니다.

let vs = VideoMode()
let ds = VideoMode()

if (vs === ds) {
    print("vs와 ds는 동일한 VideoMode 인스턴스를 참조하고 있습니다")
} else {
    print("vs와 ds는 서로 다른 VideoMode 인스턴스를 참조하고 있습니다")
}

// 실행 결과
vs와 ds는 서로 다른 VideoMode 인스턴스를 참조하고 있습니다

ds에 참조 할당된 인스턴스는 vs에 참조 할당된 인스턴스가 아닌 새롭게 생성된 인스턴스입니다. 동일한 타입의 인스턴스지만 같은 메모리 주소를 참조하는 것은 아니므로 비교 연산의 결과가 false로 처리됩니다.

그러면 어느 경우에 구조체를 사용하고 어느 경우에 클래스를 사용해야 할까요? 다음에 해당한다면 구조체를 사용하는 것이 좋습니다.

  1. 서로 연관된 몇 개의 기본 데이터 타입들을 캡슐화하여 묶는 것이 목적일 때

  2. 캡슐화된 데이터에 상속이 필요하지 않을 때

  3. 캡슐화된 데이터를 전달하거나 할당하는 과정에서 참조 방식보다는 값이 복사되는 것이 합리적일 때

  4. 캡슐화된 원본 데이터를 보존해야 할 때

여기에 해당하지 않는 경우라면 일반적으로 구조체보다는 클래스를 정의하여 사용하는 것이 좋습니다. 상수나 변수에 할당할 때도 값의 복사가 발생하지 않기 때문에 여러 곳에 할당하더라도 메모리의 낭비가 없으며, 인스턴스가 늘어나지 않으므로 코딩상의 혼란이 적습니다.

Last updated