💡 Intro
저번주 금요일에 졸업식을 가느라 우테코 수업에 참여하지 못했다. 여태까지 했던 수업들과는 좀 다르게 어려운 내용을 다루었던 것 같고 그 내용에 대해 크루들에게 자세하게 설명을 전해들었다. 그 중에서도 전 페어가(아크) 정리한 글의 내용이 한 번에 이해할 수 있을만큼 좋아서 그 내용을 참고하여 내가 이해한 방식으로 다시 정리해보려고 한다.
❓ 동등성(identity)
- 동일한 정보를 가지고 있는 오브젝트를 의미한다.
- 메모리 상에 다른 오브젝트가 존재하는 것이고, 오브젝트의 기준에 따라 동등하다고 판단한다.
자바에서 동등성을 비교하기 위해서는 == 이 아닌 .equals를 사용해야 한다.
하지만 코틀린에서 == 연산자는 내부적으로 .equals를 호출하기 때문에 코틀린에서 동등성을 비교하기 위해서는 == 연산자를 사용하면 된다.
❓ 동일성(equality)
- 두 개의 오브젝트가 완전히 동일한 것을 의미한다.
- 하나의 오브젝트가 존재하는 것이고, 그 오브젝트를 참조하는 것이다. 즉, 메모리 주소가 동일하다.
자바에서 == 연산자는 피연산자의 주소 값이 같은지를 비교하고 이 말은 곧 동일성을 비교할 수 있다는 말과 같다.
하지만 코틀린에서는 === 연산자를 이용하여 피연산자의 주소 값을 비교할 수 있고 동일성을 판단할 수 있다.
🔢 Int
코틀린은 원시 타입(primitive type)과 래퍼 타입(wrapper type)을 구분하지 않는다. 자바에서는 int와 int를 감싸고 있는 래퍼 클래스 Integer가 있지만 코틀린에서는 Int만 사용한다.
자바의 Integer 코드를 살펴보면 다음과 같다.
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
@Deprecated(since="9", forRemoval = true)
public Integer(int value) {
this.value = value;
}
기본적으로 Integer 클래스는 -127~128 까지의 Integer Cache를 가지고 있다. 즉, -127~128까지의 숫자를 미리 인스턴스로 생성하여 캐시에 가지고 있다는 것이다. 그래서 -127~128 사이의 숫자가 들어오면 미리 만들어두었던 인스턴스를 반환하고 그게 아니라면 새로운 Integer 객체를 생성하여 반환해준다. 이 점을 기억하고 들어가도록 하자.
Test
테스트 전에 앞에서 이야기한 동등성과 동일성을 어떻게 비교하는지를 먼저 알아보자.
동등성 비교
// kotlin
@Test
fun `EqualsTest`(){
val actual: Int = 1
val expected: Int = 1
assertThat(actual).isEqualTo(expected)
}
// java
@Test
public final void EqualsTest() {
int actual = 1;
int expected = 1;
Assertions.assertThat(actual).isEqualTo(expected);
}
// Assertion.assertThat.isEqualTo()
public SELF isEqualTo(int expected) {
integers.assertEqual(info, actual, expected);
return myself;
}
actual과 expected 모두 1을 넣어 초기화 해주고 isEqualTo() 를 이용하여 동등성을 비교한다. 코틀린 코드를 자바로 디컴파일하면 actual, expected 모두 int로 변환되어 들어가는 것을 확인할 수 있다. 비교를 int 대 int 로 하고 있기 때문에 True가 나올 것이다.
동등성을 비교할 수 있는 isEqualTo()의 파라미터로는 int가 넘어가는 것에 주목하자.
동일성 비교
// kotlin
@Test
fun `IdentityTest`(){
val actual: Int = 1
val expected: Int = 1
assertThat(actual).isSameAs(expected)
}
// java
@Test
public final void IdentityTest() {
int actual = 1;
int expected = 1;
Assertions.assertThat(actual).isSameAs(Integer.valueOf(expected));
}
// Assertion.assertThat.isSameAs()
public SELF isSameAs(Object expected) {
objects.assertSame(info, actual, expected);
return myself;
}
동등성 테스트와 마찬가지로 actual과 expected를 1로 초기화 해주지만 이번에는 isSameAs() 를 이용하여 동일성을 비교한다. isSameAs()의 파라미터로 무엇이 들어가는지 확인하면 동등성의 테스트와는 다른 것을 볼 수 있다.
동일성을 비교할 수 있는 isSameAs()의 파라미터로는 Object가 넘어간다. 이러한 이유 때문에 isSameAs()의 파라미터로 expected를 바로 넘겨주는 것이 아닌 Integer.valueOf(expected)를 통해 넘겨주는 것이다.
그럼 이제 확인하고자 하는 테스트에 대해 알아보자. 총 5가지의 테스트를 진행할 것이고 결과를 미리 확인하면 다음과 같다.
어떤 테스트를 진행하는 것일까? 하나씩 알아보도록 하자.
Test1
// kotlin
@Test
fun `Test1`(){
val actual: Int = 1
val expected: Int = 1
assertThat(actual).isEqualTo(expected)
assertThat(actual).isSameAs(expected)
}
// java
@Test
public final void Test1() {
int actual = 1;
int expected = 1;
Assertions.assertThat(actual).isEqualTo(expected);
Assertions.assertThat(actual).isSameAs(Integer.valueOf(expected));
}
첫 번째 테스트 코드다. 동등성과 동일성을 비교하는 코드와 정확히 일치한다. actual과 expected 모두 1을 넣어 초기화 했고, isEqualTo()를 통해 동등성을, isSameAs()를 통해 동일성을 비교한다.
Test1의 결과는 True다. 동등성을 비교하는 isEqualTo()의 결과는 쉽게 유추할 수 있지만 동일성을 비교하는 isSameAs()의 결과도 True인 이유는 알쏭달쏭하게 느껴진다. 그 이유는 앞에서 이야기한 Integer Cache와 연관이 있는데 다시 상기해보면 -127~128 사이 값은 미리 인스턴스로 생성해서 가지고 있고, 그 사이 값이 들어오면 해당 인스턴스를 반환하고 그 외의 값이 들어오면 새로 생성해서 반환해준다고 했다.
따라서 같은 오브젝트를 참조하는 것이고 결과도 True가 되는 것이다.
Test2
// kotlin
@Test
fun `Test2`(){
val actual: Int = 1000
val expected: Int = 1000
assertThat(actual).isEqualTo(expected)
assertThat(actual).isSameAs(expected)
}
// java
@Test
public final void Test2() {
int actual = 1000;
int expected = 1000;
Assertions.assertThat(actual).isEqualTo(expected);
Assertions.assertThat(actual).isSameAs(Integer.valueOf(expected));
}
그럼 Test2의 결과는 어떻게 될까? False다. 왜 False냐고?
Integer Cache는 -127~128사이 값만을 가지고 있다. 즉, 1000이 들어오면 그 때마다 새로운 객체를 반환할 것이고 isEqualTo()는 True를 반환하겠지만 isSameAs() 는 비교하는 두 객체의 주소가 다르기 때문에 False를 반환한다.
Test3
// kotlin
@Test
fun `Test3`(){
val actual: Int = 1
val expected: Int = 1
assertThat(actual == expected).isTrue
assertThat(actual === expected).isTrue
}
// java
@Test
public final void Test3() {
int actual = true;
int expected = true;
AbstractBooleanAssert var10000 = Assertions.assertThat(true);
Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(actual == expected)");
var10000.isTrue();
var10000 = Assertions.assertThat(true);
Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(actual === expected)");
var10000.isTrue();
}
Test3의 결과는 어떻게 될까? True다.
자바 코드의 Assertions.assertThat()을 보면 true가 들어가있는 것을 볼 수 있다. 엥? 코틀린 코드에서는 분명히 actual == expected를 넣어줬는데?
어떻게 된 것이나면 assertThat으로 들어가기 전에 actual == expected 비교를 먼저 진행하고 결과인 true를 넘겨주기 때문에 Integer로 변환되지 않는 것이다.
Test4
// kotlin
@Test
fun `Test4`(){
val actual: Int? = 1
val expected: Int? = 1
assertThat(actual == expected).isTrue
assertThat(actual === expected).isTrue
}
// java
@Test
public final void Test4() {
Integer actual = 1;
Integer expected = 1;
AbstractBooleanAssert var10000 = Assertions.assertThat(Intrinsics.areEqual(actual, expected));
Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(actual == expected)");
var10000.isTrue();
var10000 = Assertions.assertThat(actual == expected);
Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(actual === expected)");
var10000.isTrue();
}
Test4의 결과는 무엇일까? True다.
언뜻보면 Test3과 다를게 없어 보이지만 actual과 expected에 nullable타입을 넣어주었다. 자바에서 int는 null값을 가질 수 없기 때문에 int가 아닌 Integer로 변환된 것을 볼 수 있다. 그리고 역시나 Integer Cache의 이유로 actual과 expected는 동등하고 동일하다.
Test5
// kotlin
@Test
fun `Test5`(){
val actual: Int? = 1000
val expected: Int? = 10000
assertThat(actual == expected).isTrue
assertThat(actual === expected).isTrue
}
// java
@Test
public final void Test5() {
Integer actual = 1000;
Integer expected = 10000;
AbstractBooleanAssert var10000 = Assertions.assertThat(Intrinsics.areEqual(actual, expected));
Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(actual == expected)");
var10000.isTrue();
var10000 = Assertions.assertThat(actual == expected);
Intrinsics.checkNotNullExpressionValue(var10000, "assertThat(actual === expected)");
var10000.isTrue();
}
마지막 테스트의 결과는 어떻게 될까? False다.
Test4와 마찬가지로 nullable타입을 넣어주었고, 같은 이유로 Integer로 변환된 것을 볼 수 있다. 초기화해준 1000은 Integer Cache 범위를 벗어나기 때문에 매번 새로운 객체를 반환해준다. 따라서 isSameAs() 에서 False를 반환한다.