포스트

람다에서 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는 여기서 두 번 할당된다. builder().build()로 한 번, 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 라이센스를 따릅니다.