개발을 하다보면 '상태'라는 단어를 정말 많이 듣게 됩니다. 그렇다면 상태가 뭘까요?
객체 지향 프로그래밍에서는 모든 데이터를 객체(object)로 취급하며, 이러한 객체간에 메시지를 주고 받는게 바로 프로그래밍의 중심이 됩니다.
객체(object)란 간단히 이야기하자면 실생활에서 우리가 인식할 수 있는 사물로 설명할 수 있습니다. 커피 머신, 바리스타 등등...
이러한 객체는 자신만의 상태(state)와 행동(behavior)으로 구성되게 되는데, 이 '상태'를 의미합니다. 각 객체가 지닌 고유한 특성인거죠.
그럼 상태가 가변적이란건 무슨 의미일까요?
상태는 각 객체가 지닌 고유한 특성이라고 설명드렸는데요. 예를 들어보겠습니다.
버튼은 어떨까요? 이왕이면 조금 더 직관적이게 누르기만하면 핵 미사일이 발사되는 버튼이 있다고 생각해봅시다.
이건 버튼이라는 객체이며, 이 객체는 눌림/눌리지 않음의 상태를 가집니다. 코드로 표현하면 이렇게 되겠네요.
class Button(
var isPressed: Boolean = false
)
fun main() {
val button = Button()
button.isPressed = true
}
여기서 isPressed 프로퍼티는 가변성(mutability)을 지니기 때문에 상태를 마음대로 변경할 수 있습니다.
button 객체를 생성한 main 함수에서 상태를 변경할 수도 있고, 객체를 다른 함수로 넘긴 뒤에 해당 함수에서 변경될 수도 있고, .... 변경 가능 지점이 매우 다양합니다.
이렇게 변경 가능 지점이 다양할 경우 몇 가지 문제가 발생할 수 있는데요. (이는 아래에서 자세히 다루도록 하겠습니다.)
그럼 문제가 발생하지 않게 하려면 어떻게 해야할까요?
이 버튼은 권한을 가진 한 명만 누를 수 있고, 만약 누르고 싶다면 그 사람에게 부탁하는 식으로 설계하면 되지 않을까요?
여러 방법으로 풀어낼 수 있지만, 두 가지 정도의 코드 예시를 들어보겠습니다.
각자 장단점이 있고, 예제이다보니 최적의 케이스는 아님을 감안해주세요 😅
class SafetyButton(
val isPressed: Boolean = false
) {
fun press(): SafetyButton {
return SafetyButton(true)
}
fun takeOff(): SafetyButton {
return SafetyButton(false)
}
}
fun main() {
val button = SafetyButton()
val pressedButton = button.press()
val takeOffButton = pressedButton.takeOff()
println(button.isPressed)
println(pressedButton.isPressed)
println(takeOffButton.isPressed)
}
class SafetyButtonV2 {
private var _isPressed: Boolean = false
val isPressed: Boolean
get() = _isPressed
fun press(): Boolean {
this._isPressed = true
return this._isPressed
}
fun takeOff(): Boolean {
this._isPressed = false
return this._isPressed
}
}
fun main() {
val button = SafetyButtonV2()
println(button.isPressed)
button.press()
println(button.isPressed)
button.takeOff()
println(button.isPressed)
}
코드가 더 길고, 번잡해진 것 같지 않나요? 이렇게 까지 하면서 상태의 가변성을 최소화하고, 가변성을 지녔다면 외부에 공개하지 않는 식으로 설계하는 이유가 있을까요?
상태를 가변적으로 관리했을 때 발생하는 문제
1. 프로그램을 이해하고 디버그하기 힘들어진다.
상태가 가변적이라면 이러한 상태를 변경, 조회하는 곳들 간의 관계를 이해해야 하며, 상태 변경이 많아지면 이를 추적하는 것이 힘들어집니다. 이러한 클래스는 이해하기도 어렵고, 이후에 코드를 수정하기도 어렵습니다.
클래스가 예상하지 못한 상황 또는 오류를 발생시키는 경우에는 더욱 큰 문제가 됩니다.
2. 가변성(mutability)이 있으면, 코드의 실행을 추론하기 어려워진다.
코드의 실행 시점(Run time)에 따라서 값이 달라질 수 있으므로 현재 어떤 값을 갖고 있는지 알아야 코드의 실행을 예측할 수 있습니다. 또한 한 시점에 확인한 값이 계속 동일하게 유지된다고 확신할 수 없게됩니다.
3. 멀티스레드 프로그램 일때는 적절한 동기화가 필요하다
변경이 일어나는 모든 부분에서 충돌이 발생할 수 있기 때문에 모든 변경 시점마다 동기화를 해주어야합니다.
4. 테스트하기 어렵다
모든 상태를 테스트해야 하므로, 변경이 많으면 많을수록 더 많은 조합을 테스트해야만 합니다.
5. 상태 변경이 일어날 때, 이러한 변경을 다른 부분에 알려야 하는 경우가 있다.
예를 들어 정렬되어 있는 리스트에 가변 요소를 추가한다면 요소에 변경이 일어날 때 마다 리스트 전체를 다시 정렬해야 합니다. 이 경우 변경 이후 정렬이 되어야한다는 걸 정렬의 역할을 가진 클래스에게 알려야하는 불필요함이 발생합니다.
가변성을 지니지 않은, 불변 객체를 사용했을 때의 장점은 위에서 언급된 문제 대부분을 해결할 수 있습니다.
불변(immutable) 객체 사용의 장점
- 한 번 정의된 상태가 유지되므로, 코드를 이해하기 쉽다.
- 공유했을 때도 충돌이 따로 일어나지 않으므로, 병렬처리를 안전하게 할 수 있다.
- immutable 객체에 대한 참조는 변경되지 않으므로, 쉽게 캐시할 수 있다.
- 방어적 복사본(defensive copy)을 만들 필요가 없다. 또한 객체를 복사할 때 깊은 복사를 따로 하지 않아도 된다.
- 다른 객체를 만들 때 활용하기 좋다. 또한 immutable 객체는 실행을 더 쉽게 예측할 수 있다.
- immutable 객체는 Set 또는 Map의 키로 사용할 수 있다. mutable 객체는 사용할 수 없다.
공유 상태 관리는 매우 어렵습니다. 변경 가능한 부분에 의한 일관성(con-sistency) 문제, 복잡성(complexity) 증가와 관련된 문제 등등 다양한 부분을 신경 써야하는데 변경 가능 지점이 다양하다면 더욱 어려워집니다.
그렇기 때문에 최대한 변경 가능 지점을 노출하지 않고, 가능하다면 무조건 가변성을 제한하는 방향으로 코드를 설계하는 것을 권장합니다.
☕️ Networking
기술 직군의 기술적인 교류, 커리어 이야기, 직군 무관 네트워킹 모두 환영합니다!
위클리 아카데미 오픈 채팅방(비밀번호: 9323)
kakaotalk: https://open.kakao.com/o/gyvuT5Yd