본문 바로가기

우테코/level1(코틀린)

[우테코]02/17 level1 네번째 수업(로또 피드백)

🐗로또 피드백🐗

현재까지 학습테스트, 단위테스트, TDD 맛보기를 진행했으니 이제 계속 미션을 진행해 나가며 TDD에 익숙해져 나가는 과정을 거친다고 한다.

시작하기
- 요구 사항 분성을 통한 기능목록 작성
- 객체 설계를 통해 어느 부분부터 구현을 시작할 것인지 결정

지난 수업에 이야기했듯이 개발이전에 설계가 선행되어야하고 

개발 시작과 동시에 일단 객체를 설계하게된다.(TDD와 함께 코딩하기 전에)

 

설계의 관점

설계를 할때 의인화 하는 방법을 사용하기도 하는데 객체 자체에 인격을 부여하듯이 일상사물에서 모티브를 가져오면 모두가 이해할수있도록 공감을 유도할수있다고한다. 

의인화는 좋은 접근법이니 잘 사용해보자.

 

기능목록
-구매할 Lotto의 매수 구하기
    -1000 -> 1
    -1500 -> 1
    -500 -> error
-한장의 Lotto 생성
-당첨 번호 생성
    -정상적인 당첨 번호 입력
    -유효하지 않은 당첨 번호
-한장의 Lotto에 대한 당첨 결과 구하기

이런식으로 기능목록을 뽑게 될것인데

기능목록을 작성할때 해피케이스만 고려하지말고 

처음부터 예외케이스를 많이 생각하고 고려하고 가야한다고 한다.

-> 안그러면 나중에 오히려 수정하는 과정을 많이 거쳐야 할테니 어려워질것이다.

 

쉬운 기능목록부터 정복해나가면서 어려운 기능목록을 해결하는 방식으로 시작하면된다.

+기능목록은 계속해서 추가될 것이다. 개발해 나가면서 빠진것을 추가해나가자

 

 

TDD로 기능개발할 단위를 쪼개기 

어디서부터 기능을 개발해야할까?

기능들을 쪼개서 한사이클에 돌릴수 있는 단위로 나누는 연습을 해야한다.-> 그중 베이스가 되는 쉬운 기능들부터 만들어나가자

기능을 쪼개놓고 api를 손에 잡히는 대로 띄엄띄엄 만들어나가서 합쳐나가는 것이다. 

api가 어떤 단위로 나눠지는지 파악하고 만들어내는 능력을 길러야한다

각객체들이 분리될테고 그 객체내에서도 각기능들을 쪼개서 단위별로 TDD를 통해 구현해나간다.

 

기능을 쪼갠 예시

- 로또 구매 금액을 전달하면 구매할 수 있는 로또의 장수를 반환한다.
- 구매한 로또와 당첨번호를 넣으면 당첨 결과를 반환한다.
- 당첨 결과를 입력하면 당첨금 총액을 반환한다.
- 당첨 금액과 구매 금액을 넣으면 수익률을 반환한다.

 

 

"한 장의 로또에 대한 당첨 결과 구하기 기능"을 개발해보는 과정으로 전체적인 프로세스를 살펴보자

 

-객체 설계 이런것 어려워서 어떻게 해야할지 감이 안온다면 일단 최상위 함수로 구현해서 리펙토링을 통해서 해결해나가자

-리펙토링 할때는 객체지향 생활체조 원칙, 그리고 맨날 배우는것들 다 적용해서 리펙터링을 하자

 

근데 객체지향 생활체조 원칙이 뭐임?

-> 이거는 소트웍스 엔솔러지라는 책에 원칙인데 이거는 뭐 썰이 이것저것 있다고 킹갓제너럴 제이슨 코치님이 말해줬지만 잡설은 생략하고

객체지향이라는 것은 참 이렇다 하고 명확하게 떨어지는 개념이 아니기 때문에 항상 추상적으로 정리되어있는 두루뭉술한 설명이 난무한다. 

그래서 객체지향의 원칙을 지킨 코드가 뭔데? 라고 묻는다면 뭘 제시하기가 어려운데

어쨋든 어떤 유명한 회사의 어떤 개발자 아져씨가 자신이 객체지향을 지키는 코드를 짜기위해 자신의 책상에 붙여놓은 객체지향을 위한 코드 규칙인데 절대적인것은 아니지만 좋은 방법중 하나이니 따라가 보는것이다. 

 

fun match(
    userLotto: List<Int>,
    winningLotto: List<Int>,
    bonusNumber: Int
): Int {
    val matchCount = match(userLotto, winningLotto)
    if (matchCount == 6) {
        return 1
    }
    val matchBonus = userLotto.contains(bonusNumber)
    if (matchCount == 5 && matchBonus) {
        return 2
    }
    if (matchCount > 2) {
        return 6 - matchCount + 2
    }
    return 0
}

이런식으로 일단 최상위 함수로 갈겨서 만든다.

 

일단 만들어진 최상위 함수를 어떤방식으로 리펙토링 할까?

 

1. 메서드 분리(일단 함수가 하는일이 너무많다.)

-> 메서드를 분리한다.

함수가 한가지 일만 하도록 구현한다.

10라인 넘지말라는 말이 함수가 10줄을 넘으면 여러가지 일을 할가능성이 크니까 함수 하나가 한가지 일만 하도록 만드려는 가이드 라인이다.

 

 

2.클래스 분리

객체지향은 블루칼라의 언어라고 한다(애초에 천재는 논리자체에 오류를 안 만들어서 원시값 포장,일급컬랙션 이런것들 아예 사용조차 할필요가 없게 만드는게 천재라고 ㅋㅋㅋㅋㅋ)

우리같은 범인 그리고 그중에서도 능력치가 떨어지는 나는 객체지향에 의존할수 밖에없다.(의존성 분리하고싶다.) 

 

이제 클래스를 분리해야하는 기준 혹은 클래스 분리에서 지켜야할 사항들을  쭉 살펴볼것이다.

 

-모든 원시값과 문자열을 포장한다.(기준)

fun match(
    userLotto: List<LottoNumber>,
    winningLotto: List<LottoNumber>,
    bonusNumber: LottoNumber
): Int {
    // ...
}

로또번호를 포장하지않고 그냥 원시값인 Int로 사용한다면 위험하다.

뭐가 위험하다는 걸까?

로또 번호에는 많은 게임내의 규칙이 적용되고있다 ex)1~45 사이의 숫자여야 한다.

이런것들을 매번 검사하려면 코드가 중복될것이고 그것을 행하지 않는다면 로또의 규칙에 맞지 않는 숫자가 돌아다닐 가능성이 생기는 것이다.

하지만 로또 번호를 다 포장한다면 -> 포장 과정에서 코드 중복없이 로또의 규칙에 맞는 검증이 일어나기 때문에 검증된 값을 항상 사용할수 있는 이점을 얻는것이다.

 

여기서 또 용어가 하나 등장한다. VO(값객체) 

값객체 이야기 나오면 또 시리즈로 쭈르룩 비슷한거 또 다나온다 Entity,DTO

하지만 우테코는 선닦길만 쫓아가면 되기 때문에 선배들이 다 써놓은 테코블에 내가 궁금한것들이 적혀있다.아래에 두개의 테코블 글을 첨부할것이니 참고하자!!!

 

DTO, VO, Entity 가 뭔지 대략적으로 설명하는글

https://tecoble.techcourse.co.kr/post/2021-05-16-dto-vs-vo-vs-entity/

 

DTO vs VO vs Entity

DTO와 VO는 분명히 다른 개념이다. 그런데, 같은 개념으로 생각해서 사용하는 경우가 많다. 왜일까? ⌜Core J2EE Patterns: Best Practices and Design Strategies⌟ 책의 초판에서는 데이터 전송용 객체를 로 정의

tecoble.techcourse.co.kr

 

VO만 집중공격

https://tecoble.techcourse.co.kr/post/2020-06-11-value-object/

 

VO(Value Ojbect)란 무엇일까?

프로그래밍을 하다 보면 VO라는 이야기를 종종 듣게 된다. VO와 함께 언급되는 개념으로는 Entity, DTO등이 있다. 그리고 더 나아가서는 도메인 주도 설계까지도 함께 언급된다. 이 글에서는 우선 다

tecoble.techcourse.co.kr

 

우리가 로또번호 같은걸 포장하고나면 인스턴스 끼리 비교할때 값을 가지고 비교한다.

즉 인스턴스끼리 동일성을 가지고 비교하는것이 아니라 값들을 통해 동등성을 비교하게 된다.

ex) 로또번호45 와 로또번호45는 같은것아닌가 이게 다르다고 느껴지면 주소값이 눈에 보이는것이니 병원부터 가보자

 

이렇게 값을가지고 비교하는 객체들을 VO(값객체라고 부른다) 

VO의 특징을 정리한 이미지를 봐보자

이말을 좀더 자세히 생각해보자면

값을 포장해놓고 보니 클래스로 감싸져있고 이 클래스의 본질은 값을 나타낼 뿐이다.

값을 나타내는것이니 값이 바뀐다면 이미 다른것이 되어버린다-> 가변객체라면 VO가 아니다 값은 딱한번 생성자를 통해 설정된다.

값들이 기준에 맞는지 검사하는 로직을 들고 있을 수 있다.

 

아 참고로 값 객체인지 아닌지 구분하는 기준중 하나가 equals와 hashcode를 비교하는것이다

-> 값을 비교하는것이 본질인 객체이니 어떻게 보면 당연한 이야기이다.

 

이런 특징들을 종합해보면 원시값을 포장해놓는것과 VO는 대충 비스무리한 이야기하는것을 파악할수 있다.

 

-> 참고로 아래나올 객체를 추적(캐싱) 하는방법을 통해 성능을 높이는 방법을 값객체에서 쓸수 있을것이다.

값객체는 값으로 비교하기 때문에 여러개가 있을 필요가 없을 테니까!!

equals 편하게 오버라이딩 하려면 cmd+n 누르면 equals 와 hashcode 오버라이딩을 자동으로 해주는 기능이있는데 인텔리제이는 정말 친절한것같다.

 

이러한 이점들이 있기 떄문에 원시값은 포장되어야한다.(비지니스로직 여기저기 퍼져있지않고 제자리에 잘 뭉쳐있고 나눠져있게됨)

 

 


이제 수업중간에 동등성, 동일성 비교에대한 이야기가 나왔는데 그에 대한 공부는 로또미션중 공부하다 진하게 다뤘으니 생략해도 될것같다.

https://mccoy-devloper.tistory.com/99

 

[우테코]로또 미션 코드리뷰 관련 학습내용

멧돼지 피드백 관련 학습 질문과 답변 1. 원시값 포장의 이점과 기준? 원시값들을 모두 포장하려고하니 로또구입금액, 로또구입개수 같은것들은 거의 같은 역할을 하는것들이라 검증 하는 내용

mccoy-devloper.tistory.com

 

자바에서 string은 리터럴로 만들면 캐싱되고 당연히 코틀린도 그 위에서 돌아가니 캐싱될것이고(궁금하다면 위글을 참고하자)

자바의 Integer는 -128 부터 127 까지는 캐싱이 되어있어 이 범위안에서는 String 값처럼 동등성 동일성에서 열받는 상황을 연출한다.

 

그리고 이런부분에서 JUnit로 학습테스트를해보면 진짜 이상한 기작들이 많이 일어난다.

참고: JUnit에서 동일성비교는 isSameAs 동등성 비교는 isEqualTo 이다.

 

그래서 JUnit과 코틀린과 자바의 짬뽕으로인해 예상할수없는 이상한 상황들이 벌어지는 것에 대해 퀴즈쇼가 펼쳐졌다.

 

이 부분에 대해서 다 정리하자니 너무 길어서 코드와 키워드 형태로 기록을 남기려고 한다.

 

package study

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class IntTest {
    @Test
    fun test1() {
        val actual: Int = 1
        val expected: Int = 1
        assertThat(actual).isEqualTo(expected)
        assertThat(actual).isSameAs(expected)
    }

    @Test
    fun test2() {
        val actual: Int = 1000
        val expected: Int = 1000
        assertThat(actual).isEqualTo(expected)
        assertThat(actual).isNotSameAs(expected)
    }

    @Test
    fun test3() {
        val actual: Int = 1000
        val expected: Int = 1000
        assertThat(actual == expected).isTrue
        assertThat(actual === expected).isTrue
    }

    @Test
    fun test4() {
        val actual: Int? = 1
        val expected: Int? = 1
        assertThat(actual == expected).isTrue
        assertThat(actual === expected).isTrue
    }

    @Test
    fun test5() {
        val actual: Int? = 1000
        val expected: Int? = 1000
        assertThat(actual == expected).isTrue
        assertThat(actual === expected).isFalse
    }
}

JUnit의 동일성 비교함수 isSameAs는 Object 타입을 받는다.

-> Object는 자바것이고 그것을 받는 형태로 제작되어있으니 int를 넘기면 자바의 Integer로 래핑해서 전달된다(궁금하다면 디컴파일 해보면됨)

근데 자바 Integer는 -128~127은 캐싱되니 같은 인스턴스로 비교하고있는 상황이고

동등성비교든 동일성 비교든 이범위에서는 다 True로 나오는것이다.

 

뭐 막간 상식 Integer의 생성자는 사실 자바9부터 Deprecated 됐다고한다 하지만 자바는 하위호환을 잘 지원하기 때문에 완전히 막지는 않았다고 한다. (사실 자바 잘몰라서 뭔소리인지 참 ... 시간없으니 일단 넘어간다.)

 

3번 테스트는 왜 동일성이 같냐? Int가 자바의 Integer로 바뀔일도 없고 그냥 원시값끼리 비교한거니 주소값도 같을것이다.Int가 원시값임을 기억하자

 

이제 코틀린은 널러블까지 생각해야하는데

코틀린은 기본적으로 자바위에서 돌아가고 자바는 널러블이라는것이 없다 그래서 널러블을 사용한다면 자바코드로 바뀔때는 Integer로 바뀌어서 사용된다. -> 널러블없는데 표현해야하니까

 

자 이런이유로 또 캐싱 떄문에 이상한 현상이 벌어지는것이다.


클래스

클래스는 맨날 붕어빵틀 이야기만 나오는데 다양한 방법으로 이용할 수 있다.

 

클래스는 객체의 팩토리(factory),객체를 생성,추적(캐싱),적절한 시점에 파괴 하는 기능을 구현할 수 있다.

클래스는 객체를 생성하고 객체를 생성하는것을 클래스가 객체를 '인스턴스화 한다'고 표현한다.

 

위의 클래스의 기능들을 쭉 풀어나가보자

 

1. 객체를 만든다?

생성자를 호출함으로서 객체를 생성한다.

팩토리 메서드 같은것을 통해서 객체를 생성할수도 있겠지만 결국 근본적으로 생성자를 호출해서 객체를 생성하게 되어있다.

 

2.객체를 추적한다?

객체를 추적한다는 개념이 뭔가? 생성해놓고 그 자리에서만 쓰고 버려버리는 것이 아니라 데리고 다니면서 알뜰하게 재활용하는 것이다. 물론 파괴할때도 데리고 다니다가 파괴할수도 있을것이다. 이렇게 객체를 만들어놓고 데리고 다니는 방법을 살펴보자.          

 

나는 말을 잘못하기 때문에 유치원생 수준으로 말을하지만 예쁘게 써져있는 우테코의 자료에 의하면 이렇게 정리되어있다.

종종 클래스를 객체의 템플릿으로 보지만 '객체의 능동적인 관리자'로 생각해야 한다. 클래스는 객체를 보관하고 필요할 때 객체를 꺼낼 수 있고 더 이상 필요하지 않을 때에는 객체를 반환할 수 있는 저장소(storage unit) 또는 웨어하우스(warehouse)로 바라봐야 한다.

이게 곧 추적이다(BUFFALO TRACE가 아니라 객체를 추적하자 ㅋㅋㅋㅋ)

 

JVM 기반 언어에서는 생성자를 호출하면 무조건 새로운 인스턴스를 만들어낸다.

-> 그렇다면 다른 방법으로 인스턴스를 만들방법이 없나? 

팩토리 메서드를 사용하면된다.

https://blog.kotlin-academy.com/item-30-consider-factory-functions-instead-of-constructors-e1c747fc475

 

Item 30: Consider factory functions instead of constructors

Yes, primary constructor, but how about other ways to create a class? Part of Effective Kotlin by Marcin Moskała.

blog.kotlin-academy.com

이글을 보면 팩토리 메서드가 세상 자세히 나와있다.

 

아 참고로 코틀린에서는 static이라는 개념이 없어서 정적 팩토리 메서드라고 하면 틀린것이다 (정적 팩토리 메서드는 자바거다)

그래서 코틀린에서는 그냥 팩토리 메서드라하고 컴패니언 오브젝트에다가 정의하는경우가 많다.(다른방법들도 있다)

 

어쨋든 팩토리 메서드는 단순 인스턴스를 생성 하는것이 아닌 어떤 조건을 붙일수있다. 그래서 호출되었을때 인스턴스가 기존에 생성되어 현재 가지고있는 값인지 확인하고 없다면 생성후 반환, 기존에 만들어져있는 인스턴스라면 그것을 그냥 반환하는 형태로 캐싱을 할수가 있다. 이런식으로 클래스에서 인스턴스를 만들어서 바로 독립시키는게 아니라 계속 데리고 다니면서 사용하는 형태를 띄는것이다 (이게 곧 추적)

class LottoNumber private constructor(private val value: Int) {
    companion object {
        private const val MINIMUM_NUMBER = 1
        private const val MAXIMUM_NUMBER = 45
        private val NUMBERS: Map<Int, LottoNumber> = (MINIMUM_NUMBER..MAXIMUM_NUMBER).associateWith(::LottoNumber)

        fun from(value: Int): LottoNumber {
            return NUMBERS[value] ?: throw IllegalArgumentException()
        }
    }
}

이 코드를 봐보자 현재 진행중인 로또 미션에서 로또를 어떻게 만들지 팩토리 메서드를 달아놓은것이다.

위에서 설명했듯이 캐싱을 하는형태로 제작되어있다.

LottoNumber 같은경우에는 인스턴스가 45 개 밖에 없으니(로또번호는 1~45 까지의 수니까) 이걸 미리 만들어 놓고 나중에 필요지점에서 만들어놓은걸 꺼내서 주는 방법인것이다.

이렇게 캐싱하는것을 FlyWeight Pattern 이라고 부른다.

 

이런 행위들을 클래스에서 해야한다. 이러면 메모리를 관리할수 있는 이점이있다.

 

근데 이렇게해도 LottoNumber의 인스턴스가 추가로 생성될수 있다. -> 값객체인데 여러개의 인스턴스 있어봐야 의미도 없고

필요없는 객체를 만들면 메모리만 낭비다

 

근데 사용자가 그냥 생성자를 호출하면 인스턴스를 생성할수있으니

private constructor 를 붙여줘서 외부에서 로또넘버를 못만들게 막고 팩토리 메서드로 강제로 유도하게 만들면 가장 이상적일것이다.

이런 캐싱 방식을 사용한다면 상황에 맞춰서 생성자를 막아 쓸데없이 인스턴스가 만들어지는것을 막는것이 좋다.

 

근데 이런상황에서 인스턴스를 공유하게된다(LottoNumber의 45는 다 같은 인스턴스를 쓰고있을것이다.)

이런경우 인스턴스의 값이 오염되면 공유하는 사용처에서 모두다 값이 오염되게 된다.

그러므로 이런 값들은 불변값으로 만들어야한다.(방어적 복사 같은개념)

-> 불변값은 밑에서 더 살펴볼것이니 공부해보자.

 

3.객체를 파괴한다?

객체를 적절한 시간에 파괴해야 할것이다 .

C++ 같은 언어에서는 직접 객체를 파괴해줘야한다.

하지만 JVM 기반에서는 GC(가비지컬랙터)라는 킹갓 청소부가 있기 떄문에 알아서 적당한 시점에 파괴한다.

근데 객체는 왜 파괴하는가? -> 인스턴스가 생성된것은 메모리를 먹고있는것이다 (당신이 기껏 용량을 늘려놓은 램에 올라가서 길막을 하는것이다)

근데 쓰임새가 없으면 자바는 GC가 알아서 파괴해주니 신경안쓰지만 또 신경쓰이는게있다.

 

객체를 만드는 행위 자체가 메모리를 잡아먹는것이다!!! 이 관점에서 보자면

메모리를 많이 잡아먹는 객체를 만드는 원시값 포장 이런것들을 해야하나? 메모리 겁나 먹겠네 라는 생각이 든다.

이런것들을 아까 본 FlyWeight pattern 같은것들으 톨해서 메모리를 덜잡아먹도록 개성하고 할수있겠지만 

근본적으로는 메모리를 잡아먹고 있는 것이긴 하다.

 

근데 아까도 나왔듯이 객체지향은 블루칼라의 언어 천재들은 원시값 그냥 안포장하고 메모리를 아껴도 로직으로 애초에 충돌 안나게 한다 근데 나같은 빡대가리들은 그게 안되니까 객체를 막만들어서 나의 멍청함을 막아내는것이다.

 

근데 이렇게 메모리에 민감한 업종이있는데 게임회사들이 이런게 많고 메모리가 부족했던 예전 안드로이드 기기를 다루는사람들이 이런것들에 민감하다고 한다.


가변 객체와 불변 객체

큰거 하나 왔다!!!!🔥  가변성 불변성 이거는 관리차원에서 정말 중요한 이야기인거같다. 이펙티브 코틀린에서도 징하게 하는 이야기 같고

cs를 공부해도 다양한 부분에서 이개념이 중요하다, 어쨋든 뭘 공부해도 계속 튀어나온다.(인싸인거같다.)

그리고 이번설명에서 불변객체가 얼마나 좋은지 확 와닿게 킹갓제너럴충무공제이슨이 설명해줬다.

 

예시를 봐보자

data class Cash(var dollars: Int) {
    fun mul(factor: Int) {
        dollars *= factor
    }
}

 이런 가변객체가 있다 mul이 setter니까 가변객체 ^^ 값이 바뀌니까

 

val five = Cash(5);
five.mul(10);
println(five.dollars);

그리고 위에 Cash라는 가변객체를 이용하는 상황을 봐보자

최초 Cash에 5를 담으니 변수명으로 five가 적절할것이다.

근데 중간에 값이 50으로 변경된다.(10을 곱함)

근데 지금은 코드가 몇줄이 없어서 five가 50으로 바뀐지도 인식이되고 five에 50이 있겠거니 한다.

 

근데 five를 최초에 5를 담아 초기화한 줄과

값을 10으로 바꾸는 줄사이에 10000줄의 코드가 있고

또 바꾸고난후 print하는 코드사이에 10000줄이 있다면

과연 five를 print했는데 50이 나올걸 예상하는 사람이있을까?(예상하는게 더 이상하다.)

 

근데 그렇다고 변수명을 cash 같은거로 할것인가?

그럼 다름 Cash 만들때 이름을 어떻게 해야하는가 cash2로 해야하는가 ?

이런 명확하지 않은 변수명은 당연히 안좋다.

 

이런부분을 타개하기위해 불변객체를 사용한다.

불변객체의 예시를 보자

data class Cash(val dollars: Int) {
    fun mul(factor: Int): Cash = Cash(dollars * factor)
}

이런식으로 상태를 변경할수 없도록 불변클래스로 만든다면 유지보수성이 크게 향상된다.

 

val five = Cash(5)
val fifty = five.mul(10)
println(fifty.dollars)

그럼 이렇게 해도 새로운 값을 만들어내도 새로운 인스턴스에 담겨서 새로운 변수에 담아야지 기존것을 수정할수없다.

 

이렇게 불변객체를 이용하면 새로운 변수에는 무조건 새로운 인스턴스를 만들어서 담아내야할것이다.

 

이렇게되면 따라오는 장점들은 식별자 변경(identity mutability)문제 해결, 실패 원자성(failure atomicity),

시간적 결합(temporal coupling) 문제 해결, 스레드 안정성, 단순성, 등등이있다 이제 이것들을 하나하나 살펴보자

 

1. 식별자 변경 문제

import org.junit.jupiter.api.Test

data class Cash(var dollars: Int) {
    fun multi(factor: Int) {
        dollars *= factor
    }
}

class Test {
    val five = Cash(5)
    val ten = Cash(10)
    val map = mapOf(five to "five", ten to "ten")

    @Test
    fun `내맘대로 테스트`() {
        println(map[five])
        // five 나옴

        five.multi(2)

        println(map[five])
        // ten 나옴
    }
}

현재 가변객체로 dollar 값이 5와 10인 인스턴스 두개를 key값으로 놓아서 map 을 만들었다.

그후 five에 2를 곱하여 ten 변수에 들어있는 Cash인스턴스와 dollars 값을 같게 만들면 map 에서 five값을 조회해도 ten 이 나오는 이상한 현상이 벌어진다.  이런현상을 불변객체를 사용하면 막을수 있을것이다.(값이 안바뀌니까 당연한소리다.) 

 

2.실패의 원자성 

실패의 원자성이란 어떤 작업을 했을때 실패한다면 아예 아무것도 일어나지않는것이고 성공한다면 완전히 성공하는것이다.

즉 성공과 실패가 0과 1상태만 있다. -> 중간에 실패해서 중간지점에서 애매하게 변경되는 고착상태가 없는것이다.

data class Cash(var dollars: Int, var cents: Int) {
    fun mul(factor: Int) {
        dollars *= factor

        if (/* 뭔가 잘못 됐다면 */) {
            throw RuntimeException("oops...");
        }

        cents *= factor
    }
}

또 Cash 라는 가변성 객체를 보면서 알아보자(왜 죄다 예시가 돈이지? 돈안좋아하는 사람은 없다만...)

cash 내부에는 mul이라는 함수가있는데 보다시피 dollar를 조작하고난후 조건에따라 검사한후 문제가있다면 Exception을 던지고

아니라면 cents를 조작한다 근데 만약 조건검사에서 문제가 있어서 중간에 오류를 던졌다 가정해보자

그렇다면 가변객체이므로 dollars의 값은 변경된상태로 catch로 넘어가서 처리가 될것이다. -> 이는 원하는 작업을 다 수행하지 못한채 애매하게 값만 바뀌고 끝나는 것이다. (원래 의도의 일부만 수행한채 끝날것이다)

이에 대한 대응을 하기위해선 catch 문에서 dollar값을 롤백시켜주는 작업을 해야하는데 매우 귀찮고 어쨋든 비효율적이다

 

이 또한 불변객체였다면 상태값은 변하지 않은상태로 그값을 조작해서 새로운 인스턴스로 담아내기 때문에 설령 중간에 오류가나도 상태값은 하나도 변하지않아 새로 함수를 다시 호출하면 의도한 원래값이 나올것이다. 

 

 

시간적 결합(temporal coupling) 문제

val price = Cash()
price.dollars = 29
price.cents = 95
println(price) // "$29.95"
val price = Cash()
price.dollars = 29
println(price) // "$29.00"!
price.cents = 95

가변객체라면 언제 조회하냐에 따라서 다른 값이 나올수 있다 불변객체는 초기화와 동시에 값은 정해지기 때문에 언제나 같은 값을 배출한다. 

 

스레드 안정성

  • 객체가 여러 스레드에서 동시에(concurrently) 사용될 수 있고 예측 가능한(predictable) 결과를 보장하는 객체의 품질

이렇다는데 동시성 프로그래밍을 하면 가변성 상태값은 정말 다루기 어려운문제다 그래서 맨날 세마포어 뮤텍스 어쩌구 저쩌구 cs에서 그난리를 치는건데 애초에 불변이라면 값이 변하지 않으니 여러 쓰레드에서 접근해도 문제가 없을것이다.

이것고 같은말로 여러 레이어에서 접근해도 괜찮다는 뜻이다 즉 불변객체라면 바로 View 레이어로 보내서 직접 출력해도 상관없다는 뜻이다. -> 뭔짓을 애초에 못할뿐더러 뭔짓을해도 값이 오염되지않는다.(도메인 영역을 벗어나도 데이터가 바뀌지 않는다.)

(dto와 vo 가 헷갈려하는데 그냥 dto는 불변도 아니고 데이터를 운반하는 객체일 뿐이다.)

 

단순성

불변값들을 모아서 사용하고 객체가 불변값을 많이 가질수록 객체가 더 단순해지고 응집도는 높아지고 유지보수는 쉬워진다.

결론적으로 불변객체를 많이쓸수록 개이득인것이다.

 

하지만 불변값만 사용하기는 너무 어렵고 사실상 불가능하니 불변값을 애용하자 정도로 마무리하면 될것같다.