활동 내용

  • 계산기 프로젝트 코딩 전 Class Diagram 설계

학습내용, 고민한 점

포스팅 링크

구조체와 클래스, 그리고 프로퍼티의 상수와 변수 선언에 따른 값 변경 가능 여부에 대한 고찰

구조체와 클래스를 학습할 때 구조체는 값 타입, 클래스는 참조 타입이라 차이점이 발생하는 경우가 있었습니다. 실제 메모리에서 어떤 일이 발생하고, 이에 따라 사용과 성능 상 어떤 차이점이 발생하는지는 차차 공부해 가더라도 실제 코드를 작성하는 과정에서 단순히 이용 가능한지, 값의 변경이 가능하고 불가능한 경우는 어떤 경우인지를 먼저 알아보기로 하였습니다.

경우의 수는 타입 인스턴스 생성 시 상수와 변수로 선언하는 2가지와 내부 프로퍼티를 상수와 변수로 선언하는 2가지로 총 4가지 경우가 있습니다. 여기에서 클래스와 구조체 2가지 타입을 확인해보아야 하니 결과는 8개가 나올 것입니다. 정리해보면 아래와 같습니다.

  • 클래스 또는 구조체 (2 가지 경우)
  • 인스턴스 생성 시 상수, 변수로 선언한 경우 (2 가지 경우)
  • 타입 내부 인스턴스 프로퍼티를 상수, 변수로 선언한 경우 (2 가지 경우)

선언 방식에 따른 구조체 (Structure)의 값 변경 가능 여부

먼저 타입이 구조체의 경우를 확인해보겠습니다. 타입을 먼저 정의해줍니다.

struct PersonStructure {
    let name: String
    var age: Int
    var heightInCM: Double?

//    mutating func changeName(to name: String) {
//        self.name = name // 에러 발생! 상수형으로 선언된 인스턴스 프로퍼티를 변경할 수 없습니다.
//    }

    mutating func changeAge(to age: Int) {
        self.age = age
    }
    
    mutating func changeHeight(to height: Double) {
        self.heightInCM = height
    }
}

PersonStructure 구조체는 각각 상수와 변수로 선언된 인스턴스 프로퍼티인 nameage를 가지고 있고, 값 타입인 자신의 인스턴스 프로퍼티를 인스턴스 메서드로 변경하기 위한 mutating func를 하나 가지고 있습니다. 주석 처리된 한 가지 mutating funcchangeName(to:)name 인스턴스 프로퍼티가 상수로 선언되어 있으므로 사용할 수 없는 인스턴스 메서드입니다.

계속해서 진행해보겠습니다.

let personStructure1 = PersonStructure(name: "Kio", age: 23)
var personStructure2 = PersonStructure(name: "Steven", age: 21)

personStructure1personStructure2 인스턴스는 동일하게 PersonStructure 구조체를 통해 생성된 인스턴스입니다. 하지만 각각 상수와 변수로 선언된 것이 유일하게 다른 점입니다.

먼저 상수형으로 선언된 personStructure1 인스턴스의 값 변경을 시도해볼까요?

personStructure1.name = "KangKyung" // 에러 발생! person1은 상수형 인스턴스이므로 변경할 수 없습니다.
personStructure1.age = 5 // 에러 발생! person1은 상수형 인스턴스이므로 변경할 수 없습니다.
personStructure1.changeAge(to: 7) // 에러 발생! person1은 상수형 인스턴스이므로 mutating 메서드를 사용할 수 없습니다.
personStructure1 = personStructure2 // 에러 발생! person1은 상수형 인스턴스이므로 다른 인스턴스를 할당할 수 없습니다.

위 코드는 주석을 참고해보면 모두 에러가 발생하는 코드임을 알 수 있습니다. 기본적으로 let으로 선언된 구조체의 인스턴스는 인스턴스 본인을 포함하여 내부 인스턴스를 변경할 수 없고, mutating func을 활용한 내부 프로퍼티 변경도 허용하지 않습니다. 값 타입인데다 상수형으로 선언했으니 살펴본 작동 양상이 직관적으로 이해됩니다.
구조체의 인스턴스를 상수형으로 선언하는 경우가 있는지 앞으로 경험해 보아야겠지만, 이 경우 인스턴스 프로퍼티의 타입을 옵셔널로 설정하고 초기화시키지 않는 실수를 하면 안되겠네요. 향후에 값 할당이 불가능하니 계속해서 nil일테니까요.

다음으로 변수형으로 선언된 personStructure2 인스턴스를 살펴보겠습니다.

personStructure2.name = "DuckBok" // 에러 발생! name 인스턴스 프로퍼티는 상수형으로 선언되어 있으므로 변경할 수 없습니다.
personStructure2.age = 3 // 21 -> 3
personStructure2.changeAge(to: 8) // 3 -> 8
personStructure2.height = 180.7 // nil -> Optional(180.7)
personStructure2.changeHeight(to: 181.1) // Optional(180.7) -> Optional(181.1)
personStructure2 = personStructure1

위의 예시를 살펴보면 상수형으로 선언된 인스턴스 프로퍼티인 name을 제외한 모든 프로퍼티와 인스턴스 자신의 변경이 가능함을 확인할 수 있습니다.

구조체에 대해 알아본 바를 중간 정리하면 아래와 같습니다.

선언 방식에 따른 클래스 (Class)의 값 변경 가능 여부

계속해서 클래스의 경우를 살펴보도록 하겠습니다. 구조체의 경우와 같이 타입을 먼저 정의하겠습니다.

class PersonClass {
    let name: String
    var age: Int
    var height: Double?
    
//    func changeName(to name: String) {
//        self.name = name // 에러 발생! 상수형으로 선언된 인스턴스 프로퍼티를 변경할 수 없습니다.
//    }
    
    func changeAge(to age: Int) {
        self.age = age
    }
    
    func changeHeight(to height: Double) {
        self.height = height
    }

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

PersonClass는 구조체의 경우와 마찬가지로 상수형 인스턴스 프로퍼티인 name과 변수형 인스턴스 프로퍼티 age, height를 가지고 있습니다. height는 옵셔널 타입이며, 이니셜라이저에 의해 초기화되지 않았으므로 향후 별도로 값을 변경해주지 않는 한 계속해서 nil이 할당되어 있을 것입니다. 클래스는 참조 타입이므로 인스턴스 메서드를 통해 인스턴스 프로퍼티를 변경하더라도 메서드에 mutating 키워드를 작성하지 않아도 됩니다. 다만, changeName(to:) 메서드의 경우 상수형 인스턴스 프로퍼티를 변경하려 하고 있으므로 에러로 인해 작동하지 않습니다 (애초에 컴파일도 되지 않겠지요).

계속해서 인스턴스를 생성해볼까요?

let personClass1 = PersonClass(name: "Summer", age: 20)
var personClass2 = PersonClass(name: "Coda", age: 25)

personClass1, personClass2 인스턴스를 생성하고 각각 상수형과 변수형으로 선언했습니다. 구조체 예시에서 해본 것과 같이 각자의 프로퍼티와 본체를 변경해보겠습니다. 먼저 personClass1!

personClass1.name = "Coda" // 에러 발생! name은 상수형 인스턴스 프로퍼티이므로 변경할 수 없습니다.
personClass1.age = 3 // 20 -> 3
personClass1.changeAge(to: 10) // 3 -> 10
personClass1.height = 185.3 // nil -> Optional(185.3)
personClass1.changeHeight(to: 186.0) // Optional(185.3) -> Optional(186.0)
personClass1 = personClass2 // 에러 발생! personClass1은 상수형으로 선언되어 있으므로 다른 인스턴스를 할당할 수 없습니다.

위의 사례를 보면 클래스는 인스턴스가 상수형으로 선언되어 있더라도 인스턴스 프로퍼티가 변수형으로 선언되어 있으면 값을 변경할 수 있네요. 당연히 상수형으로 선언된 프로퍼티는 변경할 수 없었습니다. 그리고 인스턴스 자체에 다른 인스턴스를 할당할 수 없도록 제한하고 있네요. 클래스 인스턴스의 상수형 선언은 인스턴스 자신이 참조하는 주소를 변경할 수 없는 상수로 여기고 있는 것으로 판단됩니다.

그럼 변수형으로 선언된 클래스 인스턴스의 사례를 볼까요?

personClass2.name = "Summer" // 에러 발생! name은 상수형 인스턴스 프로퍼티이므로 변경할 수 없습니다.
personClass2.age = 3
personClass2.changeAge(to: 10)
personClass2.height = 185.3
personClass2.changeHeight(to: 186.0)
personClass2 = personClass1

이번에는 클래스 인스턴스 본인이 다른 인스턴스로 대체 되어도 에러가 발생하지 않네요. 아무래도 personClass1 인스턴스에서 살펴본 바와 같이 클래스 인스턴스 선언 시 varlet은 인스턴스 본인의 주소가 변할 수 있는지 없는지 여부를 이야기하나 보네요.

그럼 선언 방식에 다른 클래스의 값 변경 가능 여부까지 살펴보았으니 클래스도 한 번 정리해보겠습니다.

페이지 스크롤이 번거로우니 구조체 정리 내용을 다시 가져오겠습니다.

표를 보니 구조체와 클래스 인스턴스는 타입의 인스턴스 프로퍼티를 변수형(var)로 선언하고 인스턴스 생성 시 상수형(let)으로 선언했을 때 가장 큰 차이를 보이네요. 값 타입인 구조체는 인스턴스 프로퍼티의 변경을 할 수 없었는데, 참조 타입인 클래스는 비록 인스턴스가 상수형이더라도 프로퍼티 변경이 가능했습니다.

결론

현상에 대한 이해는 차차 해나가야겠지만, 현재로써는 값 타입과 참조 타입의 차이로 인해 발생한 현상으로 판단됩니다. 값 자체를 복사하여 인스턴스를 생성하는 구조체의 경우 내부 프로퍼티를 변경하는 것이기 때문에 인스턴스를 상수형으로 선언하면 프로퍼티의 변경이 불가능하지만, 값을 참조하는 방식으로 생성되는 클래스 인스턴스는 본체의 인스턴스 프로퍼티의 값을 변경하더라도 인스턴스 본인이 참조하는 주소를 변경하는 것이 아니기 때문에 값의 변경이 가능한 것으로 판단됩니다.

profile
합리적인 해법 찾기를 좋아합니다.

0개의 댓글