Swift의 구조체(struct), 속성(property) 그리고 메서드(method)에 대해서 알아보겠습니다.
구조체 기본
Swift는 자신만의 타입을 디자인하는 방법으로 구조체 기능을 제공합니다. 이 구조체에 자기만의 변수, 상수 그리고 함수를 정의할 수 있죠.
이제 간단한 구조체를 만들어 보겠습니다. 구조체의 이름은 Movie
이며 영화 이름을 변수로써 속서을 갖습니다 :
이제 이 구조를 타입으로 하는 인스턴스를 생성해보겠습니다 :
구조체의 속성 title
과 구조체의 인스턴스 eternalSunshine
모두 변수로 생성하였기 때문에 값을 변경할 수 있습니다 :
만일 둘 중 하나라도 상수로 설정되어 있다면 값을 변경할 수 없습니다 :
다른 속성의 값에 따라 동적으로 속성의 값을 설정할 수도 있습니다. 이를 computed property
라고 합니다. 영화 자막이 존재하는지 여부를 담은 caption
속성을 추가하고 로그에 출력해보겠습니다 :
Computed 속성, Stored 속성 어느 것을 사용하는 것이 좋을까요?
구조체에서 정보를 저장하는 속성에는 Stored 속성과 Computed 속성이 있습니다. Stored 속성을 정보를 메모리 어딘가에 저장한 다음 필요할 때 사용하는 속성이며, Computed 속성은 그때 그때 다시 계산하여 사용하게 됩니다.
어느 속성을 사용할지 결정하는 기준은 “다른 데이터의 의존”과 “성능” 입니다.
성능을 기준으로 하였을 때, 속성의 값이 변경되지 않고 속성을 조회하는 경우가 많은 때에는 Stored 속성이 유리한 반면, 속성을 드물게 조회하는 경우에는 Computed 속성이 유리합니다.
다른 데이터와의 의존성을 기준으로 했을 때는, Computed 속성이 유리합니다. 프로그램의 최신 상태를 반영해서 정보를 조회할 수 있기 때문이죠.
한편 Lazy 속성이라는 것도 존재하는데, 이는 거의 사용하지 않는 Stored 속성의 성능 문제를 어느정도 해결하고, 속성 옵저버는 Stored 속성의 의존성 문제를 어느정도 해결합니다.
속성 옵저버
속성 옵저버는 속성이 변경되기 전, 후에 코드를 실행할 수 있는 기능을 제공합니다.
앞서 만든 Movie
구조체에 사용자가 영화를 어느정도 보았는지 나타내는 progress
속성을 추가하고, 관찰해보겠습니다 :
progress
의 값이 변경될 때 마다 didSet
속성 옵저버는 변화를 감지하고 내부의 코드를 실행합니다. didSet
외에도 속성이 변하기 전 코드를 실행하는 willSet
이 존재하지만 잘 사용되지는 않습니다.
속성 옵저버를 사용하지 않고 값이 변경될 때 마다 함수를 실행하면 될텐데, 왜 속성 옵저버를 사용할까요?
바로 속성이 변경되면 자동으로 실행되기 때문에 사용합니다. 속성이 변경되었는데 함수를 실행하는 것을 깜빡하면 찾기 힘든 버그가 발생할 여지가 있죠.
하지만 속성 옵저버 내부에 너무 느린 작업을 넣게되어도 속성이 변경되는데 지나치게 오랜 시간이 소요되는 문제가 발생합니다. 이 역시 좋은 결과는 아니죠.
메서드
구조체에 내부에 함수를 정의할 수 있는데, 이 함수는 ‘메서드’라고 불립니다.
이 메서드는 속성에 접근 가능하며 이 속성을 이용하여 가공된 데이터를 반환하는 등의 작업을 할 수 있죠.
Movie
구조체에 상영시간을 밀리초로 표현한 runnigTimeInMs
와 남은 상영시간을 반환하는 getRemainRunningTimeInMin
메서드를 추가하고 실행해보겠습니다 :
이처럼 메서드는 구조체, 열거형 등의 내부에서 무언가 기능을 제공하는 역할을 합니다.
Mutating 메서드
기본적으로 Swift는 특별한 요청 없이는 메서드가 내부의 속성을 변경하는 것을 허용하지 않습니다 :
대신 메서드가 속성 값을 변경 가능하도록 허용하기 위해서는 메서드 앞에 mutating
키워드를 적어주어야 합니다 :
이제 메서드 내에서 속성의 값을 변경할 수 있습니다 :
Swift의 핵심 타입은 대부분 구조체로 이루어져 있습니다.
문자열, 정수, 배열, 딕셔너리, 불리언 등 많은 Swift의 타입들이 구조체로 이루어져 있습니다.
문자열을 예시로 들면 :
위와 같이 사용할 수 있는 것은 문자열 타입은 구조체이며, count
속성과 uppercased
메서드가 존재하기 때문이죠.
이니셜라이저
구조체를 만들고 인스턴스를 생성하려면, 인스턴스의 초기 값을 함께 전달해주어야 합니다 :
이렇게 User
구조체의 인스턴스를 생성할 때 속성의 값을 넘겨주지 않고, 자신만의 방법으로 속성을 초기화 할 수 있는 방법을 제공하는 것이 init
이라는 이니셜라이저입니다.
위의 User
구조체를 다음과 같이 변경할 수 있습니다 :
init
을 사용하게 되면, User
인스턴스를 생성할 때 name
속성을 위한 값을 넘겨주면 에러가 발생합니다 :
새롭게 전달된 값으로 속성의 기존 값을 덮어씌워주면 되지않을까 싶지만, 실수로 init
에서 초기화 한 값 중 중요한 값을 덮어씌우지 않기 위해 허용하지 않는 것입니다.
위에서 처럼 사용하기 위해서는, 사용자 이니셜라이저를 extension
으로 이동시키면 됩니다 :
현재 인스턴스 참조하기
메서드에는 특별한 상수인 self
에 접근할 수 있습니다. 현재 사용되고 있는 구조체의 인스턴스를 카리키고 있죠.
self
를 사용하면 속성의 이름과 메서드의 파라미터 이름이 동일할 때, 구분할 수 있도록 도움을 줍니다 :
Lazy 속성
성능 최적화를 위해 Swift는 몇몇 속성을 필요할 때에 생성합니다. 예를 들어, 어떤 사람의 가계도를 만드는데는 꽤나 많은 시간이 필요합니다.
먼저 가계도를 의미하는 FamilyTree
구조체를 생성합니다. 여기서는 별다른 작업을 하지 않지만 실제로는 엄청난 작업을 필요로 할 것입니다 :
이제 User
구조체에 familyTree
속성을 만들고 FamilyTree
인스턴스를 할당합니다 :
그런데 사람마다 가계도가 필요할 수도, 그렇지 않을 수도 있습니다. 그렇기 때문에 즉시 초기화를 할 필요는 없죠. 이때 사용하는 것이 lazy
키워드 입니다 :
실제로 사용되기 전까지 속성을 생성하지 않는다는 점에서 Computed 속성과 비슷해 보입니다. 하지만 lazy 속성은 계산 결과를 저장하고, 이어지는 작업에서 다시 접근할 때 재계산 하지 않는다는 점에서 Computed 속성과 차이를 보입니다.
정적 속성과 메서드
지금까지는 모든 속성과 메서드가 구조체의 인스턴스에 속해있었습니다. 이는 인스턴스를 생성할 때마다 각자의 속성과 메서드를 갖게 된다는 의미죠.
여러 인스턴스를 포괄하여 공통적인 속성과 메서드를 사용할 수 있도록 하는 것이 static
입니다.
User
구조체에 각 인스턴스에서 사용할 수 있는 counts
라는 정적 속성을 생성해보겠습니다. 이 속성은 인스턴스가 생성될 때 마다 하나씩 커지도록 설정되어 있습니다 :
이렇듯 정적 속성 및 메서드의 일반적인 사용 용도 중 하나는 전체 앱에서 사용할 수 있는 공통 기능을 저장해두는 것입니다.
접근 제어
접근 제어는 특정 코드에서만 접근이 가능하도록 속성과 메서드를 제어하는 것을 의미합니다. 이는 속성을 바로 접근해서 조회하는 것을 방지하는데 도움이 되죠.
예를 들어, 사용자마다 비밀 코드를 발급해준다고 가정해봅시다. 이와 같은 값은 구조체 내부에서만 접근 가능해야 하죠. 이런 경우 private
키워드를 사용하면 됩니다 :
private
로 설정된 속성에 접근하려고 하면 에러가 발생합니다 :
접근 제어를 사용하면 다른 사람이 코드를 보았을 때 어떤 메서드, 속성을 가져다 쓸 수 있는지 혹은 직접 접근하면 안되는지 파악하기가 쉽다는 장점이 있습니다.
지금까지 구조체, 속성 그리고 메서드에 대해서 알아보았습니다! 😎
참고 자료
100 Days Of Swift : Day8 structs, properties, and methods
100 Days Of Swift : Day9 access control, static properties, and laziness