Intro
기존 언어를 사용하다가 다른 언어를 공부하게 되면 제일 처음 확인하는 부분이 자료형이지 않을까 싶다. 물론 구조적 차이 등에 대한 부분도 있지만 코드를 작성하기 위해서는 변수를 어떻게 생성하고 사용하는지에 대한 부분이다.
나는 기존에 C/C++ 등의 언어로 42서울과제를 진행해왔고, Spring에 대한 공부를 시작하면서 Java를 처음 접하였다. 그런데 Entity에서 PK를 생성할 때 자료형을 long을 사용한다고 하는데 작성된 코드들을 보면
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // Long 자료형과 long 자료형은 다르다!
이러한 식으로 java.lang 라이브러리에 있는 Long이라는 자료형(?)을 사용한다.
그렇다면 이 Long은 대체 무엇이길래, 기본 자료형이 long을 대체하고 사용되는지에 대한 궁금증을 가지게 되었다.
Wrapper 클래스
C/C++등을 공부함에 있어서 제일 큰 고비라는 포인터(*) 자료형이 자바에는 없다. 대신에 자료형 타입을 기본형 타입(Primitive Type)과 참조형 타입으로 나누어서 사용할 수 있도록 나누었다.
Java의 자료형 타입
기본 타입: byte, int, char, short, long... 등 등
참조 타입: class, interface, record(Java16이후) .. 등
Wrapper 클래스란 이러한 기본 타입의 자료형을 객체 형태로 바꿔주는 것이다. 자바는 객체지향 언어이고, 객체지향 프로그래밍을 하는 입장에서는 기본 타입보다는 객체로 사용하는것이 더 올바른 방향이라고 한다.
그럼 어떻게 기본 타입 자료형을 객체 형태로 바꿔주는 것일까?
Boxing과 UnBoxing
기본 타입 -> 객체로 변환하는 것을 Boxing, 객체 -> 기본 타입으로 변환하는 것을 UnBoxing이라 한다.
기본 타입 자료형 | <=> | Wrapper 클래스 |
boolean | Boolean | |
byte | Byte | |
char | Character | |
int | Integer | |
short | Short | |
float | Float | |
long | Long | |
double | Double |
Java 언어를 기준으로, char과 int형태를 제외하고는 맨 앞자리를 대문자로 변경하면 된다.
그렇다면 기본 타입과 Wrapper 클래스에 어떠한 차이가 있는지 알아보자.
위 사진을 보면, 기본 타입자료인 Value에 1을 넣으면 long형태로 저장하게 되는데 Wrapper class에는 이렇게 값을 넣으니까 오류가 나게 된다. 객체 형태이기 때문에 new 키워드를 이용해 생성자 형태로 값을 넣어주니 생성이 되었다.
그럼 매번 이렇게 불편한 방식으로 사용을 해야할까?
정답부터 말하자면 아니다. Java 1.5버전에 오토박싱 / 언박싱 기능이 추가되었다.
위의 코드를 확인해보면, 1 뒤에 L을 붙였더니 자동으로 Long 타입으로 인식을 하게 되었다. 또한, 기본 자료형타입에 Boxing 자료형 데이터를 넣었더니 인식이 되는 것으로 나타났다. 그렇다면 한번 print를 해보자
데이터들이 전부 같게 나오는 것을 확인할 수 있다. 그렇다면 어차피 똑같은 1을 쓰는 건데 왜 Wrapper 클래스를 사용하는걸까?
Wrapper 클래스의 사용이유
Wrapper 클래스의 사용이유에 대해서 설명하기 전에 먼저 코드를 하나 작성해보겠다.
public class Main {
static long Primitive;
static Long Wrapper;
public static void main(String[] args) {
System.out.println("Primitive = " + Primitive);
System.out.println("Wrapper = " + Wrapper);
}
}
이렇게 static 변수로 선언만 하고 초기화를 하지 않고 나서 print로 찍어보면 결과가 서로 같을까? 다를까?
정답은 다르다! 이다. long 자료형의 경우에는 JVM에 의해서 자동으로 0으로 초기화 되지만, 객체인 Long 클래스는 선언하였지만 초기화하지 않았기 때문에 null로 나오게 된다.
- Null 값의 허용
- 내가 어떠한 Entity를 만들고 생성한다고 하였을 때, pk값을 정해주기 이전 값으로는 0이 맞을까 null이 맞을까? 0과 null은 서로 다르다. 0은 없다는 것을 뜻할 수도 있지만 말 그대로 0 자체가 의미가 될 수도 있다. 하지만 null의 경우 '데이터가 없다' 라는 것을 의미하기 때문에 이러한 점에서 Long을 사용하는 것이 조금 더 적합해 보인다.
- 하지만 기본값을 0으로 할당해야 하는 경우라면(쇼핑몰 재고와 같이?) 굳이 Long을 사용하지 않고 long을 사용해도 될 것 같다.
- 객체지향적 프로그래밍
- 자바는 객체지향적인 언어이다. 기본 타입도 객체로 사용할 수 있게 함으로써, 객체지향적인 프로그래밍이 가능해진다.
기본 타입과 Wrapper 클래스에 대한 차이
long의 경우 스택(Stack) 메모리에 할당이 되는 반면, Long의 경우 객체이므로 힙(Heap) 메모리에 할당되게 된다. 즉 객체 타입인 Long의 경우 메모리 할당, 가비지 컬렉션에 의해 수명 주기 관리, 필요없는 객체 해제 등 오버헤드가 발생할 수 있다는 뜻이다.
이로 인해 성능적인 측면에서 차이를 보이게 된다. 아래 코드를 보면
public class Main {
public static void main(String[] args) {
// long 사용
long startTime1 = System.nanoTime();
long sum1 = 0;
for (long i = 0; i < 1000000; i++) {
sum1 += i;
}
long endTime1 = System.nanoTime();
// Long 사용
long startTime2 = System.nanoTime();
Long sum2 = 0L;
for (long i = 0; i < 1000000; i++) {
sum2 += i;
}
long endTime2 = System.nanoTime();
// 결과 출력
System.out.println("long 연산 시간: " + (endTime1 - startTime1) + " 나노초");
System.out.println("Long 연산 시간: " + (endTime2 - startTime2) + " 나노초");
}
}
맨 처음은 long 타입을 이용해 100만번 연산을 시행하고, 아래는 Long 타입을 이용해 100만번 연산해보았다. 그 결과는 다음과 같다.
Wrapper 클래스의 경우, 내부적으로 들어가면 엄청나게 많은 static 함수들이 존재함을 알 수 있다. 아무래도 새로운 자료형을 만드는 것이니, 기본적인 long이 할 수 있는 부분들을 처리하거나 하는 함수들을 작성한 것 같다.
공부하다 보니 Wrapper 클래스에 대해서 재밌는 부분을 알아낼 수 있었다.
public class Main {
public static void main(String[] args) {
long a = 1;
long b = 1;
if (a == b)
System.out.println("일치");
else
System.out.println("다름");
Long c = 1L;
Long d = 1L;
if (c == d)
System.out.println("일치");
else
System.out.println("다름");
}
}
이러한 코드를 작성해보았다. 결과는 어떻게 나와야할까?
내 맨 처음 생각은, long은 자료형의 값끼리 비교이고, Long의 경우 equals()를 쓰지 않으면 값이 아닌 객체를 비교하니까 일치/다름 이 나오는 것이 맞겠다 였다. 정답은
일치 / 일치 였다.
응? 뭐지? 주소가 같은가? 해시코드를 찍어봐야겠다.
// 추가
System.out.println("c 객체 = " + System.identityHashCode(c));
System.out.println("d 객체 = " + System.identityHashCode(d));
엥???? 뭐야 다른 동적할당을 해준 것 같은데 왜 같다고 나오지..?
자 그럼 다음 코드를 보겠다.
public class Main {
public static void main(String[] args) {
long a = 128;
long b = 128;
if (a == b)
System.out.println("일치");
else
System.out.println("다름");
Long c = 128L;
Long d = 128L;
if (c == d)
System.out.println("일치");
else
System.out.println("다름");
System.out.println("c 객체 = " + System.identityHashCode(c));
System.out.println("d 객체 = " + System.identityHashCode(d));
}
}
값을 128로 바꿔보았다. 이전 결과를 생각해봤을때는 당연히 같은 "일치/일치/서로 같은 해시코드"
이 나오겠지?
값이 달라졌다고 결과가 달라질 수 있는건가? 당연히 달라질 수 있으니까 내용을 적고 있다. 그 이유는 다음과 같다.
궁금해서 Long 내부의 코드를 찾아보다가 발견한 부분이다. -128 ~ 127 까지는 이미 캐싱되어 있는 객체 그 자체를 return해주기 때문에 맨 처음 1L에 관련해서 같은 객체이기 때문에 "일치/일치" 가 나오게 된 것이다.
Outro
아무래도 성능적인 측면만 고려한다면 long을 사용할 때에는 long을 사용하는 것이 좋을 것같다.
그런데 우리는 객체지향 프로그래밍을 하니까 Long을 써야한다.... 그럼 성능적인 부분을 완전 포기하는 게 맞는건가?
사실 이 부분은 경험이 없어서 모르겠다.. 조금 더 경험이 쌓이면 long을 써도 되는 경우를 구분하거나 알아낼 수 있지 않을까 하는 생각이 든다.
아무튼 Entity를 설계함에 있어 왜 다른 언어와 다르게 Long이라는 자료형을 쓰는지 궁금했는데, 궁금증을 해소할 수 있는 시간이었다.
Reference
https://coding-factory.tistory.com/547
https://jaynamm.tistory.com/entry/JAVA-%EB%9E%98%ED%8D%BC-%ED%81%B4%EB%9E%98%EC%8A%A4-Wrapper-Class
'Back-End' 카테고리의 다른 글
[Back-End] 인증/인가와 쿠키/세션/토큰(JWT) (2) | 2024.01.12 |
---|---|
[AWS] AWS, EC2, S3란 무엇일까? (0) | 2023.11.10 |
[Spring] 스프링 DB 접근 기술 (0) | 2023.11.08 |
[Backend] 디자인패턴 - MVC패턴 (2) | 2023.10.09 |
[Backend] REST, REST API, RESTful (0) | 2023.10.08 |
댓글