본문 바로가기
swift

스위프트 구조체와 클래스

by 승환파크 2024. 4. 15.

구조체와 클래스는 변수나 상수, 함수가 모두 총동원되어 만들어진다. 개념적으로 구조체와 클래스는 하나의 큰 코드 블록이다. 이 안에 변수나 상수를 넣어 값을 지정하거나 함수를 넣어서 기능을 정의할 수도 있다. 값을 저장할 수는 없지만 특정 기능을 실행할 수 있는 함수와, 값을 저장할 수 있지만 혼자서 특정 기능을 수행할 수는 없는 변수, 상수의 특성을 모두 모아 놓았다고 생각하면 된다. 이 때문에 클래스와 구조체는 다른 종류의 개게에 의존하지 않고도 자체적으로 값을 저장하거나 함수적인 기능을 구현할 수 있다.

 

구조체와 클래스 내에서 정의된 변수와 상수, 그리고 함수를 부르는 명칭이 일반의 그것과는 다르다. 똑같은 변수나 상수라도 구조체와 클래스 내부에서 정의되면 프로퍼티(Properties)라는 이름을 가진다. 속성 변수 또는 상수라고 불리기도 한다. 함수도 구조체와 클래스 내부에서 정의되면 펑션(Function)이 아니라 메소드(Method)라고 불린다.

 

프로퍼티와 메소드를 합해서 구조체나 클래스의 멤버(Member)라고 표현하는데, 이는 프로퍼티와 메소드가 구조체나 클래스를 이루는 핵심 요소이기 때문이다.

 

스위프트는 기본적으로 객체지향 언어이다. 필요한 기능을 객체로 구현하여 사용한다는 것이 객체지향 언어의 핵심인데, 이 때 객체를 만들어내는 주요 대상이 바로 구조체와 클래스이다. 구조체와 클래스는 객체지향의 근간을 이루는 핵심 객체일 뿐 아니라 iOS에서 실행되는 애플리케이션을 만들 때에도 중심 역할을 하는 중요한 개념이다.

 

구조체 VS 클래스

스위프트에서 구조체와 클래스는 여러 면에서 유사하다. 공통점은 아래와 같다.

  • 프로퍼티 : 변수나 상수를 사용하여 값을 저장하는 프로퍼티를 정의할 수 있다.
  • 메소드 : 함수를 사용하여 기능을 제공하는 메서드를 정의할 수 있다.
  • 서브스크립트 : 속성값에 접근하 수 있는 방법을 제공하는 서브스크립트를 정의할 수 있다.
  • 초기화 블록 : 객체를 원하는 초기 상태로 설정해주는 초기화 블록을 정의할 수 있다.
  • 확장 : 객체에 함수적 기능을 추가하는 확장(extends) 구문을 사용할 수 있다.
  • 프로토콜 : 특정 형식의 함수적 표준을 제공하기 위한 프로토콜을 구현할 수 있다.

하지만 구조체와 클래스의 결정적인 차이점이 있다. 클래스의 기능 범위가 구조체보다 크다. 구조체는 할 수 없지만, 클래스는 할 수 있는 기능에는 아래와 같은 것들이 존재한다.

  • 상속 : 클래스의 특성을 다른 클래스에게 물려줄 수 있다.
  • 타입 캐스팅 : 실행 시 컴파일러가 클래스 인스턴스의 타입을 미리 파악하고 검사할 수 있다.
  • 소멸화 구문 : 인스턴스가 소멸되기 직전에 처리해야 할 구문을 미리 등록해 놓을 수 있다.
  • 참조에 의한 전달 : 클래스 인스턴스가 전달될 때에는 참조 형식으로 제공되며, 이 때 참조가 가능한 개수는 제약이 없다.

 

구조체와 클래스의 기본 개념

정의 구문

구조체와 클래스는 정의하는 구문이 매우 비슷하다. 구조체는 struct 키워드를 사용하여 정의하고, 클래스는 class키워드를 사용하여 정의하는 정도의 차이만 존재한다.

// 구조체 정의 형식
struct 구조체_이름 {
	// 구조체 정의 내용이 들어갈 부분
}

// 클래스의 정의 형식
class 클래스_이름 {
	// 클래스 정의 내용이 들어갈 부분
}
// 구조체 예시
struct Resolution {
    // 구조체 정의 내용이 들어갈 부분
}

// 클래스 예시
class VideoMode {
    // 클래스 정의 내용이 들어갈 부분
}

 

객체의 이름을 작성할 때에는 표준 스위프트 객체 코딩 형식에 따라 구조체 이름과 클래스 이름의 첫 글자는 대문자로, 나머지 글자는 소문자로 작성하는 것이 원칙이다.

 

두 개 이상의 복합 단어로 이루어지는 경우 단어별로 끊어 첫 글자는 대문자로, 나머지 글자는 소문자로 작성하면 된다. 이러한 표기법을 카멜(Camel)표기법이라고 한다. 대문자와 소문자가 마치 낙타 등의 혹을 연상시킨다고 하여 부르는 이름이다. 언더바(_)는 사용하지 않는 것이 좋다. 이러한 네이밍 룰(Naming Rule)은 실무에서 꼭 지키는 것이 좋다.

 

메서드와 프로퍼티

구조체와 클래스에서는 변수나 상수를 정의하여 내부적인 값을 저장할 수 있다. 이렇게 구조체와 클래스 내부에서 정의된 변수나 상수를 프로퍼티(Property) 또는 속성이라고 한다. 또한 함수를 정의하여 특정 기능을 정의할 수도 있는데, 이를 메소드(Method)라고 한다. 이들은 구조체나 클래스 내부에 정의된다는 것만 차이가 있을 뿐 , 일반 변수/상수/함수를 정의하는 것과 문법적으로 동일하다. 아래는 구조체와 클래스르 정의하고 여기에 프로퍼티와 메소드를 작성한 예시이다.

struct Resolution {
    var width = 0
    var height = 0
    
    func desc() -> String {
        return "Resolution 구조체"
    }
}

// 클래스 예시
class VideoMode {
    var interlaced = false
    var frameRate = 0.0
    var name: String?
    
    func desc() -> String {
        return "VideoMode 클래스"
    }
}

 

위 코드는 Resolution이라는 이름의 구조체와 VideoMode라는 이름의 클래스를 선언하는 예제이다. Resolution 구조체는 픽셀 기반 디스플레이의 해상도 정보를 관리하기 위한 목적이라고 생각하면 되고, VideoMode 클래스는 비디오 디스플레이에서 표현되는 비이도에 대한 정보를 관리하기 위한 목적이라고 생각하면 된다.

 

먼저 Resolution 구조체를 살펴보면 width, height라는 두 개의 저장 프로퍼티가 있다. 저장 프로퍼티란 특정 값을 저장하기 위해 클래스나 구조체 내부에 정의된 변수나 상수를 말한다. 이들 두 프로퍼티는 선언될 때 초기값으로 0이 대입되었으므로 타입 추론 규칙에 의해 Int 타입의 데이터 형식으로 추론된다.

 

반면 VideoMode 클래스는 세 개의 저장 프로퍼티가 정의되어 있고, 이들은 모두 기본자료형으로 초기화되어 있다. interlaced는 초기값 false에 의하여 Bool 타입으로 정의되면서 'interlaced되지 않은 비디오 모드'라는 의미를 가지게 되고, frameRate 는 0.0이 초기값이므로 Double 타입으로, 비디오 모드이 이름을 표현하는 name 프로퍼티는 옵셔널 문자열로 정의된다. name 프로퍼티는 초기값이 할당되지 않았는데, 옵셔널 타입 프로퍼티에 초기값이 할당되지 않으면 자동으로 nil이라는 기본값으로 초기화 된다. 이 때의 의미는 'name 프로퍼티에 값이 존재하지 않음'으로 해석된다. 구조체와 클래스에는 desc라는 이름의 메소드가 하나씩 작성되어있다. 이 메소드의 역할은 각각의 객체에 대한 설명하는 문자열을 반환하는 것이다. 예제에서 확인할 수 있는 것처럼 메소드는 함수의 형태를 띄고 있으며 몇 가지 차이점을 제외하면 함수와 거의 같다.

 

인스턴스

우리가 정의한 구조체나 클래스를 그대로 사용해서 값을 저장하거나 메소드를 실행할 수는 없다. 프로그래미에서는 실질적인 값을 저장하고 사용하려면 메모리 공간을 할당받은 객체가 필요하다.

 

구조체나 클래스는 실행할 수 있는 객체가 아니라, 값을 담을 수 있는 실질적인 그릇을 만들어내기 위한 일종의 틀로 생각해야한다. 즉 원형(Origin)이라는 것이다. 틀 역할을 하는 구조체나 클래스를 정의하고 이를 바탕으로 실질적으로 값을 담을 여러 개의 그릇을 만들어내는 것, 이것이 객체지향 프로그래밍의 원리이다.

 

이렇게 원형 틀을 이용해 찍어낸 그릇을 인스턴스(Instance)라고 한다. 컴퓨터공학적으로 이야기해 보면 타입의 설계도를 사용하여 메모리에 공간을 할당받은 것이 인스턴스이다. 우리는 이 인스턴스를 이용해 값을 저장하거나 처리해야 한다.

// Resolution 구조체에 대한 인스턴스를 생성하고 상수 insRes에 할당
let insRes = Resolution()

// VideoMode 클래스에 대한 인스턴스를 생성하고 상수 insVMode에 할당
let insVMode = VideoMode()

 

위 예제에서 구조체와 클래스 각각의 인스턴스를 생성하고 있다. 객체가 초기화되면서 인스턴스가 생성되고, 이 값을 변수나 상수에 할당하면 이제 원하는 곳에서 사용할 수 있게 된다. 이 구문을 인스턴스 생성 구문이라고 한다. 인스턴스를 생성하는 가장 단순한 형태는 Resolution() 이나 VideoMode()처럼 구조체나 클래스의 이름 뒤에 빈 괄호를 붙이는 것이다. 함수를 배울 때 사용했던 호출 연산자와 모양은 같지만 의미는 다르다. 함수의 이름 다음에 오면 함수 호출 연산자 역할을 하고, 클래스나 구조체 이름 다음에 오면 인스턴스 생성 연산자가 된다.

 

프로퍼티는 오직 인스턴스를 통해서만 접근할 수 있다. 인스턴스가 생성되지 않은 상태에서는 프로퍼티도 존재하지 않는 것이나 마찬가지이다. 프로퍼티에 접근하려면 인스턴스를 먼저 생성해야 한다. 프로퍼티에 접근할 때는 점 문법(Dot Syntax)을 이용하여 인스턴스 하위 객체에 접근할 수 있다. 점 앞이나 뒤에는 공백이 없어야 한다.

<인스턴스 이름>.<프로퍼티 이름>

 

아래는 점 구문을 이용해 인스턴스 속성에 접근해 보는 예제 코드이다.

let width = insRes.width
print("insRes 인스턴스의 width 값은 \(width)입니다.")

// 실행결과
// insRes 인스턴스의 width 값은 0입니다.

 

Resolution 구조체에 선언된 width 속성을 참조하려면 인스턴스가 필요하다. 생성된 인스턴스 insRes와 속성 width를 점 구문으로 연결하면 insRes.width가 되는데, 이를 이용하면 속성의 값을 읽어올 수 있다. insRes.width 프로퍼티는 구좇에 내에서 대입된 값으로 초기화되며, 이후 값을 수정하지 않았으므로 0이 출력된다.

 

만약 객체에 정의된 프로퍼티가 서브 프로퍼티를 가지고 있는 객체라면 다음과 같이 계속 점 구문을 이용하여 단계적으로 접근할 수 있다.

<인스턴스 이름>.<프로퍼티 이름>.<프로퍼티의 서브 프로퍼티 이름>

 

이를 사용하기 위해서 VideoMode 클래스를 수정할 것이다.

class VideoMode {
    var interlaced = false
    var frameRate = 0.0
    var name: String?
    
    var res = Resolution() // 추가
    
    func desc() -> String {
        return "VideoMode 클래스"
    }
}

// videoMode 클래스에 대한 인스턴스를 생성하고 상수에 할당
let vMode = VideoMode()

print("vMode 인스턴스의 width 값은 \(vMode.res.width)입니다.")

// 실행결과
// vMode 인스턴스의 width 값은 0입니다.

 

추가된 프로퍼티 res는 VideoMode 클래스의 프로퍼티이자 동시에 Resolution 구조체의 인스턴스이다. 따라서 res 프로퍼티 하위에는 width 프로퍼티가 존재한다. Resolution 구제쳉서 정의된 속성이다. 개념적으로는 vMode -> resolution -> width로, width 프로퍼티에 접근하려면 아래와 같이 단계적으로 접근해야 한다.

vMode.res.width

 

점 구문은 프로퍼티에 값을 대입할 때에도 사용된다.

// 점 구문을 이용하여 인스턴스의 프로퍼티에 값을 할당
vMode.name = "Sample"
vMode.res.width = 1280

print("\(vMode.name!) 인스턴스의 width 값은 \(vMode.res.width)입니다.")

// 실행 결과
// Sample 인스턴스의 width 값은 1280입니다.

 

프로퍼티의 하위 프로퍼티에 값을 할당할 때도 위와 같이 점 구문을 연속으로 연결하여 값을 할당할 수 있다. 이러한 방식을 사슬이 계속 연결되는 방식과 비슷하다 하여 체인(chain)이라고 한다. 이러한 체인 방식은 생산성 향상에 많은 도움을 준다.

 

초기화

구조체의 초기화

구조체나 클래스 이름 뒤에 빈 괄호를 붙이면 기본적인 인스턴스가 만들어진다. 필요에 따라 빈 괄호가 아니라 인자값을 넣어주기도 하는데, 이 때 입력되는 인자값들은 대부분 객체의 프로퍼티를 초기화하기 위해 반드시 필요한 값들이다.

 

스위프트에서 옵셔널 타입으로 선언되지 않은 프로퍼티는 명시적으로 초기화해야 한다. 초기화 되지 않은 프로퍼티가 있으면 컴파일 오류가 발생한다. 명시적 초기화란 두 가지 경우 중 하나를 의미한다.

  1. 프로퍼티를 선언하면서 동시에 초기값을 지정하는 경우
  2. 초기화 메서드 내에서 프로퍼티의 초기값을 지정하는 경우

이것을 의미하느 바는 클래스, 구조체의 모든 프로퍼티는 적어도 인스턴스가 생성되는 시점까지는 반드시 초기화디어야 한다는 것이다. 위 두 가지가 아니라면 옵셔널 타입으로 선언되어 자동으로 nil을 할당받게 해야 한다.

 

구조체는 모든 프로퍼티의 값을 인자값으로 입력받아 초기화하는 기본 초기화 구문을 자동으로 제공한다. 프로퍼티를 보통 멤버 변수라고 부르므로, 이 초기화 구문을 '멤버와이즈 초기화 구문'(Memberwise Initializer)라고 부르기도 한다.

// width와 height에 매개변수를 지정하여 Resolution 인스턴스 생성
let defaultRes = Resolution(width: 1024, height: 768)

 

앞에서 구조체로 정의한 Resolution의 멤버와이즈 초기화 구문이다. 이 구문은 Resolution 구조체가 가지고 있는 두 개의 프로퍼티 width, height를 초기화하기 위한 인자값을 입력받아, 내부적으로 프로퍼티를 초기화 한다.

print("width : \(defaultRes.width), height : \(defaultRes.height)")

// 실행 결과
// width : 1024, height : 768

 

이처럼 멤버와이즈 초기화 구문은 인스턴스를 생성하는 형식을 정의할 뿐 아니라, 입력된 인자값을 이용하여 프로퍼티를 초기화하는 과정까지 알아서 처리한다.

 

멤버와이즈 초기화 구문 외에도 구조체의 인스턴스를 생성할 때 사용할 수 있는 초기화 구문은 하나가 더 있다. 최초에 Resolution 인스턴스를 만들 때 사용했던, 빈 괄호 형식이다. 이 초기화 구문은 아무 인자값도 입력받지 않으며, 따라서 어떤 프로퍼티도 초기화하지 않는다. 단순히 구조체 인스턴스를 생성하는 역할만 할 뿐이다. 따라서 이 초기화 형식을 사용하려면, 객체의 모든 프로퍼티는 선언과 동시에 초기값이 지정되어 있어야 한다.

 

클래스의 초기화

클래스는 구조체와 달리 멤버와이즈 형식의 초기화 구문이 제공되지 않는다. 클래스에서 제공하는 것은 빈 괄호 형태의 기본 초기화 구문뿐이다. 만약 초기화되지 않은 프로퍼티가 있다면 기본 초기화 구문은 사용할 수 없으며, 이 때에는 직접 초기화 구문을 정의해서 내부에서 해당 프로퍼티를 초기화해 주어야 한다. 물론 모든 프로퍼티의 초기값을 지정했다면 별도의 초기화 구문을 정의할 필요는 없다. 클래스의 초기화에 대해서는 아래 두 원칙을 지키는게 좋다.

  1. 모든 프로퍼티는 정의할 때 초기값을 주던가, 아니면 옵셔널 타입으로 선언한다.
  2. 인스턴스를 생성할 때에는 클래스명 뒤에 ()를 붙여준다.

프로퍼티와 초기화 구문에 대해 명확한 이해가 생기기 전까지는 위의 두 원칙을 지키는 게 좋다.

 

구조체의 값 전달 방식 : 복사에 의한 전달

구조체와 클래스의 결정적 차이 중 하나가 바로 값을 전달하는 방식이다. 구조체는 인스턴스를 생성한 후 이를 변수나 상수에 할당하거나 함수의 인자값으로 전달할 때 값을 복사하여 전달하는 방식을 사용한다. 이를 값 타입(Value Type), 또는 복사에 의한 전달이라고 한다. 스위프트에서 제공하는 정수, 문자열, 배열 또는 딕셔너리 등 기본 자료형은 모두 복사를 통해 값이 전달된다. 이는 이들 자료형이 구조체로 구현되었기 때문이다.

 

스위프트에서 구조체는 값 타입이다. 이 말은 우리가 생성하는 모든 구조체 인스턴스들이 상수나 변수에 할당될 때 복사된다는 의미이다. 물론, 구조체로 정의된 인스턴스들이 함수의 인자값으로 사용될 때도 마찬가지이다.

 

구조체 인스턴스를 변수에 대입하면 기존의 인스턴스가 그대로 대입되는 것이 아니라 이를 복사한 새로운 값이 대입된다. 따라서 변수에 대입된 인스턴스와 기존의 인스턴스는 서로 독립적이다. 인스턴스를 할당한 후 기존 인스턴스나 할당된 쪽의 인스턴스에 무언가 변경이 발생하더라도 서로에게 전혀 영향을 미치지 않는다.

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

 

Resolution은 구조체이므로 hd를 cinema에 대입하는 시점에서 기존 인스턴스의 복사본이 하나 더 만들어진 다음, 이 복사본이 cinema 변수에 대입된다. hd와 cinema는 같은 width 와 height값을 가지고 있지만 값만 같을 뿐 실제로는 별개의 인스턴스가 대입되어 있다.

 

아래는 cinema 변수에 할당된 인스턴스의 프로퍼티를 변경하는 예제 코드이다.

cinema.width = 2048
print("cinema 인스턴스의 width 값은 \(cinema.width)입니다.")

// 실행결과
// cinema 인스턴스의 width 값은 2048입니다.

print("hd 인스턴스의 width 값은 \(hd.width)입니다.")

// 실행 결과
// hd 인스턴스의 width 값은 1920입니다.

 

원래의 값이 바뀌지 않은 그대로이다. cinema 인스턴스에서 발생하는 값의 변경은 hd 인스턴스에 아무런 영향을 미치지 못하고, 마찬가지로 hd 인스턴스에 변경이 발생하더라도 cinema 인스턴스에는 아무런 영향을 미치지 않는다. 단순히 값의 변경 뿐 아니라 값이 소멸해도 마찬가지이다. 둘은 서로 다른 길을 걷고 있는 분리된 인스턴스들이기 때문이다.

 

이러한 구조체의 특성은 인스턴스를 상수에 할당할 것인지 변수에 할당할 것인지에도 영향을 미친다. 구조체 인스턴스가 상수에 할당되면 프로퍼티의 값을 변경할 수 없다. 값을 변경하려면 변수에 할당해야 한다.

 

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

값이 복사에 의해 전달되는 구조체와는 달리, 클래스는 메모리 주소 참조에 의한 전달 방식을 사용한다. 이를 참조 타입(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로 처리된다.

 

다음에 해당되면 클래스가 아닌 구조체를 사용하는 것이 좋다.

  • 서로 연관된 몇 개의 기본 데이터 타입들을 캡슐화하여 묶는 것이 목적일 때
  • 캡슐화된 데이터에 상속이 필요하지 않을 때
  • 캡슐화된 데이터를 전달하거나 할당하는 과정에서 참조 방식보다는 값이 복사되는 것이 합리적일 때
  • 캡슐화된 원본 데이터를 보존해야 할 때

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

'swift' 카테고리의 다른 글

스위프트 메소드  (0) 2024.04.16
스위프트 프로퍼티  (0) 2024.04.15
스위프트 클로저  (0) 2024.04.12
스위프트 일급 객체로서의 함수  (0) 2024.04.12
스위프트 매개변수  (1) 2024.04.09