LazyInitializationException이란 객체를 로딩할 때 지연로딩이 적용된 엔티티 또는 컬렉션을 초기화하지 못했을 때 발생하는 런타임 예외입니다.
지연 로딩?
지연로딩이란 자신과 연관된 엔티티를 실제로 사용할 때 연관된 엔티티를 조회하는 것을 말합니다.
Team과 Member로 예시를 들면 1:N 관계에서 매번 Team을 불러올 때마다 Member를 가져올 필요가 없다면 이때 지연 로딩을 사용합니다.
배달의 민족 상점을 예시로 들어보겠습니다.
배달의 민족에서 음식을 시키기 위해 카테고리별 상점들을 보고 있을 때 저희는 상점의 정보만 필요합니다. 리뷰를 보고 싶으면 상점을 클릭해서 그 상점의 리뷰를 보죠. 상점과 리뷰는 1:N관계인데 상점을 호출할 때 상점에 달린 리뷰를 가져올 필요가 없는 경우에 하는 것이 지연 로딩입니다.
지연 로딩을 사용하면 Member에서 Team에 관한 정보를 실제로 필요한 시점에 DB에서 가져오게 됩니다.
Member를 조회할 때, Team 객체에는 프록시 객체가 들어가며 실제로 Team 정보가 필요한 순간에 초기화됩니다.
Proxy객체란?
실제 객체의 대리자 역할을 합니다. Member 객체가 호출될 때 자연스레 Team 객체도 호출되는데 이때 Team 객체에는 Proxy객체가 들어가고, Team 객체가 필요한 시점에 실제 DB에서 데이터를 가져와 초기화를 합니다.
문제 상황
public class Member extends BaseTimeEntity implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "memberId")
private Long id;
@Enumerated(EnumType.STRING)
private OAuthProvider oauthPlatform;
private String name;
private String profileImg;
private String nickname;
...
@ElementCollection
@CollectionTable(name = "aDateColorTypes", joinColumns = @JoinColumn(name = "member_id"))
private Map<String, String> adateColorTypes = new HashMap<>();
}
Member와 adateColorTypes는 1:N의 관계고, 현재 지연로딩이 적용되어 있습니다.
그러므로 Member를 저장할 때 adateColorTypes는 Proxy객체로 초기화되어 있습니다.
public void saveAdateColorAndName(AdateColorNameRequestDto adateColorNameRequestDto, Member member) {
Map<String, String> adateColorTypes = member.getAdateColorTypes();
adateColorTypes.put(adateColorNameRequestDto.getColor(), adateColorNameRequestDto.getName());
memberRepository.save(member);
}
문제가 발생한 부분의 코드입니다.
2번째 줄에서 adateColorTypes에 member의 adateColorTypes값을 할당해 줄 때 해당 Proxy객체를 초기화하지 않고 사용하려고 하기 때문에 에러가 발생합니다.
지연로딩을 설명할 때 Proxy객체는 값이 필요한 시점에 실제 DB에서 값을 불러온다며?
Proxy객체는 영속성 컨텍스트가 종료되지 않은 상태일 때 DB에서 값을 가져와 초기화할 수 있습니다.
위의 코드에서는 member 를 저장한 뒤, member에서 adateColorTypes을 가져오는데 이 과정에서 트랜젝션이 커밋되거나 롤백되면 영속성 컨텍스트가 종료됩니다. member가 저장되는 것은 트랜젝션이 커밋되는 것입니다.
그러므로 영속성 컨텍스트가 종료 됐으므로 Proxy 객체는 DB에서 값을 가져올 수 없습니다.
해결 방법
즉시로딩?
즉시로딩은 지연로딩과 다르게 Member를 조회할 때 Team의 정보도 가져오는 방식이다.
이는 성능상 문제가 생길 수 있기 때문에 고려하지 않았다.
Transactional 내부에서 연관관계 미리 조회
영속상태에서 미리 연관관계 정보를 가져오면 LazyInitializationException에러가 발생하지 않을 것 같아!!
@Transactional
public Member getMemberWithAdateColorTypes(Long memberId) {
Member member = memberRepository.findById(memberId).orElseThrow(
() -> new UnknownMemberException());
member.getAdateColorTypes().forEach((key, value) -> {});
return member;
}
코드에 대해 간단히 설명하면, memberId를 기준으로 DB에서 member 객체를 가져옵니다.
그런 뒤, member 객체에서 adateColorTypes를 호출합니다.
이러면 member 객체에 adateColorTypes 정보가 반영되므로 이 메서드에서 반환하는 member 객체를 사용하면 문제없이 호출할 수 있습니다.
해결 완료
Controller
@PostMapping("/member/regist/color")
public ResponseEntity<String> registAdateColorAndName(
@RequestBody @Validated AdateColorNameRequestDto adateColorNameRequestDto,
@AuthenticationPrincipal MemberPrincipal memberPrincipal) {
Member member = memberService.getMemberWithAdateColorTypes(memberPrincipal.getMember().getId());
memberService.saveAdateColorAndName(adateColorNameRequestDto, member);
return ResponseEntity.ok("color&name 등록 완료");
}
Service
@Transactional
public void saveAdateColorAndName(AdateColorNameRequestDto adateColorNameRequestDto, Member member) {
Map<String, String> adateColorTypes = member.getAdateColorTypes();
adateColorTypes.put(adateColorNameRequestDto.getColor(), adateColorNameRequestDto.getName());
memberRepository.save(member);
}
@Transactional
public Member getMemberWithAdateColorTypes(Long memberId) {
Member member = memberRepository.findById(memberId).orElseThrow(
() -> new UnknownMemberException());
member.getAdateColorTypes().forEach((key, value) -> {});
return member;
}