[우테코] 로또 피드백 2

Yerin·2023년 3월 5일

우아한테크코스

목록 보기
6/10
post-thumbnail

수업을 시작하기에 앞서 제이슨이 C#이 가장 트렌드를 이끄는 언어라면서 ~
코틀린의 미래를 보고 싶다면 C#을 보라고 했다 (이런 말 안했을수도 있음.. 그냥 내 기억.. 😉)

저번 시간에 진짜 머리 터지는 줄 알아서 수업 내용 정리는 뒤로하고 로또 2단계 미션까지 어제 제출했었다. 그리고 오늘 들은 수업 내용 까먹기 전에 바로 정리 갈기기 ~ (하하호호)

하지만 결국 포스팅은 2주일이 지나서야 올리는 사람.. (나야나.. 나야나..)


value class

먼저 저번에 동등성-동일성에 대한 퀴즈가 재밌으셨는지 value class에 대해서도 언급하고 지나갔다.

wrapper 클래스를 사용하면 함수의 가독성이 좋아진다.
하지만 호출할 때마다 객체를 생성해야하기 때문에 비용이 발생한다.
이 비용을 절감하기 위해 만들어진 것이 value class이다.

특징

  • value class는 인스턴스화가 일어나지 않는다. (객체가 아님)
  • 힙 메모리를 차지하지 않는다. (유효성 검증만 하고 다시 반환)
  • decompile을 해보면, 다른 class init과 다르게 require문이 생성자가 아닌 정적 팩토리 메서드 내에 존재
@JvmInlinevalue class LottoNumeber(val number: Int) {
	init {
		require(number in 1..45)    	
    }
}


@Test
fun `동일성비교`() {
	// 통과
    assertThat(LottoNumber(1)).isEqaulTo(LottoNumber(1))
    // 실패, 동일성 비교 연산자 사용 불가
    assertThat(LottoNumber(1)).isSameAs(LottoNumber(1))
	test(LottoNumber(1))
}

// number로 받게 만들었는데 디컴파일 해보면 int로 받는 것을 알 수 있다. 
private fun test(number: LottoNumber // number: Int) {
	println(number)
}

// -> 로또 넘버를 생성 시 (1) 1부터 45 까지 캐싱을 해두어도 되고, (2)value class를 활용할 수도 있다.

🤔 아니 그러면 data 클래스랑 value 클래스랑 다른게 뭐야..?


클래스 간의 의존 관계

a와 b가 알고있다!

상속

상속은 코드를 재사용하는 강력한 수단이지만, 이에 대항하는 단점이 존재한다.

SOLID 중 리스코프 치환법칙을 위반 할 수 있다
코틀린은 기본적으로 final 이기 때문에 상속을 위해서는 open으로 바꿔줘야 한다.

아래는 상속을 잘못했을 경우 나타나는 예시이다.

class LottoNumbers : HashSet<LottoNumber>() {
    var addCount = 0
        private set

    override fun add(lottoNumber: LottoNumber): Boolean {
        addCount++
        return super.add(lottoNumber)
    }

    override fun addAll(c: Collection<LottoNumber>): Boolean {
        addCount += c.size
        return super.addAll(c)
    }
}

// 통과
@Test
fun add() {
	val numbers = LottoNumbers()
    numbers.add(LottoNUmber(1))
    assertThat(numbers.addCount).isEqualTo(1)
}

// 통과
@Test
fun add2() {
	val numbers = LottoNumbers()
    numbers.add(LottoNUmber(1))
    numbers.add(LottoNUmber(1))
    assertThat(numbers.addCount).isEqualTo(2)
    assertThat(numbers.size).isEqualTo(1)
}

// 실패
// HashSet에게 모든 걸 위임 함 -> addAll에서 add를 호출 but 이 add는 override된것임.. => 4
// 원래 있는 addAll 사용하면 통과 댐 -> HashSet의 구조를 알아야 해결할 수 있는 방법
@Test
fun add3() {
	val numbers = LottoNumbers()
    numbers.addAll(listOf(LottoNUmber(1), LottoNUmber(45)))
    assertThat(numbers.addCount).isEqualTo(2)
}

상속이 적절한 경우?
설명만 들어보면 상속에는 장점이 아닌 것 같다.
상속은 언제 사용하는 것이 좋을까? ==> 확장이 아닌 정제
확장은 새로운 행동을 덧붙여 기존의 행동을 부분적으로 보완하는 것을 의미하고
정제한 부분적으로 불완전한 행동을 완전하게 만드는 것을 의미한다.

객체 지향 초기에 가장 중요시 여기는 개념은 재사용성(reusability)이었지만, 지금은 워낙 시스템이 방대해지고 잦은 변화가 발생하다 보니 유연성(flexiblity)이 더 중요한 개념이 되었다.

  • 기존 클래스를 확장하는 대신 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하자.
  • 상속은 반드시 하위 클래스가 상위 클래스의 '진짜' 하위 타입인 상황에서만 쓰여야 한다.
  • 클래스 B가 클래스 A와 is-a 관계일 때만 클래스 A를 상속해야 한다. (B가 정말 A인가?)

클래스 분리

클래스 분리 이야기를 하고 있었는데 이렇게 멀리 돌아서 왔다. (ㅋㅋ 기억이 안난다면 저번 게시물 참고 !)

🤔 우리는 클래스를 어떻게 바라봐야할까 ? 저번 포스팅에도 적었던 말이다.

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

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

함수의 인자 수를 최소화한다.

private 함수를 테스트 하고 싶다

새로운 클래스로 만들어서 테스트 할 수 있다.
다음의 예시는 Rank라는 새로운 클래스를 만들어 테스트를 해볼 수 있을 것이다.

fun match(userLotto: Lotto, winningLotto: WinningLotto): Int {
    val matchCount = match(userLotto, winningLotto)
    val matchBonus = userLotto.contains(bonusNumber)
    return rank(matchCount, matchBonus)
}

private fun rank(matchCount: Int, matchBonus: Boolean): Int {
    if (matchCount == 6) {
        return 1
    }
    if (matchCount == 5 && matchBonus) {
        return 2
    }
    if (matchCount > 2) {
        return 6 - matchCount + 2
    }
    return 0
}

일급 컬렉션

일급 컬렉션을 사용하다보면 계속해서 Lotto의 numbers를 꺼내 쓰는 일이 발생한다.
일일이 꺼내쓰지 않기 위해서 자체적으로 containsforEach와 같은 메서드를 구현할 수 있다.

class Lotto(val numbers: Set<LottoNumber>) {
	init {
    	require(numbers.size == 6)
    }
    
    fun contains(number: LottoNumber): Boolean {
    	return  numbers.contains(number)
    }
    
    fun forEach(action: (LottoNumber) -> Unit) {
    	return numbers.forEach(action)
    }
}

하지만 lotto.numbers.forEach 도 싫고, lotto 내 자체 구현한 forEach도 싫다면?
==> by 를 사용하여 위임(delegation)을 할 수 가 있다.
위임해서 사용하면 lotto.forEach 나 lotto.map을 자유롭게 사용이 가능해진다. (자동 override)
클래스를 위임시에는 by 키워드의 왼쪽에는 인터페이스, 오른쪽엔 해당 인터페이스를 구현한 클래스가 필요하다.

class Lotto(val numbers: Set<LottoNumber>): Set<LottoNumbers> by numbers {
	init {
    	require(numbers.size == 6)
    }
}

❔ 그렇다면 위의 코드와 아래의 코드 중 어떤 것이 좋을까? ㅇ_ㅇ ?

컬렉션 타입을 위임하여 쓰는 것이라면,, 비즈니스 로직을 뺏어쓰는 것과 같다는 리뷰를 받은 다른 크루를 보았다..
흠.. 이 부분은 고민을 더 해봐야겠지만, 아무래도 상황에 따라 답이 달라지지 않을까 싶다.

❔ 그렇다면 상속과 위임의 차이는 무엇일까?
클래스 위임과 상속의 차이
상속 : is a
위임 : has a



기타

리스코프 치환 원칙 : LSP (Liskov Substitution Principle)

부모 클래스와 자식 클래스 사이의 행위가 일관성이 있어야 한다.
자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행이 보장되어야 한다.
즉, 부모 클래스의 인스턴스를 사용하는 위치에 자식 클래스의 인스턴스를 대신 사용했을 때 코드가 원래 의도대로 작동해야 한다는 뜻.


더 알아봐잉
👻 SOLID 원칙
👻 by 위임 더 알아보기

지금 포스팅이 엄청나게 밀려서 일요일-월요일 사이에 모든 걸 해결해야한다..
진짜 눈 깜빡하는 사이에 일이 왜 이렇게 쌓인건지 ㅜㅜ 하.. 그래도 화이(팅)

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

0개의 댓글