Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

Kuma's Curious Paradise

테스트 코드 4 - LazyInitializationException 문제와 @EntityGraph 본문

스프링

테스트 코드 4 - LazyInitializationException 문제와 @EntityGraph

쿠마냥 2024. 7. 8. 14:10

[문제]

개발 도중, item을 생성하는 로직에서 LazyInitializationException이 발생하였다.

문제가 발생한 코드는 아래의 createItem()으로, Brand 엔티티와 연관된 Item 엔티티 목록을 가져오던 도중 에러가 터졌다.

 

[ItemService.java]
public class ItemService {
    private final BrandManager brandManager;

    public void createItem(Partners partners, ItemCreateRequest request) {
        Item newItem = request.toItemEntity();
        Brand brand = partners.getBrand();

        brandManager.updateBrandItem(brand, newItem);
    }
}
[에러 메시지]
failed to lazily initialize a collection of role: com.example.domain.Brand.items: could not initialize proxy - no Session

 

[고려할 사항]

- Brand ↔ Item : 일대다 관계 / FetchType.Lazy. / CascadeType.MERGE

- request.toItemEntity() 메서드는 다음의 일을 수행한다

  • Item 생성에 관련한 DTO 객체의 파라메터를 Item 객체와 ItemOption 객체로 만들어 ItemOption 객체를 Item객체에 리스트로 넣어 완전한 Item 객체로 만든다.

- brandManager.updateBrandItem() 메서드는 다음의 일을 수행한다.

  • item과 brand 사이에 연관관계를 맺어준다.
  • brandRepository.save()로 brand의 내용을 갱신하여 저장한다. cascade type.merge이기 때문에 jpa는 item 새로 생성된 item과 brand의 연관관계 업데이트를 자동으로 수행한다.
[BrandManager.java]
public void updateBrandItem(Brand brand, Item item){
    item.updateBrand(brand);
    brandRepository.save(brand);
}

[Item.java]
public void updateBrand(Brand brand) {
    Assert.notNull(brand, "brand는 null일 수 없습니다.");
    this.brand = brand;
    brand.addItem(this);
}

[Brand.java]
public void addItem(Item item) {
    Assert.notNull(item, "item은 null일 수 없습니다.");
    items.add(item);
}

 

[해결 방안]

public class ItemService {

    private final BrandManager brandManager;
    private final ItemManager itemManager;

    public void createItem(Partners partners, ItemCreateRequest request) {
        Brand brand = brandManager.getBrandWithItems(partners);
        Item newItem = request.toItemEntity();

        request.itemOptions().forEach(optionRequest -> {
            ItemOption itemOption = optionRequest.toItemOptionEntity();
            newItem.addItemOption(itemOption);
        });

        brand.addItem(newItem);
        itemManager.createItem(newItem);
    }
}

 

- 기존에 CascadeType.Merge만 존재했는데, 해당 기능은 없던 레코드를 생성하는 데 관여하는게 아니라 따로 save를 수행하지 않으면 정상적인 작동이 되지 않는다.

- 따라서, CascadeType.Persist를 추가했다.

- 동시에, ItemOption을 만들어 연관관계 편의 메서드를 통해 각각의 객체에 정보를 저장하고, CascadeType.Persist를 이용하여 저장한다.

- ItemCreateRequest의 toItemEntity() 메서드가 너무 많은 일을 한다고 생각하여, 해당 메서드를 Item 객체만 만들고, request 객체의 itemOptioCreateRequest를 가져와 toItemOptionEntity()를 수행하고 Service 로직에서 두 개의 객체의 연관관계를 생성한다.

 

[또 다른 문제]

- Partners ↔ Brand : 일대다 관계 / FetchType.Lazy. / CascadeType.MERGE

- Brand ↔ Item과 같은 관계를 가지고 있다.

[ItemService.java]
public class ItemService {

    private final BrandManager brandManager;

    public void createItem(Partners partners, ItemCreateRequest request) {
        Item newItem = request.toItemEntity();
        Brand brand = partners.getBrand();

        brandManager.updateBrandItem(brand, newItem);
    }
}

그렇다면, 왜 애초에 Brand brand = partners.getBrand()에서 LazyInitializationException이 터지지 않은걸까? 다시 말해, Partners에 걸려 있는 Brand는 LazyLoading이지만 잘 가져와지고, Brand에 걸려 있는 Item은 LazyInitializationException이 발생하는 이유가 무엇일까?

 

[해결 과정]

 

사용자가 로그인을 한다 → SecurityContext에서 Partners 객체를 들고온다 → Lazy loading인 Brand는 함께 로딩되지만, 그 안에 있는 items들은 로딩되지 않는 것을 볼 수 있다.

 

원인은 PartnersRepository에 있었다. 이곳의 findByEmail()에는 @EntityGraph가 달려 있어서, SecurityContext에서 Partners 객체를 들고올 때 Brand가 조인되어 함께 로딩된 것.

 

 

이렇게 간단한 문제 해결이 오래 걸린 이유는 다음과 같다.

1. Brand와 Item 사이의 문제라고 생각하여 Partners까지 범위를 확장하여 생각지 못했다.

2. Security를 구성할 당시 이 부분을 정확히 기록해두지 않았다. ㅠ.ㅠ

 

 

@EntityGraph를 주석처리한 뒤 디버그를 돌려보았다.

- Brand는 null이 되었지만, Partners의 getIdentifierMethod()에는 Brand.getId()가 있는 것을 볼 수 있다.

- Brand가 Partners와 함께 로그인 시 PersistentContext에 등록되어 영속 상태가 되었다가, 필터의 로직이 끝나고 준영속 상태가 되어 Id 필드만 남아있는것을 확인할 수 있다.

 

 

따라서 brand id를 가지고 brand를 위 사진처럼 영속화하는 것이 가능하다.

 

[보안해야 할 점]

- 문제가 존재를 알아채지 못한 이유는 JPA 슬라이스 테스트를 진행하지 않음에 있었던 것 같다. 이러한 오류를 잡아내기 위해 테스트 코드를 만드는데, 테스트 코드가 제대로 기능하지 못해서 마음이 아프기도 하다. 지금도 테스트 코드 작성에 많은 공수가 드는 상황. JPA 테스트까지 모두 진행해야 할까? 앞으로 테스트 코드를 어떻게 진행해야 할지 고민이 많아졌다.

 

- CascadeType에 대한 이해가 부족했다. 특히 cascade type.merge와 persist가 어떻게 다른지에 대해. 이 부분은 뒤에서 좀 더 자세히 다룬다.