문제 상황
특정 엔티티를 다른 엔티티의 외래 키(FK)로 저장할 때, 기존에는 findById를 사용하여 해당 엔티티를 DB에서 완전히 조회한 후 세팅하는 방식을 사용했습니다.
@Transactional
public void addFavorite(Long announceId, Long memberId) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new EntityNotFoundException());
Announce announce = announceRepository.findById(announceId)
.orElseThrow(() -> new EntityNotFoundException());
MemberFavorite memberFavorite = new MemberFavorite(member, announce);
favoriteRepository.save(memberFavorite);
}
MemberFavorite을 저장하기 위해 DB 입장에서 실제로 필요한 값은 member_id와 announce_id, 즉 숫자 두 개뿐입니다. 그런데 findById는 해당 엔티티의 모든 칼럼을 SELECT 해서 가져옵니다.
INSERT 한 번을 위해 SELECT가 2번 먼저 실행되는 구조였습니다.
원인 분석
findById는 메서드는 호출하는 즉시 DB에 SELECT 쿼리를 날리고 실제 엔티티 객체를 반환합니다.
엔티티의 필드 데이터를 읽어야 하는 상황이라면 당연히 필요한 동작이지만, 단순히 연관 관계를 맺기 위해 ID 값만 필요한 경우에는 명백히 불필요한 조회입니다.
해결 방법 — getReferenceById
getReferenceById는 DB를 실제로 조회하지 않고, ID 값만 품고 있는 프록시(Proxy) 객체를 즉시 반환합니다.
프락시의 실제 데이터는 해당 필드에 접근하는 시점에 비로소 조회됩니다.
FK 설정처럼 ID 값만 사용하는 경우에는 끝까지 DB 조회가 발생하지 않습니다.
@Transactional
public void addFavorite(Long announceId, Long memberId) {
Member member = memberRepository.getReferenceById(memberId);
Announce announce = announceRepository.getReferenceById(announceId);
MemberFavorite memberFavorite = new MemberFavorite();
memberFavorite.setMember(member);
memberFavorite.setAnnounce(announce);
favoriteRepository.save(memberFavorite);
}
이 방식으로 변경하면 addFavorite 호출 시 SELECT 쿼리 없이 INSERT 한 번만 실행됩니다.
findById vs getReferenceById 비교
| findById | getReferenceById | |
| 조회 시점 | 메서드 호출 즉시 | 실제 데이터가 필요할 때까지 지연 |
| 반환 타입 | 실제 엔티티 객체 (Optional) | 프록시 객체 |
| SELECT 쿼리 | 즉시 발생 | ID만 사용할 경우 발생 안 함 |
| 적합한 상황 | 엔티티의 필드 데이터를 읽어야 할 때 | 연관 관계(FK)만 설정하고 싶을 때 |
⚠️ 주의사항
1. 예외 발생 시점이 다릅니다.
findById는 ID가 존재하지 않으면 메서드 호출 시점에 바로 예외를 던질 수 있지만, getReferenceById는 프락시의 실제 데이터에 접근하는 시점에 EntityNotFoundException이 발생합니다.
단순 저장 용도로 쓰는 경우에는 문제가 없지만, 이후에 프록시 객체의 필드를 읽는 코드가 있다면 예외 발생 위치가 예상과 달라질 수 있습니다.
2. 트랜잭션 범위 내에서만 사용해야 합니다.
프록시프락시 객체는 트랜잭션 범위 내에서만 초기화가 가능합니다. 트랜잭션 밖에서 프락시의 데이터를 읽으려 하면 LazyInitializationException이 발생합니다. 하지만 위 코드처럼 저장 용도로만 사용하는 경우에는 해당하지 않습니다.
정리
FK 설정만을 목적으로 엔티티가 필요한 상황이라면, DB에서 전체 데이터를 가져오는 findById 대신 프락시 객체를 반환하는 getReferenceById를 사용하는 것이 올바른 선택입니다. JPA의 지연 로딩 메커니즘을 적극적으로 활용하는 방식이기도 합니다.
엔티티의 필드 데이터가 실제로 필요한 경우에는 findById를, 단순히 연관 관계를 맺는 경우에는 getReferenceById를 구분해서 사용하는 습관을 들이면 불필요한 쿼리를 줄이는 데 큰 도움이 됩니다.
'❄️ 트러블 슈팅' 카테고리의 다른 글
| [트러블슈팅] Redis 호출 5번을 Pipeline으로 1번에 끝내기 (0) | 2026.03.23 |
|---|---|
| [트러블 슈팅] SOS 상세 조회 API의 Row Explosion 문제 해결 (0) | 2026.03.22 |
| [트러블 슈팅]공고 상세 조회 API의 불필요한 DB 쿼리 줄이기 (0) | 2026.03.22 |
| [JWT 트러블슈팅] 매 요청마다 DB 조회가 발생하는 문제와 해결 방법 (0) | 2026.03.22 |
| [트러블슈팅] @OneToOne(mappedBy) LAZY 로딩이 동작하지 않는 이유와 해결 방법 (0) | 2026.03.22 |