두번째 미션 로또 피드백에 대한 강의 정리입니다.
데일리 미팅때 웹으로 자기소개 만든거 소개하고 바아로 수업.. (당담 코치가 제이슨이어서 계속 연장선인 너낌..)
오늘 졸업식 갈거라 수업 끝나면 바아로 가방 싸서 나갈 예정 ㅎㅎ
❔ 모든 것을 객체로 포장해야할까? -> 이것 저것 시도해 보며 나만의 기준을 만들자
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
}
리팩터링, 어디서 어떻게 시작할 것인가?
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
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 사용
제이슨이 수업을 하다가 와다다다다다다다 진도를 나가서 클래스 분리하는 방법은 다음 시간에 배우게 되는데..! 일단 다음 내용부터 확인해보자.
인스턴스도 새로 안만들고 생성자의 역할을 하는 것은 무엇일까?
바로 (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))
}
아무튼 아무튼 이름 정할때도 그렇고 수정도 굉장히 번거롭다
주소값 자체를 복사 -> 복사된 객체의 인스턴스는 원본 객체의 인스턴스와 같은 메모리 주소를 사용한다. 복사된 객체의 값이 변경되면 원본 객체의 값도 변경된다.
새로운 메모리 공간에 객체의 모든 값을 복사 -> 다른 메모리 주소 값을 참조하기 때문에 복사된 객체의 변경 값은 원본 객체의 값에 영향을 주지 못한다.
생성자로 받은 가변 데이터들을 외부에서 변경하는 것을 막기 위해 복사본을 이용하는 방법으로 대표적으로 일급 컬렉션을 만들어서 방어적 복사를 구현하는 방법이 있다.
❔ 엥 그럼 깊은 복사와 방어적 복사의 차이가 뭐야? 언제나 그랬듯이 누군가의 우테코 기록
깊은 복사 : 완전히 새로운 객체로 복사
방어적 복사 : 컬렉션만 새거, 주소는 그대로 담아 원본을 참조
❔ 엥 그러면 방어적 복사 왜 하는겨..? 이게 얕은 복사 아님?
방어적 복사로 불변성을 얻고자 한다면, 요소 클래스를 불변으로 설계하면 가능합니다
방어적 복사와 깊은 복사가 어느때 다르게 쓰이는지는 언젠가 공부할 일이 있겠지..^^..
진짜 너무 수업 내용 너무 긴거 아니냐며.. 복습하는데만 이렇게 오래 걸리다니..
애기는 너무 힘드러 ~

친구(무비)한테 우테코에 19살 있다고 했더니.. 본인도 아가라고 시전..
아침부터 아가 2명이 우렁차게 울었다는 소문이.. (👤👥👤👥👤👤👥👥👥)
👻 VO, DTO, DAO
👻 flyweight pattern
👻 그놈의 가비지 컬렉터 - JVM에서는 자동으로 객체 파괴, 힙 메모리 확보를 위해
👻 방어적 복사 vs 깊은 복사