캡슐화란 데이터에 대한 보호와 은닉을 목표로 내부의 속성을 숨기고 독립적인 하나의 캡슐을 만드는 것을 의미합니다.
단순히 속성을 감추는 것이 아니라 내부의 중요한 속성을 감추면서 데이터의 무결성을 보장하고 결합도를 낮추어 코드의 유연성을 증가시키는데 의의가 있습니다.
캡슐화를 거치게 되면 직접적인 데이터의 조작을 방지할 수 있습니다.
이로 인해 안정성과 일관성을 가질 수 있으며 데이터를 보호할 수 있습니다.
또 객체 내부 구현에 변화가 생기더라도 외부에 제공되는 인터페이스가 동일하다면 객체를 사용하는 외부의 코드에 대한 수정사항은 줄어들게 됩니다.
이렇게 내부 데이터를 숨김으로써 추상화를 강화하게 됩니다.
단, 캡슐화가 필요이상으로 진행될 경우 코드가 과도하게 분리되고 클래스가 비대해지게 됩니다. 이로 인해 복잡성이 증가하고 코드 간의 의사소통도 복잡해질 수 있습니다. 더불어 캡슐화된 클래스나 구조체의 책임도 과도하게 증가할 수 있습니다.
스위프트에서 캡슐화는 접근 제어자, 연산 프로퍼티, 프로퍼티 래퍼 등으로 구현될 수 있습니다.
접근 제어는 open
public
internal
fileprivate
private
등으로 나뉩니다. 이중 주로 사용되는 private
은 가장 개방성이 낮은 제어자로 동일한 클래스나 구조체 안에서만 접근이 가능합니다.
따라서 완전한 캡슐화를 원할 때 사용하게 됩니다.
추가로 internal
은 별도로 명시하지 않은 경우 갖게 되는 기본적인 접근제어자로 동일 모듈 내에서만 접근이 가능합니다. open
과 public
은 모듈이 다르더라도 접근이 가능하나 open
에서만 다른 모듈에서의 상속과 오버라이딩이 가능합니다.
private
을 통해 외부로부터의 접근을 막을 수 있으며 private(set)
이라는 제어자를 이용한다면 set
에 대해서만 보호하고 해당 프로퍼티의 값을 얻을 때는 internal
수준으로 접근할 수 있게 됩니다.
이렇게 직접적인 수정만 막은 프로퍼티는 별개의 메서드나 getter
없이 바로 속성에 접근하여 값을 읽을 수 있습니다.
숨겨진 속성에 대한 수정이 필요할 때 메서드 또는 연산 프로퍼티를 통해 수정, 접근하도록 허용할 수 있습니다. 처음 공부를 할 때는 연산 프로퍼티와 메서드를 사용하는 것에 차이가 커보이지 않았습니다.
실제론 기능적으로 차이가 존재했지만 가장 큰 차이는 속성이냐 메서드냐를 통해 코드에 드러나는 의도라고 생각합니다.
연산 프로퍼티를 사용하게 된다면 접근 시 하나의 속성처럼 접근하게 됩니다. 이로 인해 코드의 일관성을 좀 더 증진시키고 데이터 자체에 변경을 가한다는 느낌보다는 의도에 맞는 값을 얻는다는 느낌이 강하다고 느꼈습니다.
또 값을 읽기만 할 때, 특정 계산이 들어간 값을 반환하려는 경우에 가장 적합하다고 생각합니다.
메서드를 사용한다면 메서드 내부에서 데이터의 변화나 어떤 로직이 동작할 것이라는 느낌이 강했습니다.즉 내부 코드의 의도를 좀 더 명확히 전달하는 것이었습니다.
예를 들어 내가 빌린 돈의 이자를 얻는 행위를 할 때 다음과 같은 코드가 생깁니다.
여기서 동일하게 간단한 월간 이자가 계산되지만 접근 시 monthlyInterest
로 접근하는 것과 calculateMonthlyInterest()
로 접근하는 것은 느낌이 사뭇 다릅니다.
이러한 전달의 차이가 제가 느낀 가장 큰 차이입니다.
프로퍼티 래퍼는 이름 그대로 프로퍼티를 감싸는 것으로 이해하면 좋을 듯 합니다.
프로퍼티를 감싸서 추가적인 기능을 제공해주는 기능입니다.
@propertyWrapper
struct LimitRange <Value: Numeric & Comparable> {
private var value: Value
private let range: ClosedRange<Value>
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
var wrappedValue: Value {
get { return value }
set {
if range.contains(newValue) {
value = newValue
}
}
}
}
위와 같이 숫자 타입에 범위의 제한을 두는 프로퍼티 래퍼를 정의해보았습니다.
프로퍼티 래퍼 내부의 값은 모두 접근이 불가능하고 wrappedValue
를 통해서만 값을 얻고 설정할 수 있습니다. 즉 앞서 나온 접근제어와 연산프로퍼티로 내부 로직이 숨겨지고 일반적인 속성에 접근하듯이 사용하도록 캡슐화가 적용된 것입니다.
프로퍼티 래퍼는 projectedValue
라는 예약어를 통해서 추가적인 정보에 $
를 통해 접근할 수 있기도 합니다. 이 부분은 프로퍼티 래퍼에 대한 글을 따로 작성해보겠습니다.
struct Car {
@LimitRange(0...120) private(set) var speed: Int = 0
init(speed: Int = 0) {
self.speed = speed
}
func changeSpeed(to newSpeed: Int) -> Car {
var newCar = self
newCar.speed = newSpeed
return newCar
}
}
이제 캡슐화를 구현해둔 프로퍼티래퍼로 speed
를 감싸는 것만으로 speed
에 대한 값 변경에 대해 제한이 생기고 해당 제한의 세세한 내용을 사용자는 알 수 없게 처리되었습니다.
또한 캡슐화된 유효성 검사를 통해 speed
의 값이 범위를 벗어나지 않을 것이라는 데이터 무결성이 보장됩니다.
동일한 캡슐화 로직이 반복되는 부분을 프로퍼티 래퍼로 만들어 사용하면 코드를 더 간결하고 보기 쉽게 작성하면서 캡슐화를 유지할 수 있습니다.
var myCar: Car = Car()
myCar = myCar.changeSpeed(to: 80)
print("speed: \(myCar.speed)")
print("-----------------------------")
myCar = myCar.changeSpeed(to: 200)
print("speed: \(myCar.speed)")
print("-----------------------------")
myCar = myCar.changeSpeed(to: 100)
print("speed: \(myCar.speed)")
print("-----------------------------")
myCar = myCar.changeSpeed(to: 200)
print("speed: \(myCar.speed)")
print("-----------------------------")
얼마만이에요 작가님.... 눈물나요