1256 단어
6 분
Long 객체 동일성 비교 이슈 및 캐싱 범위로 인한 버그

1. 문제 상황#

버디야 서비스를 운영하던 중 서비스 초반에는 자신의 정보가 올바르게 조회 되다가 어느순간 새로 만들어진 계정부터는 자신의 정보를 제대로 불러오지 못하는 현상이 발생하였다.

image

2. 해결 과정#

문제 분석#

로컬에서 테스트할 때는 정상적으로 동작하였기에 프론트 측에서 캐싱이 잘못되어 다른 사람의 정보를 불러온다고 생각했다. 하지만 프론트 측에서 원인을 발견할 수 없었고 서버에서 조회 로직을 하나씩 뜯어보며 원인을 분석하기로 하였다.

image

서비스 로직을 보면 다음과 같다.

  • 조회하려는 id가 자신의 id와 같다면 → 5가지 정보 반환
  • 다르면 → 중요한 개인정보를 제외한 2가지 정보 반환

서비스 초기에는 자신의 정보를 제대로 불러왔기에 이 로직에 문제가 있을 거라고 생각을 못했다..

if (studentInfo.id() != userId) { // 내 id가 아니면 2가지 정보
        return UserResponse.fromOtherUserInfo(student, matchingProfile);
}

바로 이 부분 != 비교이다. 해당 로직에서는 Long 객체를 != 으로 비교하고 있는데 객체를 동일 비교한다, 객체를 동일 비교한다.. 생각해보니 잘못됐다.

동일성 vs 동등성#

  • 동일성 : 객체가 같은 주소를 가리키는지 비교
  • 동등성 : 객체의 내부 값이 같은지 비교

객체는 동등 비교해야 하는데 동일 비교를 하여 문제가 발생한 것이다. 내가 원하는 것은 Long 주소 값이 같을 때 비교를 하고 싶은 것이 아니라 Long 값이 같을 때 분기 처리이기 때문이다.

Question?

근데 왜 서비스 초반에는 == 으로 동일성 비교해도 제대로 동작하고, 이후부터는 제대로 동작하지 않았을까?

사실 초반에 오류가 발생하지 않아 원인 파악이 힘들었던 이유가 이 부분이다. 결론부터 말하면 Java에서는 Integer, Long과 같은 객체들은 성능 최적화를 위해 -128 ~ 127까지 값을 캐싱해두기 때문에 같은 주소를 참조하지만, 해당 범위를 벗어나면 매번 새 객체가 만들어져 주소가 다르기 때문이다. 사용자 id를 1L부터 차례로 할당하므로 1 ~ 127명의 사용자는 자신의 정보를 제대로 불러와 문제를 발견할 수 없었지만, 128번째 사용자부터 자신의 정보를 제대로 불러오지 않아 서비스 중간에 발생한 것이었다.

image

Long.class의 코드로 들어가보면 LongCache라는 내부 클래스가 있다. LongCache는 -128 ~ 127 범위의 Long 객체를 static 배열에 미리 만들어 놓고, 필요하면 JVM 공유 메모리(CDS)에서 재사용하도록 설계한 코드이다.

Question?

여기서 캐시되면 왜 같은 주소값이 될까? 의문이 들어 조금 더 찾아봤다.

Long.class 안에 valueOf라는 메서드를 살펴보자.

public static Long valueOf(long l) {
        int offset = true;
        return l >= -128L && l <= 127L ? Long.LongCache.cache[(int)l + 128] : new Long(l);
}

-128 ~ 127 사이에 있는 값은 위에서 봤던 LongCache 배열에 미리 캐싱된 Long 객체를 꺼내서 반환하므로 같은 주소를 가리키고, 이외 범위는 new Long으로 새로운 Long 객체를 만들어서 반환한다.

image 메모리의 구조를 그리면 다음과 같다. image

실제 메모리 주소는 아니지만 객체마다 동일성을 식별하는 내부 값인 identityHashCode를 출력해보면 캐싱 범위에 포함되는 2L은 서로 동일하고, 캐싱 범위에 포함되지 않는 130L은 다른 것을 확인할 수 있다.

로직 수정#

if (!studentInfo.id().equals(userId)) {
       return UserResponse.fromOtherUserInfo(student, matchingProfile);
}

처음에 != 으로 동일 비교했던 로직을 equals 동등 비교를 사용하여 다음과 같이 수정하였고, 128번째 이후 사용자도 제대로 자신의 정보를 불러올 수 있었다.

깨달은 점#

위 과정들을 다 이해하고 나서야 왜 서비스 초기에는 정상 작동하다가 중간에 문제가 발생했는지 궁금증이 풀리게 되었고 이론으로만 알았던 동등 비교와 동일 비교의 중요성에 대해 몸소 느낄 수 있었다. 또한 다양한 경계값에서의 테스트 코드 작성의 중요함을 알게 되었고 이번 일을 계기로 동일성, 동등성 절대 까먹지 않을 것 같다..

Long 객체 동일성 비교 이슈 및 캐싱 범위로 인한 버그
저자
Joonyoung Hwang
게시일
2025-06-08