[항해 취업코스] 개발자 취준 기록 4일차-지연로딩

Godtaek·2024년 3월 10일
0

Spring

목록 보기
8/9
post-thumbnail

1. 지연 로딩?

지연 로딩(Lazy Loading) : 필요한 시점에 연관된 데이터를 로딩하는 방식

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "book_id")
    private Book book;

어노테이션을 통해서 FetchType을 지정해줄 수 있다.

지연 로딩은 필요할 때, 데이터를 로딩함으로 메모리 사용을 최적화할 수 있고, 성능 최적화를 할 수 있다.

2. 코드로 보기

@Entity
@Data
public class SaleBooks {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "book_id")
    private Book book;

    @Column(name = "price")
    private Long price = 1000L;

    @Column(name = "stock")
    private Long stock = 1000L;

    public static SaleBooks of(Book book) {
        SaleBooks saleBooks = new SaleBooks();
        saleBooks.setBook(book);
        return saleBooks;
    }
}

간단하게 book과 1:N 관계를 가지는 책 상품 엔티티를 정의했다. 지연 로딩을 사용한다.

// SaleBookService
@Service
@RequiredArgsConstructor
public class SaleBookService {
    private final SaleBookRepository saleBookRepository;
    private final BookService bookService;

    @Transactional
    public SaleBooks createSaleBook(Long bookId) {
        Book book = bookService.getBook(bookId);
        SaleBooks saleBooks = SaleBooks.of(book);
        saleBookRepository.save(saleBooks);
        return saleBooks;
    }

    public SaleBooks getSaleBooks(Long saleBookId) {
        SaleBooks saleBooks = saleBookRepository.findById(saleBookId).orElseThrow(RuntimeException::new);
        System.out.println("--------------------------------");
        Book book = saleBooks.getBook();
        return saleBooks;
    }
}

saleBook 엔티티를 create하고 get하는 메서드를 작성했다.
getSaleBooks에서 지연로딩이 정상 작동하는지 확인해보자.

지연 로딩은 데이터를 필요할 때 가져오는 형식이라고 했다. getSaleBook 메서드에서 book을 호출할 때, book 데이터를 불러와야 한다.

결과 사진을 봤을 때, SaleBook select를 한 번, book select를 한 번한 것을 확인할 수 있다. 그런데 에러가 뜬다. 왜일까?

에러?

일단 에러 코드를 보자

2024-03-10T22:01:56.861+09:00 ERROR 19580 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor]] with root cause

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: com.example.jpatest.model.SaleBooks["book"]->com.example.jpatest.model.Book$HibernateProxy$xY9Vletn["hibernateLazyInitializer"])

대략 해석하자면 proxy 객체를 직렬화할 수 없다는 뜻이다.

해당 에러가 나는 이유는 Lazy Loading을 할 때, 연관관계가 있는 엔티티 데이터를 프록시 객체로 가져오기 때문이다. 쉽게 말해서 엔티티 프록시 객체가 Book이라는 연관관계 엔티티를 있는 척 한다.

사실 엔티티를 그대로 반환하는 것이 좋은 방법은 아니기 때문에 Dto를 만들어 해결하는 것이 좋다. Dto를 직렬화할 때도 해당 에러를 볼 수 있는데, 필요한 데이터라면 Dto에서 초기화를 통해, 필요없는 데이터라면 @JsonIgnore를 사용하여 해결할 수 있다.

프록시?

프록시는 실제 클래스를 상속받아서 만들어지는 껍데기 데이터다.
EntityManager.getRefernce()를 통해 조회가 가능하다. 메서드에서 볼 수 있듯이, 실제 객체의 참조를 보관한다.
프록시 객체는 처음 사용할 때 초기화하고, 실제 엔티티 객체로 변하진 않는다. 그래서 위와 같은 에러가 터지는 것.
그리고 준영속 상태에서 프록시 객체를 초기화하면 에러가 터진다.

예상되는 문제

그런데 Lazy Loading을 사용하면 N+1문제가 발생할 거라 쉽게 예상할 수 있다.
EAGER 즉시 로딩을 사용한다면 바로 모든 데이터를 가져와 문제가 없지만, Lazy Loading을 사용한다면 필요할 때마다 select를 하기 때문에 최초 한 번 쿼리 + 필요한 데이터 N번 쿼리를 호출한다.

// EAGER
Hibernate: select sb1_0.id,b1_0.id,b1_0.content,b1_0.created_at,b1_0.name,b1_0.updated_at,sb1_0.price,sb1_0.stock from sale_books sb1_0 left join book b1_0 on b1_0.id=sb1_0.book_id where sb1_0.id=?

EAGER에서 LEFT Join을 활용하여 데이터를 가져오지만, LAZY에서는 아까봤듯이, select문으로 연관 데이터를 조회한다. N+1문제가 터질 수 있다. 그렇다고 즉시 로딩에서 N+1이 터지지 않는 것은 아니다.

그래서 다음 주제는 아마 N+1이 되지 않을까 싶다.

3. 마치며

다음은 N+1이다!
사실 실무에서는 지연 로딩을 대부분 사용하라고 한다. (그렇다더라....)

연관관계가 복잡할수록 지연 로딩은 필요없이 많은 데이터를 가져오기 때문에 성능 문제와 역시 N+1문제가 존재한다.

다만, 특정 데이터가 특정 연관 데이터와 반드시 함께 사용한다면 즉시 로딩을 고려해볼만 하다.

profile
성장하는 개발자가 되겠습니다

0개의 댓글