|

자바 equals와 == 차이: 문자열 비교에서 왜 자꾸 틀릴까

자바 equals와 == 차이: 문자열 비교에서 왜 자꾸 틀릴까
primitive, reference, String interning, null-safe 비교 기준을 한 번에 정리한다

자바 equals와 == 차이는 문자열 비교에서 특히 자주 틀리는 문법입니다. 어떤 때는 ==가 맞는 것처럼 보이고, 어떤 때는 틀리기 때문에 입문자도 실무 초급도 반복해서 헷갈립니다.

많은 설명이 이 주제를 ==는 주소 비교, equals()는 값 비교라고 요약합니다. 방향은 맞지만, 그대로 외우면 다시 헷갈리기 쉽습니다. 자바에서 ==는 항상 같은 방식으로만 해석되지 않고, equals()도 모든 클래스에서 자동으로 같은 의미를 가지는 메서드는 아니기 때문입니다.

이번 글에서는 자바 ==와 equals 차이를 primitive, reference type, String, null 안전성 기준으로 나눠 정리하겠습니다. 즉, 이 글의 목표는 문장을 외우는 것이 아니라 지금 내가 무엇을 비교하려는지 먼저 판단하는 기준을 만드는 것입니다.


자바 equals와 == 차이, 왜 이렇게 자주 헷갈릴까

이 주제가 특히 헷갈리는 이유는 문자열 비교에서 ==가 어떤 경우에는 맞는 것처럼 보이기 때문입니다. 아래 두 코드를 비교해보면 혼란이 왜 생기는지 바로 드러납니다.

String a = "hello";
String b = "hello";

System.out.println(a == b);       // true
System.out.println(a.equals(b));  // true

String x = new String("hello");
String y = new String("hello");

System.out.println(x == y);       // false
System.out.println(x.equals(y));  // true

문제는 문법 하나를 몰라서가 아니라, 값 비교, 같은 객체인지 확인하는 비교, 클래스가 정의한 동등성 비교를 한꺼번에 섞어서 생각하기 때문입니다.


먼저 결론

  • primitive 비교에서는 ==를 값 비교로 이해할 수 있다
  • reference type 비교에서는 ==를 같은 객체를 가리키는지 보는 비교로 이해하는 편이 안전하다
  • equals는 클래스가 정의한 동등성 기준에 따라 의미가 달라진다

Java의 Object.equals() 기본 구현은 두 참조가 같은 객체를 가리킬 때만 true가 되는 방식입니다. 하지만 많은 클래스는 equals()를 override하고, 대표적으로 String은 문자열 내용을 기준으로 equals()를 다시 정의합니다.

자바 ==와 equals를 primitive, reference type, String, null-safe 기준으로 비교한 표
자바 ==와 equals 빠른 비교표: primitive, reference type, String, null-safe 기준 요약

primitive와 reference type

primitive 비교

예를 들어 int 비교는 값 자체를 비교한다고 이해하면 됩니다. 1010은 같으므로 true가 나옵니다. double, char, boolean 같은 primitive도 기본적으로 같은 흐름으로 보면 됩니다.

int x = 10;
int y = 10;

System.out.println(x == y);  // true

reference type 비교

문제는 reference type입니다. Integer처럼 객체를 비교할 때는 같은 값을 담고 있어 보여도 서로 다른 객체일 수 있습니다. 그래서 reference type에서 ==는 보통 두 변수가 같은 객체를 가리키는가를 보는 기준으로 이해하는 편이 안전합니다.

Integer a = new Integer(100);
Integer b = new Integer(100);

System.out.println(a == b);  // false

많은 설명에서 이것을 주소 비교라고 부르지만, 실전 설명에서는 같은 객체를 가리키는지라는 표현이 더 직접적이고 오해도 적습니다.


String 비교

자바에서 ==와 equals가 가장 많이 헷갈리는 지점은 거의 항상 String입니다. 문자열은 자주 쓰고, 어떤 경우에는 ==가 우연히 true처럼 보이기 때문입니다.

문자열 리터럴 비교

String a = "java";
String b = "java";

System.out.println(a == b);       // true
System.out.println(a.equals(b));  // true

이 결과만 보면 문자열도 ==로 비교해도 되겠다고 착각하기 쉽습니다.

new String 비교

String a = new String("java");
String b = new String("java");

System.out.println(a == b);       // false
System.out.println(a.equals(b));  // true

여기서는 결과가 달라집니다. 왜냐하면 ab는 같은 내용을 가진 서로 다른 객체이기 때문입니다. 즉, 문자열 비교에서 ==는 내용이 같은지를 묻는 연산이 아니라, 두 변수가 같은 String 객체를 가리키는지를 보는 쪽에 가깝습니다. 반면 String.equals()는 문자열 내용을 비교합니다.

여기서 많이 듣는 말이 String interning입니다. 핵심만 말하면, 문자열 리터럴은 재사용되어 ==가 우연히 true처럼 보일 수 있습니다. 하지만 이건 문자열 내용 비교 규칙이 아니라, 자바가 문자열을 다루는 방식 때문에 생기는 예외처럼 보이는 장면에 가깝습니다.

실전 기준

실전에서는 이 기준 하나를 먼저 잡는 것이 좋습니다. 문자열 내용 비교는 ==에 기대지 말고 equals 기준으로 생각한다. 문자열 리터럴 비교에서 ==가 우연히 true가 나오는 경우가 있어도, 그 결과를 일반 규칙처럼 믿기 시작하면 언젠가 반드시 실수합니다.


equals와 Objects.equals 사용 시 주의점

null 가능성

String name = null;

System.out.println(name.equals("java"));  // NullPointerException

namenull이면 메서드를 호출할 수 없기 때문에 NullPointerException이 납니다. 그래서 실전에서는 아래처럼 상수 문자열 쪽에서 equals()를 호출하는 방식이 더 안전합니다.

String name = null;

System.out.println("java".equals(name));  // false

클래스마다 다른 의미

또 하나 중요한 점은 equals()가 항상 내용 비교라는 뜻은 아니라는 것입니다. Object.equals() 기본 구현은 같은 객체인지 보는 방식이고, 각 클래스는 필요에 따라 equals()를 override할 수 있습니다. 즉, equals()는 마법 같은 값 비교 연산자가 아니라 그 클래스가 정의한 동등성 기준입니다.


실무 기준

  • primitive 비교는 ==
  • 같은 객체인지 확인할 때도 ==
  • 문자열 내용 비교는 equals
  • null 비교는 == null
  • null 가능성이 있는 내용 비교는 Objects.equals 또는 상수 문자열 기준 equals 고려
  • 객체 비교는 그 클래스가 정의한 동등성 기준을 먼저 확인

즉, 자바 ==와 equals 차이는 문법 차이라기보다 지금 무엇을 비교하려는지의 차이로 이해하는 편이 정확합니다.


자주 하는 실수 4가지

  1. 문자열 내용을 ==로 비교하는 실수
  2. equals는 항상 값 비교라고 믿는 실수
  3. null 가능성을 빼먹고 equals()를 호출하는 실수
  4. 우연히 맞은 결과를 일반 규칙처럼 믿는 실수

한 번에 기억하는 기준

자바 ==와 equals 차이는 문법 차이가 아니라, 지금 무엇을 비교하려는지의 차이입니다. primitive 값인지, 같은 객체인지, 문자열 내용인지, 클래스가 정의한 동등성인지 먼저 묻는 습관이 생기면 비교 실수가 크게 줄어듭니다.


마무리

자바 ==와 equals 차이는 단순 문법 문제처럼 보이지만, 실제로는 자바가 값을 다루는 방식과 객체를 다루는 방식을 구분하는 기본 주제에 가깝습니다. 그래서 이 주제를 정확히 이해하면 문자열 비교 실수만 줄어드는 것이 아니라, reference type을 어떻게 봐야 하는지와 클래스가 정의한 동등성이 무엇인지까지 같이 정리됩니다.

관련해서 코틀린 쪽의 모델링 감각을 같이 보고 싶다면 코틀린 data class 완전 정리, data class와 sealed class: 모델링을 더 명확하게 만드는 법도 함께 읽어보면 좋습니다. 공식 기준은 Object.equals 문서, String 문서, JLS equality operators를 참고하면 됩니다.

함께보면 좋은 글