2014년 7월 10일 목요일

Swift - let(상수선언)에 대해 파고들기

스위프트(Swift)의 변수와 상수 선언에 관한 글에서 이미 var 와 let에 대해 거론한 적이 있다. 기본적으로 이 둘은 ‘변수'와 ‘상수’를 선언하기 위한 명령어이다. 여기서 let 이라는 상수를 선언하기 위한 명령어에 대해 아주 약간만 더 깊이(?) 파고들어 가볼까 한다.

let의 일반적인 정의

굳이 다시 설명이 필요할까 싶지만, let은 상수 타입의 심볼을 정의하고 이 심볼에 값을 대입하기 위한 용도로 사용한다. 한번 값이 지정(assign)되면 다시는 값을 바꿀 수 없다.
let value = 10

// 아래 코드는 에러가 발생한다.
value = 20
이 예제가 let의 성질(?)을 보여준다.

레퍼런스의 변경

상수의 특징을 생각할 때는 C의 포인터 타입 변수와 비슷하게 생각하자. let은 이미 지정된 해당 심볼의 값을 바꿀 수 없다라고 했지만, 정확히 말하자면 해당 심볼은 특정 메모리 레퍼런스를 값으로 가지게 되는데 이 레퍼런스 포인터가 한번 지정되면 바꿀 수 없다는 의미이다. 이 말을 다르게 해석하면, 포인터를 바꾸는 것만 금지되는 것이지 해당 포인터가 가리키는 오브젝트의 멤버를 엑세스 하는 것은 간섭하지 않는다라는 의미이다.
class SomeClass {
    var value = 0
    
    init(_ value: Int) {
        self.value = value
    }
}

let someObj = SomeClass(10)

// 아래 코드는 문제 없이 실행된다.
someObj.value = 20
여기서 보면 someObj 라는 상수를 만들고 이 상수가 SomeClass(10)의 오브젝트를 가지도록 코딩되었다. 하지만 someObj의 포인터 자체가 변경되는 것이 아닌 someObj 내부의 프로퍼티를 변경되는 것 까지는 막지 않는다.

과연 이것이 상수(Constant)의 정의와 맞는가 틀린가. 답은 사람마다 다를 것 같다.

하지만 struct의 경우는 다르다.

이젠 struct 로 구성된 예제를 보자.
struct SomeType {
    var value = 0
    
    func printValue() {
        println(value)
    }

    mutating func changeValue(v: Int) {
        value = v
    }
}

let someIns = SomeType()

someIns.printValue()

// 아래 코드는 에러가 발생한다:
someIns.changeValue(100)
let으로 선언된 someIns 라는 상수는 SomeType() 이라는 구조체의 인스턴스를 가지게 되었다. 앞서 본 예제와 비슷하게 생각한다면, changeValue() 라는 메소드를 실행시키는 것에는 문제가 없어야 한다. 하지만 이번에는 '상수를 변경하려 했기 때문에 안돼!' 라는 식의 오류가 발생한다.

여기서 struct와 class와의 중요한 차이점 중 하나로 ‘mutating’ 이라는 키워드를 꼽을 수 있다. struct 만이 가지게 되는, 멤버 프로퍼티를 수정하는 메소드에 붙여줘야 하는 키워드가 바로 mutating이다.

자 결론적으로, let으로 선언된 상수는 mutating func 의 사용을 상수 변경 행위로 파악하고 컴파일 타임에 미리 차단한다.

이번에는 과연 '상수'라는 정의에 어울리는가 안어울리는가? 역시나 답은 정해져 있지 않다.

또 다른 예제

이번에는 Core Foundation에 정의되어 있는 수 많은 타입 들 중 하나를 테스트 해 보자. NSMutableArray는 변경 가능한 배열(Array)을 위한 타입이다.
let ma = NSMutableArray()

// 아래 코드는 문제없이 실행된다.
ma.addObject("E1”)
여기서 우리는 NSMutableArray가 struct가 아닌 class로 선언되어 있구나 라고 파악이 가능해진다. 사실 Core Foundation 쪽은 Objective-C를 위해 만들어진 프레임워크라서 struct는 C의 struct와 동일하게 메소드를 가질 수 없다. (물론 함수 포인터는 예외). 그래서 대부분의 타입은 클래스로 만들어져 있고 그걸 이번 테스트로 확실하게 알 수 있었다.

다시한번 더 정리

let 은 레퍼런스 변경이 금지되는 상수형(Constant) 심볼을 정의할 때 사용한다. 따라서 해당 심볼의 값이 바꾸려는 행위는 컴파일 타임 에러를 발생시킨다.

이런 상수의 값으로 구조체(struct) 타입의 인스턴스가 값으로 지정될 경우 mutating func의 사용 또한 상수를 변경하는 행위로 파악하고 금지시킨다.

하지만 class 오브젝트가 상수의 값으로 지정된 경우에는 method 사용이나 프로퍼티 엑세스에 별 다른 제한이 없다.

왜?!

앞서 여러 글과 커뮤니티에 mutating은 왜 존재하고 왜 class에는 지원되지 않고 struct 에만 지원되느니 뭐니 불만을 토로했던 적이 많았다. 그런데 이번 글을 쓰는 과정에서 일단 mutating 자체의 존재 의미는 알게 되었다.

하지만, 'mutating 의 존재 의미’ 만 알게 된 것이지, 왜 struct와 class의 취급이 다르냐에 대해서는 여전히 의문이 남는다. 애초에 let의 존재의미가 class와는 맞지 않게 되어 있는데, 이는 기존 코드 호환성을 위한 것일까 아니면 의도일까.

내가 내릴 수 있는 판단은 여전히 ‘Swift는 미완성이기 때문' 이다. 아마 시간이 흐르면 뭔가 변화가 생기리라 생각된다.

시간이 지나니 뭔가 개념이 잡히게 되는 것 같다. struct 는 값(value)을 담기 위한 특수한 타입을 생성하기 위한 용도인데 이 녀석은 let 과 함께 immutable 한 특성을 가지게 하는데 중요한 역활을 한다. 이 내용은 '언제 class 대신 srtruct 를 사용하는가' 글을 참고하자.

[관련글] Swift - 변수와 상수 그리고 타입
[관련글] Swift - 구조체(Structure) 훑어보기
[관련글] Swift - 클래스(Class) 훑어보기
[관련글] Swift - 메소드(Method)
[관련글] Swift - 언제 class 대신 struct 를 사용하는가
[관련글] 스위프트(Swift) 가이드

댓글 4개 :

gonzales_eat :

'레퍼런스의 변경' 항목에서 someObj.value = 10 으로 수정하셔야 할 것 같습니다^^;;

Seorenn :

@gonzales_eat
해당 내용은
'let 으로 선언한 someObj 의 프로퍼티인 value 의 값을 바꾸려고 해도 아무 문제가 없다'
를 설명하려는 것이지 value 의 값이 얼마다 라는걸 의미하는게 아닙니다.

음... 제가 이해가 어렵게 써 놓은 것일지도 모르겠네요.

ryun :

음 그러니까 포인터가 변경될수 없다는건 변수가 가리키는 메모리의 주소값이 변경될수 없다는 뜻으로 이해했습니다.
그런데 let val = 10으로 선언,초기화 한 것을 val = 20을 했을때 에러가 발생한다고 하셨죠.
이것을 val이 가리키는 메모리 영역에 저장된 10이라는 값이 20으로 바뀌는게 아닌 20이라는 값이 저장된 메모리의 포인터 주소로 바꿔주는 것이라서 에러가 발생한다라고 이해하고 있으면 될까요?

Seorenn :

@정동륜: 이 글의 핵심은 struct 이냐 class 냐에 따라 let 으로 선언된 상수의 성격이 어떻게 변하는지를 보여주는 것입니다. 즉 class 인스턴스가 let 으로 선언되었다면 언급하신 대로 레퍼런스 주소가 고정된다라는 의미로 이해할 수 있습니다. 하지만 struct 인스턴스일 경우는 레퍼런스가 아닌 데이터 자체의 고정을 의미하게 되므로 그 의미가 다르다고 할 수 있습니다.