포스트

람다에서 JPA 엔티티를 쓰다 컴파일 에러가 난 진짜 이유

람다에서 JPA 엔티티를 쓰다 컴파일 에러가 난 진짜 이유

문제 상황

동시성 테스트를 작성하던 중, 람다에서 다음과 같은 컴파일 에러를 만났다.

Local variable must be final or effectively final

처음에는 람다 문법이나 멀티 스레드 문제를 의심했지만,
실제 원인은 테스트 코드에서 자주 쓰는 엔티티 저장 패턴에 있었다.


원인이 되는 코드

문제의 핵심은 아래 코드다.

1
2
3
4
5
Concert concert = Concert.builder()
        .title("test_title")
        .build();

concert = concertRepository.save(concert);

concert는 여기서 두 번 할당된다.

1.builder().build() 2.repository.save(concert)

그리고 이 변수를 람다 안에서 사용하고 있었다.

1
2
3
4
5
6
7
8
Callable<MakeReservationResult> task = () -> {
    User concurrentUser = createUser();
    return reserveSeat(
        concurrentUser.getId(),
        concert.getId(),
        seat.getId()
    );
};

💥 이 시점에서 컴파일 에러 발생


왜 에러가 나는가?

람다에서 캡처하는 지역 변수는 반드시

  • final 이거나
  • effectively final

이어야 한다.

하지만 concert는 재할당이 발생했기 때문에 effectively final 조건을 만족하지 못한다.

👉 람다 자체는 문제가 없고 👉 람다가 캡처한 변수의 상태가 문제였다.


자주 헷갈리는 포인트

아래 코드는 문제가 없다.

1
Seat seat = createSeatWithConcert(...);
  • 재할당 없음
  • effectively final 유지
  • 람다에서 사용 가능

❌ 문제는 오직 concert였다.


해결 방법 1️⃣ (가장 추천)

ID만 분리해서 람다에 전달

1
2
3
4
5
6
7
8
9
10
11
Long concertId = concert.getId();
Long seatId = seat.getId();

Callable<MakeReservationResult> task = () -> {
    User concurrentUser = createUser();
    return reserveSeat(
        concurrentUser.getId(),
        concertId,
        seatId
    );
};

이 방식의 장점

  • ✔ 람다는 값만 캡처
  • ✔ 엔티티 참조 제거
  • ✔ 동시성 테스트에서 더 안전
  • ✔ 실무에서 가장 많이 쓰는 패턴

해결 방법 2️⃣ (선언을 한 번만)

1
2
3
Concert concert = concertRepository.save(
    Concert.builder().title("test_title").build()
);

이렇게 하면 재할당이 사라지므로 람다에서도 바로 사용 가능하다.

1
concert.getId()

비추천하는 방법

1
final Concert concert = ...
  • 동작은 하지만
  • 테스트 가독성 저하
  • “왜 final이지?”라는 의문만 남음

이 패턴이 동시성 테스트에서 자주 나오는 이유

  • 테스트 준비 단계에서
    • 엔티티 생성
    • save() 후 재할당
  • 이후
    • 람다 / 스레드에서 엔티티 참조 👉 이 흐름이 거의 고정 패턴이다.

그래서 ID만 분리하는 습관이 가장 안전하다.


정리

  • concert = concertRepository.save(concert) 👉 코드 자체는 전혀 문제없다

  • 문제의 본질은 👉 재할당된 변수를 람다에서 캡처한 것

  • Spring/JPA 기반 동시성 테스트에서는 👉 엔티티 대신 ID를 final 값으로 분리하는 것이 정석


한 줄 요약

람다에서 쓰는 변수는 한 번만 할당돼야 한다. JPA 엔티티는 재할당되기 쉬우니 동시성 테스트에서는 ID만 캡처하자.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.