문제의 코드는... 바로 다음의 코드였다!!!
@GetMapping("/paths")
public ResponseEntity<PathResponse> searchPath(
@RequestParam("source") Long sourceId,
@RequestParam("target") Long targetId){
return ResponseEntity.ok(pathService.findShortestPath(sourceId, targetId));
}
위의 코드를 보며 불편함을 느끼지 않았다면 그대 바로 이 포스트의 제목에 대해 무지한 자! (공격한 거 아닙니다ㅎㅎ) 나 또한 별 생각없이 long
과 Long
을 혼용해서 사용하고 있었다.
그런데 두둥! 다음과 같은 리뷰를 받았다.
Long을 primitive type이 아닌 wrapper type으로 사용하셨는데요!
type을 wrapper type을 사용하신 이유가 있으실까요?
이펙티브자바 책을 보시면 primitive type을 사용하는 것을 권장하고 있습니다 :)
오호... 이펙티브 자바 책에서는 객체화된 기본 자료형 대신 기본 자료형을 이용하라고 나와있다. 그래서 이에 대해서 정리를 하고자 한다.
여기서 기본 자료형은 int, double, boolean등을 말하고 객체화된 기본 자료형은 Integer, Double, Boolean 등을 말한다.
그렇다면 기본 자료형과 객체화된 기본 자료형의 차이는 무엇인가?
기본 자료형은 값만 가지지만 객체화된 기본 자료형은 값 외에도 신원(identity)을 가진다.
기본 자료형에 저장되는 값은 전부 기능적으로 완전한 값(fully functional value)이지만, 객체화된 기본 자료형에 저장되는 값에는 기능적으로 완전한 값 외에 아무 기능도 없는 값, 즉 null이 하나 더 있다.
기본 자료형은 시간이나 공간 요구량 측면에서 일반적으로 객체 표현형보다 효율적이다.
※ 신원(identity)를 주소값이라 생각해도 될 듯 하다.
자 이렇게 나와있는 대로 차이점을 적어봤지만 감이 잡히지 않을 것이다.
그래서 예제를 살펴보면서 차이점에 대해 감을 잡아보자!!
Comparator<Integer> naturalOrder = new Comparator<Integer>() {
public int compare(Integer first, Integer second) {
return first < second ? -1 : (first == second ? 0 : 1);
}
}
naturalOrder.compare(new Integer(42), new Integer(42)); // 1
위의 예제에서 두 Integer 객체는 42라는 동일한 값을 나타내므로, 반환 값은 0이 되어야 하지만 실제 반환되는 값은 1이다. 왜냐하면 first < second에서는 우리가 의도했던 대로 false이지만 first == second 마저 false이기 때문이다. 객체화된 기본 자료형은 신원 즉 주소값을 가진다고 했었다. 따라서 두 Integer 객체는 42라는 동일한 값을 가지지만 주소값은 다르므로 first == second에서 false를 반환하는 것이다.
정리하면 객체화된 기본 자료형에 == 연산자를 사용하는 것은 거의 항상 오류라고 봐야한다.
만약 우리가 의도했던대로 작동하게 하려면 다음과 같이 코드를 작성해야 한다.
Comparator<Integer> naturalOrder = new Comparator<Integer>() {
public int compare(Integer first, Integer second) {
int f = first;
int s = second;
return f < s ? -1 : (f == s ? 0 : 1);
}
}
naturalOrder.compare(new Integer(42), new Integer(42)); // 0
객체끼리의 ==은 주소값을 비교하지만 기본 자료형끼리의 ==은 값을 비교하므로 우리가 의도했던 대로 0이 나옴을 알 수 있다.
public class Unbelievale {
static Integer i;
public static void main(String[] args) {
if (i != 42) // 여기에서 auto-unboxing이 발생(i.intValue())하여 NPE가 발생
System.out.println("Unbelievable");
}
}
위의 경우 Unbelievable을 출력하지 않고 NullPointException을 발생시킨다.
기본 자료형(int)과 객체화된 기본 자료형(Integer)을 한 연산에서 사용하면 자동으로 객체화된 자료형이 기본자료형으로 변환(i.intValue())되는데 이 과정에서 i는 null이므로 NPE가 발생하게 되는 것이다. 이는 i가 Integer가 아닌 int였다면 발생하지 않을 예외이다.
public static void main(String[] args) {
Long sum = 0L;
for(long i=0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
위의 코드는 예상보다 훨씬 느리게 작동한다. 이는 변수 sum을 객체화된 기본 자료형에 담았기 때문인데, 오류나 경고 없이 컴파일 되는 코드지만 변수가 계속해서 객체화와 비객체화를 반복하기 때문에 성능이 느려진다.
위의 예제들로 통해 객체화된 기본자료형보다 기본자료형을 사용하는 이유에 대해 알게 되었을 것이다
그렇다면 의문이 들 것이다. 아니 그러면 언제 객체화된 기본자료형을 사용하나?
바로 컬렉션의 요소, 키, 값으로 사용할 때이다. 컬렉션에는 기본 자료형을 넣을 수 없으므로 객체화된 자료형을 사용해야 한다. 또한 제네릭에 사용될 때는 객체화된 기본자료형만 가능하다.
요약하자면 가능하다면 기본 자료형을 사용하라는 것이다. 자동 객체화는 번거로운 일을 줄여주긴 하지만, 객체화된 기본 자료형을 사용할 때 생길 수 있는 문제들까지 없애주지는 않는다. 객체화된 기본 자료형과 기본 자료형을 한 표현식 안에 뒤섞으면 비객체화가 자동으로 일어나며, 이는 성능 저하의 원인이 될 수 있고, NullPointException이 발생할 수도 있다.