[SpringBoot-JPA 활용편1] 자바 ORM 표준 JPA 프로그래밍 - 활용편2: 2. API 개발 고급 - 준비
실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 강의 - 인프런
스프링 부트와 JPA를 활용해서 API를 개발합니다. 그리고 JPA 극한의 성능 최적화 방법을 학습할 수 있습니다., 스프링 부트, 실무에서 잘 쓰고 싶다면? 복잡한 문제까지 해결하는 힘을 길러보세요
www.inflearn.com
해당 강의는 Inflearn에 등록된 김영한님의 Spring Boot 강의입니다.
API개발 고급에 대해서 특히 성능 문제에 대해서 이야기 하겠다. 그리고 기본적인 강의 실습 프로젝트를 위해 기본적인 세팅을 하는 시간이다.
Api 고급 개발 소개
성능 저하는 API 개발에서 흔히 마주치는 문제 중 하나이다. 특히, 데이터베이스를 사용하는 웹 애플리케이션에서 이 문제는 더욱 두드러진다. 대부분의 경우, 데이터를 등록하거나 수정할 때보다 데이터를 조회할 때 성능 저하가 발생하는 경향이 있다. 여기서 말하는 성능저하는 간단하게 말하면 쿼리가 많이 나가는 현상이다. 이런 성능 저하의 주요 원인 중 하나가 바로 "N+1 문제"라 불리는 현상이다.
N+1 문제란?
N+1 문제는 ORM(Object-Relational Mapping)을 사용할 때 자주 발생하는 문제로, 한 번의 쿼리로 연관된 객체를 불러올 때, 그 객체의 각 인스턴스에 대해 추가적인 쿼리가 발생하는 현상이다. 예를 들어, 게시판의 모든 게시글을 조회하는 상황에서, 게시글을 가져오는 하나의 쿼리가 실행된 후, 각 게시글에 연관된 댓글을 가져오기 위해 게시글 수만큼 추가 쿼리가 실행되는 상황이 발생할 수 있다. 결과적으로, 1개의 게시글을 조회한다면, 1(게시글 조회) + 10(각 게시글에 대한 댓글 조회) = 11개의 쿼리가 실행되는 것이다.
성능 저하의 영향
이 문제는 데이터베이스와 애플리케이션 사이의 네트워크 지연, 데이터베이스 부하 증가, 결국 사용자에게 지연된 응답 시간으로 나타나는 성능 저하를 초래한다. 특히 대규모 트래픽이 발생하는 서비스에서는 이 문제가 시스템 전체의 장애로 이어질 수 있다.
해결 방안
N+1 문제를 해결하기 위한 주요 전략 중 하나는 "페치 조인(Fetch Join)"을 사용하는 것이다. 페치 조인을 사용하면 연관된 객체를 처음 쿼리할 때 함께 로딩함으로써 추가적인 쿼리를 줄일 수 있다. JPA를 사용한다면, @EntityGraph 어노테이션 또는 JPQL의 JOIN FETCH 구문을 통해 이를 구현할 수 있다.
또 다른 방법은 "배치 사이즈(Batch Size)"를 설정하는 것이다. 이 방법은 연관된 객체를 로드할 때 설정된 사이즈만큼 여러 개의 객체를 한 번에 가져옴으로써 쿼리 수를 줄이는 전략이다. 이는 특히 하나의 트랜잭션 내에서 많은 양의 데이터를 처리할 때 유용하다.
마지막으로
DTO(Data Transfer Object)를 직접 조회하는 방식으로도 N+1 문제를 해결할 수 있다. 이 방법은 엔티티 대신에 필요한 데이터만을 포함하는 DTO를 직접 조회하여 성능을 최적화하는 전략이다. 특정 엔티티와 그 연관 관계를 함께 조회할 때 N+1 문제가 발생하는데, DTO를 직접 조회하면 이 문제를 자연스럽게 회피할 수 있다. 연관된 모든 필드를 로드하는 대신 필요한 데이터만을 명시적으로 조회하기 때문이다.
결론
성능 문제는 사용자 경험에 직접적인 영향을 미치기 때문에, API를 설계하고 개발할 때는 이러한 문제를 사전에 고려하고 적절한 최적화 기법을 적용하는 것이 중요하다. N+1 문제에 대한 이해와 해결 방안을 알고 있으면, 보다 효율적이고 성능이 우수한 애플리케이션을 만들 수 있다.
조회용 샘플 데이터 입력
프로젝트를 진행함에 있어 테스트를 위하여 데이터를 미리 삽입해놓고 시작하자.
두건의 회원. 즉, 두건의 주문, 4개의 상품
주문된 상품(주문이 두개= ORDER_ID가 2개)
코드
@Component
@RequiredArgsConstructor
public class InitDb {
private final InitService initService;
//@PostConstruct: 이 어노테이션은 해당 메소드가 클래스의 객체가 생성된 후에 자동으로 호출되어야 함을 나타낸다.
// 따라서 init() 메소드는 dbInit 클래스의 객체가 생성된 후에 자동으로 호출됨
@PostConstruct
public void init()
{
initService.dbInit1();
initService.dbInit2();
}
/**
* 아래 클래스는 데이터베이스 초기화 작업을 수행
*/
@Component
@Transactional
@RequiredArgsConstructor
static class InitService{
private final EntityManager em;
//user A, 주문 상품 2개, 주문1개
public void dbInit1()
{
//userA 초기화
Member member=createMember("userA","서울", "1","1111");
em.persist(member);
//상품 초기화(상품 등록)
Book book1= createBook("JPA1 BOOK",10000,100);
em.persist(book1);
Book book2= createBook("JPA2 BOOK",20000,100);
em.persist(book2);
//주문 상품 초기화
OrderItem orderItem1=OrderItem.createOrderItem(book1,10000,1); //1권 주문
em.persist(orderItem1);
OrderItem orderItem2=OrderItem.createOrderItem(book2,20000,2); //2권 주문
em.persist(orderItem2);
//주문 초기화(회원 주소로 배송)
Order order1 = Order.createOrder(member, createDelivery(member), orderItem1, orderItem2);
em.persist(order1);
}
//userB, 주문 상품 2개, 주문1개
public void dbInit2()
{
//userB 초기화
Member member=createMember("userB","광주", "2","2222");
em.persist(member);
//상품 초기화(상품 등록)
Book book1= createBook("SPRING1 BOOK",20000,200);
em.persist(book1);
Book book2= createBook("SPRING2 BOOK",40000,300);
em.persist(book2);
//주문 상품 초기화
OrderItem orderItem1=OrderItem.createOrderItem(book1,20000,3); //3권 주문
em.persist(orderItem1);
OrderItem orderItem2=OrderItem.createOrderItem(book2,40000,4); //4권 주문
em.persist(orderItem2);
//주문 초기화(회원 주소로 배송)
Order order2 = Order.createOrder(member, createDelivery(member), orderItem1, orderItem2);
em.persist(order2);
}
/**
* 회원, 상품, 배송 정보 setting 메서드
*/
private Member createMember(String name, String city, String street, String zipcode) {
Member member=new Member();
member.setName(name);
member.setAddress(new Address(city,street,zipcode));
return member;
}
private Book createBook(String name, int price, int stockQuantity) {
Book book=new Book();
book.setName(name);
book.setPrice(price);
book.setStockQuantity(stockQuantity);
return book;
}
private Delivery createDelivery(Member member) {
Delivery delivery=new Delivery();
delivery.setAddress(member.getAddress()); //회원 주소로 배송한다 가정.
return delivery;
}
}
}
@PostConstruct: 이 어노테이션은 해당 메소드가 클래스의 객체가 생성된 후에 자동으로 호출되어야 함을 나타낸다. 따라서 init() 메소드는 dbInit 클래스의 객체가 생성된 후에 자동으로 호출됨.