[우테코] 로또 피드백

Yerin·2023년 2월 21일

우아한테크코스

목록 보기
4/10
post-thumbnail

두번째 미션 로또 피드백에 대한 강의 정리입니다.
데일리 미팅때 웹으로 자기소개 만든거 소개하고 바아로 수업.. (당담 코치가 제이슨이어서 계속 연장선인 너낌..)
오늘 졸업식 갈거라 수업 끝나면 바아로 가방 싸서 나갈 예정 ㅎㅎ

TDD

시작하기

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

기능 목록

  • happy case -> 도출하기 쉬움
  • 예외 case를 발굴하는데 집중 -> 처음에 생각하지 않으면 번거로움이 생긴다

TDD로 구현할 기능 찾기

  • 구현 중간 부분을 자르는 연습을 해야 한다.
  • 구현 중간 부분을 자른다는 것은 구현에 필요한 메서드를 찾는 과정이다.

로또 미션

시작하기

  • 객체 설계를 어떻게 해야할지 모르겠다면 시작은 최상위 함수 구현으로 시작한 후 지속적인 리팩터링
  • 리팩토링할 때는 객체 지향 생활체조 원칙을 참고해 리팩터링

❔ 모든 것을 객체로 포장해야할까? -> 이것 저것 시도해 보며 나만의 기준을 만들자

fun match(
    userLotto: List<Int>,
    winningLotto: List<Int>,
    bonusNumber: Int
): Int {
    val matchCount = userLotton.count { winningLotto.contain(it) }
    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
}

리팩터링, 어디서 어떻게 시작할 것인가?


메서드 분리

  • 한 메서드에 오직 한 단계의 들여쓰기만 한다.
  • 함수가 한 가지 일만 잘 하도록 구현한다.
  • 정량적인 기준을 만들어 연습한다. 함수의 길이가 15라인을 넘어가지 않도록 구현한다.

클래스 분리

  • 모든 원시 값과 문자열을 포장한다.
  • 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  • 일급 컬렉션을 쓴다.
  • 함수의 인자 수를 최소화한다.
  • 함수에서 이상적인 인자 개수는 0개(무항)이다. 다음은 1개이고, 다음은 2개이다.
    3개는 가능한 피하는 편이 좋다. 4개 이상은 특별한 이유가 있어도 사용하면 안 된다.
  • private 함수를 테스트하고 싶다.
fun match (
	userLotto: List<Int>
    winningLotto: List<Int>
    bonusNumber: Int
): Int {
	// 로또 번호는 1부터 45
    // 로또의 번호는 중복되지 않은 6개
    // 당첨 번호와 보너스 번호가 중복되지 않아야 한다
    val matchCount = match(userLotto, winningLotto)
    val matchBonus = userLotto.contains(bonusNumber)
    return rank(matchCount, matchBonus)
}

// rank와 

이제 클래스 분리하는 법에 대해서 천천히 하나씩 뜯어보자


모든 원시 값과 문자열을 포장한다.

동등성 VS 동일성

일단 설명하기에 앞서 동등성과 동일성이 무엇인지 알아보자.
단어만 두고 보자면 헷갈릴 수 있지만 쉽게 구별 가능하다.

여기에서 재미있는 비유가 나온다.
똑같은 상세 스펙을 가진 두 핸드폰은 동일한가? 아니다. 동등하고 동일하지 않다.
Mac 주소를 확인해보면 두 핸드폰이 다름을 알 수 있다. -> 동일 X
하지만 두 핸드폰이 같은 성능, 같은 가격에 팔림은 같다. -> 동등 O

  • 동일성 (Identity) : == 비교를 통해 객체 내 메모리 값이 같은지 확인한다. 하나 당 하나의 값을 갖는 것이다.
  • 동등성 (Equality) : 논리적으로 같은 지위를 가졌는지 확인하는 것이다.
class IntTest {
    @Test
    fun `동일성 비교`() {
        val a = 1
        val b = 1
        assertThat(a).isEqualTo(b)
        assertThat(a).isSameAs(b)
    }

    @Test
    fun `동일성 비교2`() {
        val a = 1000
        val b = 1000
        assertThat(a).isEqualTo(b)
        assertThat(a).isSameAs(b) // 미통과
    }


    @Test
    fun `동일성 비교3`() {
        val a = 1000
        val b = 1000
        assertThat(a == b).isTrue
        assertThat(a === b).isTrue
    }

    @Test
    fun `동일성 비교4`() {
        val a: Int? = 1
        val b: Int? = 1
        assertThat(a).isEqualTo(b)
        assertThat(a).isSameAs(b)
    }

    @Test
    fun `동일성 비교5`() {
        val a: Int? = 1000
        val b: Int? = 1000
        assertThat(a).isEqualTo(b)
        assertThat(a).isSameAs(b)	// 미통과
    }

    @Test
    fun `동일성 비교6`() {
        val a: Int? = 1000
        val b: Int? = 1000
        assertThat(a == b).isTrue
        assertThat(a === b).isTrue	// 미통과
    }
} 

일단 자바에서의 Int와 Integer에 대해 알아보자.
자바에서는 Int와 Integer 타입이 존재한다.
Int는 primitive 타입(데이터를 가지는 자료형)이고, Integer는 wrapper 클래스(객체)이다.

❔ 2와 3의 차이는 무엇인가?
3은 wrapping 할 필요가 없다. -> int만 비교. 알림이 뜨면 알겠지만, a==b 수준에서 이미 true로 넘어간다.
2에서 isSameAs는 object 타입을 받기 때문에 Integer 타입으로 변경된다.

❔ 6번은 왜 미통과 되는가?
이것도 실행해보면 알겠지만, a==b에는 항상 True라는 알림이 뜨지만, a===b에는 알림이 뜨지 않는다.
코틀린은 nullable 타입이 따로 존재하지만, 자바는 무조건 nullable 타입니다.
그래서 initgier 로 변경시켜서 검사를 하게 되는 것이다.

❔ 1번과 2번의 차이는 무엇인가?
우리는 로또 번호를 생성할 때 2가지 방법을 사용할 수 있다.
1부터 45까지의 값을 미리 캐싱해두거나, 사용하는 숫자를 그때마다 객체 생성을 한다.
응용해보자면 JVM int에서 -128부터 127까지 미리 숫자를 캐싱해둔 것이다.😁

아래를 함께 보면 이해가 더 갈 수 있을 것이다.
다음은 IntTest 파일을 디컴파일한 결과이다. (이렇게 구조가 궁금할 때는 decomplie을 쓰는게 아주 유용하다)

public final class IntTest {
    @Test
    public final void 동일성_비교/* $FF was: 동일성 비교*/() {
       int a = 1;
       int b = 1;
       Assertions.assertThat(a).isEqualTo(b);
       // isSameAs가 Object를 받기 때문에 Integer로 변환된 결과
       Assertions.assertThat(a).isSameAs(Integer.valueOf(b));
    }

    @Test
    public final void 동일성_비교2/* $FF was: 동일성 비교2*/() {
       int a = 1000;
       int b = 1000;
       Assertions.assertThat(a).isEqualTo(b);
       Assertions.assertThat(a).isNotSameAs(Integer.valueOf(b));
    }

   	@Test
	public final void 동일성_비교3/* $FF was: 동일성 비교3*/() {
   		// 이미 넘어가기 전에 판단이 끝나기 때문에 true로 전달되는 것
    	int a = true;
    	int b = true;
      	AbstractBooleanAssert var10000 = Assertions.assertThat(true);
      	Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(a == b)");
      	var10000.isTrue();
      	var10000 = Assertions.assertThat(true);
      	Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(a === b)");
      	var10000.isTrue();
    }

	// nullable은 Integer로의 형변환이 발생하지 않는다.
    @Test
    public final void 동일성_비교4/* $FF was: 동일성 비교4*/() {
       Integer a = 1;
       Integer b = 1;
       Assertions.assertThat(a).isEqualTo(b);
       Assertions.assertThat(a).isSameAs(b);
    }

    @Test
    public final void 동일성_비교5/* $FF was: 동일성 비교5*/() {
       Integer a = 1000;
       Integer b = 1000;
       Assertions.assertThat(a).isEqualTo(b);
       Assertions.assertThat(a).isNotSameAs(b);
    }

    @Test
    public final void 동일성_비교6/* $FF was: 동일성 비교6*/() {
       Integer a = 1000;
       Integer b = 1000;
       AbstractBooleanAssert var10000 =  Assertions.assertThat(Intrinsics.areEqual(a, b));
       Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(a == b)");
       var10000.isTrue();
       var10000 = Assertions.assertThat(a == b);
       Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(a === b)");
       var10000.isFalse();
    }
	
    // 6을 7과 8로 분리해서 디컴파일 결과는 다음과 같다.
    @Test
    public final void 동일성_비교7/* $FF was: 동일성 비교7*/() {
       int a = 1000;
       int b = 1000;
       AbstractBooleanAssert var10000 = Assertions.assertThat(a == b);
       Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(a == b)");
       var10000.isTrue();
    }

    @Test
    public final void 동일성_비교8/* $FF was: 동일성 비교8*/() {
       Integer a = 1000;
       Integer b = 1000;
       AbstractBooleanAssert var10000 = Assertions.assertThat(a == b);
       Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(a === b)");
       var10000.isTrue();
    }
}

아래 코드는 JVM에서 Int에 대한 메서드들이 생긴 정의된 형태이다.

public SELF isEqualTo(int expected) {
    integers.assertEqual(info, actual, expected);
    return myself;
  }

@Override
  public SELF isSameAs(Object expected) {
    objects.assertSame(info, actual, expected);
    return myself;
  }

❗ 그렇다면 포장된 프로퍼티 값을 비교하는 방법은 ? map으로 일일이 푸는 것이 아니라
1. equals and hashcode 를 override : 왜 equals 사용 시 hashcode를 함께 override 하나요?
2. data class 사용


제이슨이 수업을 하다가 와다다다다다다다 진도를 나가서 클래스 분리하는 방법은 다음 시간에 배우게 되는데..! 일단 다음 내용부터 확인해보자.


클래스

클래스 = 객체의 factory

  • 객체를 만들고, 추적하고, 적절한 시점에 파괴한다.
  • 객체를 생성하며 일반적으로 클래스가 객체를 '인스턴스화한다(instantiate)'라고 표현한다.
  • 클래스는 생성자를 사용하여 객체를 생성할 수 있다. (생성자는 무조건 인스턴스를 생성한다.)

팩토리 메서드

인스턴스도 새로 안만들고 생성자의 역할을 하는 것은 무엇일까?
바로 (static) factory method이다.
코틀린에서는 static이 없기 때문에 -> companion object / 최상위 함수 를 이용한다.

찾다보니 테코블에 어떤 분이 작성하신 을 찾았다. ㅋ_ㅋ

팩토리 메서드란?
객체 생성의 역할을 하는 클래스 메서드
팩토리 메서드의 가장 자주쓰이는 예시는 enum class와 valueOf이다.
이번 로또 미션의 코드로 알아보자.

enum class Rank(val countOfMatch: Int, val winningMoney: Int) {
    FIRST(6, 2_000_000_000),
    SECOND(5, 30_000_000),
    THIRD(5, 1_500_000),
    FOURTH(4, 50_000),
    FIFTH(3, 5_000),
    MISS(0, 0);

    companion object {
        fun valueOf(countOfMatch: Int, matchBonus: Boolean): Rank? {
            if (matchBonus and (countOfMatch == 5)) {
                return SECOND
            }
            if (countOfMatch in 1..2) {
                return MISS
            }
            return values().findLast { rank -> rank.countOfMatch == countOfMatch }
        }
    }
}

미리 생성된 객체를 “조회”를 하는 메서드이기 때문에 팩토리의 역할을 한다고 볼 수는 없지만, 외부에서 원하는 객체를 반환해주고 있으므로 결과적으로는 정적 팩토리 메서드라고 간주해도 좋다.

그렇다면.. 드는 의문

객체 생성을 왜 생성자가 안하고 굳이 팩토리 메서드를 사용하나요?

  • 이름을 가질 수 있음 : 메서드 이름에 객체의 생성 목적을 담아 낼 수 있다.
  • 호출할 때마다 새로운 객체를 생성할 필요가 없음 : 갯수가 대충 정해져 있다면 캐싱 가능
  • 객체 생성을 캡슐화 할 수 있다. (데이터 은닉)

클래스의 역할

마지막으로.. 클래스는 객체를 보관하고 필요할 때 객체를 꺼낼 수 있고 더 이상 필요하지 않을 때에는 객체를 반환할 수 있는 저장소(storage unit) 또는 웨어하우스(warehouse)로 바라봐야 한다.

다음 예제에서는 로또 번호를 미리 45개를 캐싱 해놓았다.
하지만 객체가 꼭 45개만 생성될까?
그렇지 않다. 클라이언트가 모르고 또 생성할 수 있기 때문이다.
왜냐고? 클라이언트 입장에서 from이 정의되어있는지 까보기 전에는 모르기 때문이다.
-> 헤결법 : private constructor를 붙여주면 외부에서 로또 넘버 생성자 호출 불가하다.
그러면 외안되? 하고 까보면 from을 사용하도록 유도할 수 있다. 😊

data class LottoNumber (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)

		// 보통 factory method들은 명명 규칙이 정해져있다.
        fun from(value: Int): LottoNumber {
            return NUMBERS[value] ?: throw IllegalArgumentException("로또 넘버는 1부터 45까지 입니다.")
        }
    }
}

// 다음과 같이 private constructor 사용
data 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("로또 넘버는 1부터 45까지 입니다.")
        }
    }
}


// 1~45까지 이미 정해져 있기 떄문에 힙메모리 걱정없는 어쩌구 저쩌구를 생성할 수 있다.  
@Test
fun `동일성 비교` () {
	assertThat(LottoNumber.from(1)).isEqualTo.from(1))
    assertThat(LottoNumber.from(1)).isSameAs.from(1))
}

가변 객체와 불변 객체

가변 객체

아무튼 아무튼 이름 정할때도 그렇고 수정도 굉장히 번거롭다

불변 객체

  • 생성자를 통해서만 초기화가 가능하다.
  • 모든 클래스를 상태를 변경할 수 없는 불변 클래스(immutable class)로 만들면 유지 보수성이 크게 향상된다.
  • 객체가 완전하고 견고한 상태이거나 아니면 아예 실패하는 실패 원자성(failure atomicity)을 가진다.
  • 스레드 안정성 : 객체가 여러 스레드에서 동시에(concurrently) 사용될 수 있고 예측 가능한(predictable) 결과를 보장하는 객체의 품질
  • 단순성(simplicity), 객체가 더 단순해질 수록 응집도는 더 높아지고, 유지보수는 더 쉬워진다.
  • 불변 객체의 크기가 작은 이유는 불변 객체의 경우 생성자 안에서만 상태를 초기화할 수 있기 때문이다.

방어적 복사

얕은 복사

주소값 자체를 복사 -> 복사된 객체의 인스턴스는 원본 객체의 인스턴스와 같은 메모리 주소를 사용한다. 복사된 객체의 값이 변경되면 원본 객체의 값도 변경된다.

깊은 복사

새로운 메모리 공간에 객체의 모든 값을 복사 -> 다른 메모리 주소 값을 참조하기 때문에 복사된 객체의 변경 값은 원본 객체의 값에 영향을 주지 못한다.

방어적 복사

생성자로 받은 가변 데이터들을 외부에서 변경하는 것을 막기 위해 복사본을 이용하는 방법으로 대표적으로 일급 컬렉션을 만들어서 방어적 복사를 구현하는 방법이 있다.

❔ 엥 그럼 깊은 복사와 방어적 복사의 차이가 뭐야? 언제나 그랬듯이 누군가의 우테코 기록
깊은 복사 : 완전히 새로운 객체로 복사
방어적 복사 : 컬렉션만 새거, 주소는 그대로 담아 원본을 참조

❔ 엥 그러면 방어적 복사 왜 하는겨..? 이게 얕은 복사 아님?
방어적 복사로 불변성을 얻고자 한다면, 요소 클래스를 불변으로 설계하면 가능합니다

방어적 복사와 깊은 복사가 어느때 다르게 쓰이는지는 언젠가 공부할 일이 있겠지..^^..



진짜 너무 수업 내용 너무 긴거 아니냐며.. 복습하는데만 이렇게 오래 걸리다니..
애기는 너무 힘드러 ~

친구(무비)한테 우테코에 19살 있다고 했더니.. 본인도 아가라고 시전..
아침부터 아가 2명이 우렁차게 울었다는 소문이.. (👤👥👤👥👤👤👥👥👥)



👻 VO, DTO, DAO
👻 flyweight pattern
👻 그놈의 가비지 컬렉터 - JVM에서는 자동으로 객체 파괴, 힙 메모리 확보를 위해
👻 방어적 복사 vs 깊은 복사

profile
𝙸 𝚐𝚘𝚝𝚝𝚊 𝚕𝚒𝚟𝚎 𝚖𝚢 𝚕𝚒𝚏𝚎 𝙽𝙾𝚆, 𝙽𝙾𝚃 𝚕𝚊𝚝𝚎𝚛 !

0개의 댓글