본문 바로가기

CS

자바에서의 동일성과 동등성, 그리고 원시형과 객체형의 차이

객체를 다루는 자바 백엔드 개발자라면 반드시 이해하고 넘어가야 할 개념이 있습니다. 바로 동일성과 동등성 그리고 원시형과 객체형의 차이입니다. 이 글에서는 해당 개념들을 예제 코드와 함께 명확하게 정리해 보겠습니다.

 

 

 

 

 

자바에서 객체를 비교할 때, 우리는 두 가지 기준으로 비교할 수 있습니다.

동일성 (Identity) 두 객체가 메모리 상에서 같은 객체인지 확인 [주소 값 비교] == 연산자
동등성 (Equality) 두 객체가 논리적으로 같은 값을 가지는지 확인 equals() 메서드

 

 


 

동일성 (==)

 

동일성은 단순히 객체의 참조값이 같은지를 비교합니다. 즉, 두 객체가 같은 메모리 주소를 참조하는지를 보는 것입니다.

 

 

아래에서의 예시를 보게 되면 apple1과 apple2는 내용은 같지만 서로 다른 객체입니다. 반면 apple3는 apple1을 얕은 복사로서 참조하고 있기 때문에 동일한 객체입니다.

 

Apple apple1 = new Apple(100);
Apple apple2 = new Apple(100);
Apple apple3 = apple1; // 얕은 복사

System.out.println(apple1 == apple2); // false
System.out.println(apple1 == apple3); // true

 

 


 

동등성 (equals())

 

동등성은 객체의 실제 값(내용)이 같은지를 비교합니다. 따라서 equals() 메서드를 오버라이딩해서 객체의 필드를 비교하도록 구현해야 합니다.

 

public class Apple {
    private final int weight;

    public Apple(int weight) {
        this.weight = weight;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Apple apple = (Apple) o;
        return weight == apple.weight;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(weight);
    }
}

 

Apple apple1 = new Apple(100);
Apple apple2 = new Apple(100);

System.out.println(apple1.equals(apple2)); // true

 

 

Object 클래스의 기본 equals() 구현은 ==(동일성)으로 비교하기 때문에, 반드시 오버라이딩 해야 의미 있는 동등성 비교가 가능합니다.


문자열(String)은 왜 == 비교가 될까?

 

String str1 = "hello";
String str2 = "hello";
String str3 = new String("hello");

System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false
System.out.println(str1.equals(str3)); // true

 



"hello"는 문자열 상수 풀에 저장되므로 동일한 리터럴은 같은 참조를 사용합니다. 하지만 new 연산자를 사용할 경우에는 얘기가 달라지게 됩니다. new String("hello")는 힙에 새로운 객체를 생성하기 때문에 참조가 다릅니다. 즉, 문자열도 객체이기 때문에 항상 equals()를 사용한 동등성 비교를 해야 합니다.

 

 

이는 문자열 비교에서 예상치 못한 == 결과를 피하기 위함이며, 특히 외부 입력 값이나 new String()으로 생성된 객체와 비교할 때는 반드시 equals()를 사용해야 안정적이고 일관된 결과를 얻을 수 있습니다. 문자열은 자바에서 자주 쓰이는 객체이기 때문에, 이러한 String Pool의 동작 방식을 이해하는 것은 성능 최적화와 버그 방지 측면에서도 매우 중요합니다.

 


래퍼 클래스(Integer 등)의 동일성과 동등성

 

 

Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;

System.out.println(i1 == i2); // true (캐싱)
System.out.println(i3 == i4); // false (캐싱 범위 초과)
System.out.println(i3.equals(i4)); // true

 

자바는 -128 ~ 127 범위의 Integer 객체를 캐싱합니다. 그 범위 안에서는 == 비교도 가능하지만, 이건 내부 구현 디테일에 의존한 예외적인 경우이기 때문에 반드시 equals()로 비교하는 습관이 중요합니다.


원시형 vs 객체형

 

구분 원시형 (Primitive Type) 객체형 (Reference Type)

의미 값 자체를 저장하는 타입 객체의 주소(참조값)를 저장하는 타입
예시 int, boolean, double, char 등 Integer, Boolean, Double, String, 사용자 정의 클래스
저장 위치 스택(Stack) 또는 레지스터(Register)에 값 직접 저장 힙에 생성된 객체의 참조값을 저장
비교 방식 == : 값 비교 == : 참조(동일성) 비교
equals() : 내용(동등성) 비교
null 가능 여부 null 불가 null 허용
기본값 숫자형: 0 / boolean: false null
기타 특징 성능이 좋고, 오토박싱으로 객체형과 자동 변환됨 메모리를 더 사용하고, 동등성 비교 시 equals() 필요

원시형: 값 자체 비교

 

int a = 10;
int b = 10;

System.out.println(a == b); // true (값 자체 비교)

 

 

 

객체형: 참조값 비교 vs 값 비교

Integer a = 100;
Integer b = 100;

System.out.println(a == b);        // true (자바가 -128~127 캐싱)
System.out.println(a.equals(b));   // true (값 비교)

Integer c = 200;
Integer d = 200;

System.out.println(c == d);        // false (캐싱 범위 초과)
System.out.println(c.equals(d));   // true

 

 

즉 정리하자면 원시형은 값 자체를 저장해서 스택에 저장되고 null이 될 수 없지만 객체형은 힙에 생성된 객체의 주소를 참조하기 때문에 null이 가능하고 == 로는 동일성, equals()로는 동일성을 비교합니다.


마무리

== 연산자는 두 객체가 동일한 객체인지(동일성), 즉 메모리에서 같은 주소를 참조하고 있는지를 비교합니다. 반면 equals() 메서드는 두 객체의 내용이 같은지(동등성)를 비교하며, 대부분의 경우 의미 있는 비교를 위해 오버라이딩되어 사용됩니다.

 

만약 equals()를 재정의하지 않으면, 기본 구현은 ==과 동일하게 작동하여 동일성만 비교합니다. 문자열은 자바에서 문자열 상수 풀(String Pool)을 사용하기 때문에, 같은 리터럴 문자열은 동일한 참조를 가지며 == 연산 결과가 true가 되는 경우가 있습니다. 하지만 new String("hello")처럼 명시적으로 생성된 문자열은 새 객체로 취급되므로 참조가 다르고 == 비교 시 false가 됩니다. 따라서 문자열을 비교할 때는 항상 equals()를 사용하는 것이 안전합니다.

 

래퍼 클래스(Integer, Boolean 등)도 객체이므로 == 대신 equals()를 사용해야 합니다. 특히 자바는 -128에서 127 사이의 값에 대해서는 내부적으로 객체를 캐싱하기 때문에 ==으로 true가 나올 수 있지만, 이 또한 예외적인 최적화일 뿐이며 일반적으로는 equals()로 비교하는 것이 안정적입니다.

 

마지막으로 원시형은 값 자체를 스택에 저장하기 때문에 ==으로 값 비교가 되고, null이 될 수 없습니다. 반면 객체형은 힙에 있는 객체의 주소를 저장하며, ==은 동일성 비교, equals()는 동등성 비교에 사용되고 null도 허용됩니다. 이 두 가지 차이는 자바 객체 비교에서 가장 기본적이면서도 중요한 개념입니다.