2023. 12. 23. 02:44ㆍJava
[ 작성이유 ]
스트림을 공부하던 중 Stream<int>와 IntStream의 속도성능 차이에 관해서 보게 되었다. 그런데 가만히 생각해보니 int랑 Integer 둘 다 정말 많이 쓴 것 같은데 뭔 차이가 있는지 정확하게 모르고 사용하고 있는게 아닌가 싶었다. 그래서 이 내용을 정리하고 스트림에 관해서 다시 공부를 하려고 한다.
[ 고민거리 ]
Java에서 프로그램을 짜다보면 굉장히 많은 상황이 발생한다. 가장 많이 사용하는 순간을 예시로 들어보자.
for(int i = 0 ; i < 10 ; i++){
System.out.println("시간이 " + i + "초 경과했습니다.");
}
간단하게 위와 같은 코드가 있다고 생각을 해보자. 사실 이 상태는 굉장히 이상한 상태이다. 파이썬이나 다른 언어였으면 에러로 concat ~~~하면서 에러가 뜰텐데 에러 내용은 문자와 숫자를 합칠 수 없다는 내용이 뜬다. 그런데 java에서는 이런 메세지가 뜰 수도 있지만 웬만해서 안 뜬다.
그러면 아래의 상황은 어떤가? 이 경우에는 컴파일 시점에 잡히는가?
int num1 = (int)"789";
int num2 = "987";
이 경우에는 잡힌다. 이 이유에 대해서 설명하지 못한다면 Primitive Type과 Wrapper Type에 관해서 잘 모르고서 사용하고 있었다는 것을 의미한다. 각각을 차근차근 알아보고 다시 이 이유에 대해서 설명해보자.
[ Primitive Type vs Wrapper Class ]
[ Wrapper Class의 장점 ]
Primitive Type은 들으면 알 것이다. Java에는 총 8가지의 원시타입이 있다. byte, short, int, long, float, double, boolean, char 이렇게인데 이들은 각각 고정된 데이터 비트양이 있고 그것에 따라서 표현하는 방법의 차이를 가졌다. 또한 비트의 양만큼 데이터를 저장만 할 수 있다. 또한 타입 캐스팅에 대해서 자유롭지 못하고 내장된 함수가 없어서 작업을 할 때에 굉장히 불편하다.
그렇기 때문에 두번째 코드블럭처럼 사용을 하려고 들면 타입 캐스팅에 대해서 자유롭지 못하다는 조건에 의해서 에러가 난다. 그리고 이걸 primitive Type으로 변환이 가능하게 하려면 bit 단위로 작업을 해야될텐데 위의 상황은 정말 단순해보이지만 bit 단위로 처리한다면 꽤나 복잡할 것이다. 이건 개발에 있어서 시간 낭비다.
Wrapper Class도 들으면 알 것이다. Java에서 총 8가지 원시타입에 대응하는 Wrapper Class은 Byte, Integer, Short, Long, Float, Double, Boolean, Char이 있다. 원시타입과 무슨 차이가 있냐고 물어보면 원시타입은 데이터 비트 조각이고 Wrapper Class는 객체다. Wrapper Class는 객체로써 내부에 원시타입과 여러 유용한 메서드를 함께가지고 있다. 이렇게하면 얻는 것과 잃는 것은 무엇일까? 예시를 int와 Integer를 통해서 들어보자.
int라는 원시타입에서 Integer라는 Wrapper Class로 변경되면 얻는 첫 번째 장점은 null의 사용이 가능하다는 것이다. null이 사용이 가능하다는 것은 참조와 관련된 에러에 관해서 상대적으로 유연한 대처를 가능하게 해준다.
두 번째 장점은 유용한 메서드를 추가할 수 있다. 이전의 상황에서 bit 단위로 int타입의 789를 "789"로 만드는 걸 바로 생각했고 즉시 구현할 수 있다면 모르겠지만 극소수의 사람일 것이라고 생각한다. 대신에 Wrapper Class로 바뀌면 toString() 메서드로 Integer에서 String으로 변경이 가능하고 parseInt(String)으로 간단하게 String에서 Integer로 변경할 수 있다. 뿐만 아니라 더 다양한 기능도 가능하다. Integer.toBinaryString 같이 2진수로 변경된 문자열을 얻을 수도 있고 8진수, 16진수도 가능하다. 뿐만 아니라 int는 배열인 int[]에 밖에 들어갈 수 없고 내부 배열값의 변화를 주기에 자유롭지 못하지만 Integer는 List에 들어가서 더 자유로운 구현을 할 수 있게 해준다. 값이 객체화된다는 것은 이렇게나 다양한 기능으로 데이터에 유연성을 추가시켜준다.
[ Primitive Type과 Wrapper Class의 주의 사항 ]
Wrapper Class를 사용해서 얻는 게 있으면 잃는 것도 있어야 한다. 첫 번째로 잃는 것은 메모리이다. int는 4 bytes의 용량만 가지고 있으면 충분하지만 Integer를 저장하기 위해서는 16 bytes가 필요하다. 두 번째로는 추가적인 작업 시간이 소요된다. 결국엔 어디선가 계속 Wrapper Class와 Primitive Type 사이의 변환이 계속 일어난다는 건데 이는 추가적인 오버헤드를 일으킨다. 세 번째로는 아마도 객체가 계속 생성됐다가 사라지는 현상이 일어나니 JVM 쪽의 Garbage Collection이 처리해야 될 양이 늘어날 것이다.
하지만 메모리 4배 늘어나는 거 쯤이야 생각보다 얼마 안되고 내부 리소스 문제는 Wrapper Class로 얻을 수 있는 장점에 비하면 솔직하게 별 것 아니라고 생각을 한다. 사실상 메모리, 작업시간을 유연성과 tradeoff한 것과 마찬가지라고 생각된다.
** 2024-02-17 추가 **
Primitive Type과 Wrapper Class를 사용할 때는 주의 사항이 있다. Wrapper Type의 단점에 가까운데, 자바는 기본적으로 Primitive Type을 사용할 때엔 값을 복사해서 넘겨준다. 반대로 Wrapper Class는 객체이고 객체들끼리는 서로 참조를 넘겨준다. 그렇기 때문에 Wrapper class를 사용하는 경우에 있어서는 객체를 사용하는 경우, 객체가 공유되는 상황을 정말 조심해야 한다.
특정한 값의 필드로 객체가 사용되는 상황 등에서 멀티 쓰레드 환경 혹은 JPA 같이 영속성 컨텍스트 내부에서 관리하는 상황의 경우, 괜히 메모리 좀 아껴보겠다고 별로 쓰지도 않을꺼 재활용하면 A를 변경했는데 B도 같이 변경되는 상황이 벌어질 수 있다. 그렇기 때문에 Wrapper Class를 필드로 사용하는 경우 new 생성자로 새로 생성해서 만들어주는 걸 권장한다. 혹은 변경할 수 없게 setter를 싹 다 닫아버려야 한다.
[ 고민거리 ]
이제 다시 고민거리로 돌아와서 생각을 해보면 대충감이 올 것이다. 만약 위의 글을 내가 잘 썼고 읽는 사람이 잘 이해했다면 다음과 같은 예측이 가능할 것이다. 첫번째 코드블럭의 반복문에서 i가 primitive type인 int이니 Integer로 바뀌어서 toString()이 자동적으로 호출이 되었을 것이고 사실 System.out.println("시간이 " + Integer.valueOf(i).toString() + "초 경과했습니다."); 이런 과정을 거친 것이라는 걸 말이다.
그런데 다시 생각을 해보면 정적 팩토리 메서드 형태로 우리는 Integer 객체를 생성하기 위해서 Integer.valueOf(i) 이런 걸 사용한 적이 있는가? Integer(String) 형태로는 좀 써봤을지 몰라도 기억상엔 없었던 것 같다. 이것이 어떻게 가능한 것일까?
자바5부터 도입된 기능인데 autoboxing과 autounboxing이라는 기능이 있다. Primitive Type을 Wrapper Class로 만드는 것이 autoboxing, 그리고 반대의 경우가 autounboxing이다. 이런 기능이 있기 때문에 우리는 아래와 같은 코드를 작성해도 에러가 나지 않고 코드를 작성할 수 있게 해준다. 이 기능때문에 int와 Integer에 관해서 구분하지 않고 사용을 해왔고 차이에 관해서 인지를 하지 못했던 것 같다.
아래는 오토박싱과 오토언박싱을 사용한 코드이다.
// 오토 박싱
int i = 10;
Integer num = i;
// 오토 언박싱
Integer num = new Integer(10);
int i = num;
오토 박싱과 언박싱이 없었다면 이렇게 작성해야 했을 것이다.
// 박싱
int i = 10;
Integer num = new Integer(i);
// 언박싱
Integer num = new Integer(10);
int i = num.intValue();
오토 박싱과 오토 언박싱 덕에 원시타입과 Wrapper 클래스 간의 혼용으로 인해서 생기는 에러는 잘 생기지 않게 되었을 것이다. 대신 이를 혼용해서 사용한다면 객체를 생성하고 없애는데 드는 오버헤드가 커질 수 있으니 둘의 구분을 배웠으니 앞으로는 주의해서 사용해야겠다.
[ 참고자료 ]
https://gyoogle.dev/blog/computer-language/Java/Auto%20Boxing%20&%20Unboxing.html
https://wildeveloperetrain.tistory.com/12
https://www.javatpoint.com/int-vs-integer-java
'Java' 카테고리의 다른 글
Java의 메모리 구조 (0) | 2023.12.28 |
---|---|
JVM, JRE, JDK의 차이가 무엇일까? (0) | 2023.12.28 |
Enum Type에 대해서 (0) | 2023.11.06 |
자바의 정석, 열거형(Enum) 2 (0) | 2023.08.29 |
자바의 정석, 열거형(enums) 1 (0) | 2023.08.07 |