[Effective Kotlin] 아이템1 : 가변성을 제한하라(추가 예정)

0

코틀린은 모듈로 프로그램을 설계한다.

모듈이란 프로젝트 아래의 개념이다.
클래스, 객체, 함수, 타입 별칭(type alias), 톱레벨 프로퍼티 등 다양한 요소로 구성된다.

모듈의 요소중 일부는 상태(state값)을 포합한다.

요소가 상태를 갖는 경우(var 이라던지.. mutable 객체라던지), 이력(history)에도 의존하게 된다.

    val account = BankAccount()
    println(account.balance) //0.00


    account.addDeposit(100) //100
    //히스토리 의존
    println(account.balance) //100
    account.withdraw(50.0)

    println(account.balance) //50
    

상태는 시간에따라 변화하는 요소를 표현할 수 있다는것은 굉장히 유용하나, 상태를 관리하는것이 꽤 어렵다.

  • 프로그램을 이해하기 힘들어지고, 추적하는것이 힘들어지며, 코드를 수정하기 힘들어진다.
  • 가변성(mutability)가 있다면 코드의 실행을 추론하기가 어려워진다.
  • 멀티스레드 프로그램일때 적절한 동기화가 필요하다.
  • 모든 상태를 테스트해야하기 때문에 테스트 하기가 어렵다.
  • 상태변경이 일어날때, 변경을 다른부분에 알려야 하는 경우가 있다.

예를 들어 정렬된 리스트에 요소가 추가되었을때, 또 정렬을 해줘야하는점이 있다.

또한 개발자라면 공유 상태 관리가 얼마나 힘든것인지 알것이다.

아래의 부분의 예제는 Mutex와 관련된 예제이다.


//일반 멀티 스레드
fun runMultiThread() {
    var num = 0
    for (i in 1..1000) {
        thread {
            Thread.sleep(10)
            num += 1
        }
    }
   Thread.sleep(1000)
    print(num)
}

한번에 num이라는값으로 Thread가 동시 접근했을떄 1000이 정확하게 나오지 않는다.
이는 코루틴을 사용해도 마찬가지이다.(물론 쓰레드를 적게쓰기때문에 오차가 줄어들지만..)

fun runMultiThread() {
    var num = 0
    for (i in 1..1000) {
        thread {
            Thread.sleep(10)
            num += 1
        }
    }
   Thread.sleep(1000)
    print(num)
}

결국 이를 해결하려면 synronized lock을 사용하여 동시접근을 막아야한다.
하지만 한 쓰레드가 접근하면 lock()을 걸기 때문에 속도가 훨씬 느려진다.

공유 객체 문제의 포스팅은 이곳에서 확인하자.(추후 추가)


fun runSyncronizedMultiThread() {
    var num = 0
    val lock = Any()
    for (i in 1..1000) {
        thread {
            Thread.sleep(10)
            synchronized(lock = lock){
                num+= 1
            }
        }
    }
    Thread.sleep(1000)
    print(num)
}

가변성은 생각보다 단점이 많아서 이를 제한하는 프로그래밍도 존재하는데 이를 순수함수형 언어라고 한다(가변성에 제약이 너무나 많이 걸린다.)

가변성은 시스템의 상태를 나타내기 위한 중요한 방법이나, 변경이 일어나야 하는 부분을 신중하고 확실하게 결정하고 사용해야한다.

코틀린에서 가변성 제한하기

코틀린은 가변성을 제한할 수 있게 설계 되어있다.

  • 읽기전용 프로퍼티(val)
  • 가변 컬렉션와 읽기 전용 컬렉션 구분하기
  • 데이터 클래스의 copy

1. 읽기전용 프로퍼티

val을 사용해 읽기 전용 프로퍼티를 만들수 있다.

val a = 10
a = 20

하지만 읽기전용 프로퍼티가 mutable 객체를 담고 있다면, 내부적으로 변할 수 있다.

val list = mutableListOf(1,2,3)
list.add(4)

또한 사용자 정의 getter로도 정의가 가능하다.

    var name = "Choi"
    var surname = "Woo"
    
    val fullName : String
    get() = "$name$surname" //Choi woo
    
    surname = "Woo Sung"
    
    fullName = "Choi woo Sung"

val 은 override 할시, var로도 표현이 가능하다.

//var 은 게터와 세터를 모두 제공한다.
//val 은 변경이 불가능하므로 게터만 제공한다.
//val 을 var로 오버라이드 할 수 있다.

interface Element {
    var active : Boolean
    val nonActive : Boolean
}

class ActualElement : Element {
    override var active: Boolean
        get() = TODO("Not yet implemented")
        set(value) { this.active = value }

    override var nonActive: Boolean 
        get() = TODO("Not yet implemented")
        set(value) { this.active = value }
}

중요할것은 valimmutable을 의미하지는 않는다.
만약 완전히 변경할 필요가 없다면 final 프로퍼티를 사용하자.

fullName은 게터로 정의 했으므로 스마트 캐스트를 사용할 수 없다.
fullName2 처럼 지역변수가 아닌 프로퍼티가 final이고, 사용자 정의 게터를 갖지 않을 경우 스마트 캐스트할 수 있다.

class immutableClass(){
    val name :String? = "Choi"
    val surname = "Woo"
    
    val fullName : String?
    get() = "$name$surname"
    
    val fullName2 = name?.let{"$it $surname"}
    
    fun main(){
        if(fullName != null){
            println(fullName.length) //Error
        }
        
        if(fullName2 != null){
            println(fullName2.length)
        }
    }
}

참고

뮤텍스와 세마포어의 차이
코틀린 프로퍼티 디컴파일

profile
쉽게 가르칠수 있도록 노력하자

0개의 댓글