java

[Java] 기본 타입 vs 참조 타입

kidmillionaire1998 2023. 9. 5. 18:03

[개요] 

자바의 데이터 타입은 기본 타입과 참조 타입으로 나눌 수 있다, 

각각의 기본 타입에는 대응하는 참조 타입이 하나씩 있으며, 이를 박싱된 기본 타입이라고 한다. 

 

기본 타입(원시 타입, primitive type) : int, double, boolean, long ... 

참조 타입(박싱된 기본 타입, wrapper class) : Integer, Double, Boolean, Long ...  

 

[차이] 

주된 차이는 다음과 같다. 

 

1. 기본 타입은 값만 가지고 있으나, 참조 타입은 값에 더해 식별성이란 속성을 갖는다. 

 

이러한 이유로, 참조 타입은 값이 같더라도, 서로 다르다고 식별될 수 있다. 

그러므로, 참조 타입의 값을 비교하기 위해서는, ==을 사용하면 안된다. 

 

[예시] 

Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
            return i < j ? -1 : (i == j ? 0 : 1);
        };

값이 같을 때, 즉, naturalOrder.compare(new Integer(42), new Integer(42))을 호출했을 때, 모두 42라는 같은 값이기 때문에, 0 출력 될 것이라는 예상과 다르게, 1을 출력한다. 

 

첫 번째 검사(i<j)에선, 오토박싱 되어 있던 Integer 인스턴스는 기본 타입으로 변환되어, i값이 j값보다 작지 않으므로, 

다음검사로 넘어가게 된다. 

 

다음 검사에서는, ==연산자는 값을 검사하는 것이 아니라, 인스턴스의 주소가 같은지 판단, 즉, 같은 인스턴스인지 판단하게 된다. 

그러므로, 아예 같은 객체를 비교하는 것이 아니라, 다음과 같이 값만 다른 참조 타입을 비교할 때, ==을 사용하면 안된다. 

 

[예시 수정 방안] 

위의 코드를 수정하려면, 다음과 같은 방법이 있다. 

 

1. 기본 타입을 다루는 비교자가 필요하다면, Comparator.naturalOrder()을 사용한다. 

 

2. 기본 타입으로 변환하여 처리한다. 

Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
            int i = iBoxed, j = jBoxed; // 오토박싱
            return i < j ? -1 : (i == j ? 0 : 1);
        };

 

 

2. 기본 타입은 언제나 유효하나, 참조 타입은 유효하지 않은 값인 null값을 가질 수 있다. 

 

null값을 가질 수 있기 때문에, 이를 신경쓰지 않고 코드를 작성하게 되면, 무시무시한 NullPointerException을 만나볼 수 있다. 

public class Unbelievable {
    static Integer i;

    public static void main(String[] args) {
        if (i == 42)
            System.out.println("믿을 수 없군!");
    }
}

null이 가능하기 때문에, 초기화가 되지 않은 i의 초기값은 null이 된다. 

null값으로 초기화된 i가 기본 타입인 42와 비교를 하게 되는데, 

참조 타입이 기본타입과 비교가 될때는, 참조타입의 박싱이 기본 타입으로 풀리게 되고(언박싱), 

결국, null을 참조하고 있던 i의 언박싱이 일어나게 되면, NullPointerException이 발생하게 된다. 

 

이를 해결하려면, Integer을 int로 바꿔주면 된다. 

 

3. 기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다. 

 

기본 타입은 스택 메모리에 저장이 되어있지만, 

참조 타입은, 하나의 인스턴스 이기 때문에 스택메모리에는 참조 주소 값만 가지고 있고, 실제 값은 힙에 할당된다. 

 

즉, 박싱이 진행되면서도, 박싱 타입 인스턴스를 힙 메모리에 올리게 되는데, 반복해서 힙 영역에 해당 인스턴스를 생성시키면, GC에 의해 힙 영역의 메모리가 수거가 되면서, 메모리의 단편화(Fragmentation)가 발생한다. 

 

단편화가 진행되면서, 힙에 올리려는 객체들이 여유 공간을 가지는 메모리를 찾기 위해 더 많은 시간을 소요해야하며,

객체가 메모리에 올라갔을 때의 메모리 공간은 연속적이어야하는데, 단편화가 심해지게 되면, 연속적인 공간이 줄어들게 되면서, 인스턴스 생성에 실패할 수 있다. 

 

JVM은 이러한 위험을 막기 위해 Compaction을 통해 단편화를 막으려하는데, 이는, 분산된 객체들을 Heap의 시작 주소로 모아 채운다. 당연히, 이러한 작업이 계속 발생하게 되면 성능의 저하로 이어진다. 

 

Fragmentation => Compaction

 

이러한 과정을 종합해보면, 박싱 연산을 수행하면, 힙 영역에 메모리 에 반복적으로 올라갔다가, GC에 수거를 당하면서 메모리 파편화가 이루어지며, 이는 JVM의 Compaction까지 유발하게 되며, 성능의 저하로 까지 이어진다. 

 

[예시] 

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을 기본 타입이 아닌 참조 타입으로 선언하였기 때문에, 성능적으로 느려진다. 

이러한 경우에는 전혀 참조 타입을 쓸 필요성이 없으며, 기본 타입으로 선언하는 것이 필요하다. 

 

그럼, 참조 타입은 언제 써야하나? 

1. 컬렉션 

컬렉션의 원소, 키 , 값등에서 쓰인다. 컬렉션에선 기본 타입을 담을 수 없다. 

 

2. 리플렉션 

리플렉션에서의 함수를 호출할때, 기본 타입이 아닌 참조 타입을 사용한다. 

 

 

개인적인 경험 (DB + JPA) 

[에러 메시지] 

Request processing failed; nested exception is org.springframework.orm.jpa.JpaSystemException: Null value was assigned to a property [domain.user.entity.User.kakaoId] of primitive type setter of domain.user.entity.User.kakaoId; nested exception is org.hibernate.PropertyAccessException: Null value was assigned to a property [class shop.cazait.domain.user.entity.User.kakaoId] of primitive type setter of shop.cazait.domain.user.entity.User.kakaoId] with root cause

 

=> database에서 nullable한 값에 절대 기본 타입을 할당하면 안된다. 

위에서 언급한 2. 기본 타입은 언제나 유효하나, 참조 타입은 유효하지 않은 값인 null값을 가질 수 있다. 에 해당하는 구체적인 사례라고 생각한다. 

 

카카오톡 회원가입을 구현하는 중이었고, OAuth를 통해 받은 kakaoId를 회원 엔티티안에 기본 타입으로 선언하였더니, 다음과 같은 에러가 떴다. kakaoId는 일반 회원가입이 아닌, 카카오톡 회원가입을 한 회원에서만 가질 수 있는, 즉, nullable한 값이기 때문에, primitive type에 null이 할당되어 에러를 발생시킨 것이었다. 

 

 

[이펙티브 자바] item 61 - 박싱된 기본 타입보다는 기본 타입을 사용하라

 

https://siyoon210.tistory.com/139

 

Java에서 원시타입 vs 참조타입 어떤 걸 사용해야 할까?

[개요] 자바에서 숫자를 다루기 위한 타입들은 크게 두 가지로 분류할 수 있습니다. 하나는 '원시 타입(primitive type)'이고, 또 다른 하나는 참조 타입(reference type)'입니다. 원시 타입은 (int, double, boo

siyoon210.tistory.com

https://syundev.tistory.com/290

 

Wrapper클래스 박싱과 언박싱

0. Intro 자바에서는 기본형 타입을 Wrapping한 Wrapper클래스를 제공한다. Wrapper클래스에 대해서 간단히 알아보고 Wrapper클래스의 문제점 Wrapper클래스의 성능을 조사한 글이다. 글의 아래 순서로 구성

syundev.tistory.com