스프링 부트와 JPA 강의 1편, 2편을 본지 거의 1년이 되어가는 상황이다.
남은 방학을 불태우자는 생각으로 복습을 진행했는데, 이번 기회에 한 번 글로 잘 정리해두려고 한다.
스프링 부트와 JPA 1편과 2편에서 느낀점과 중요한 부분을 남겨보겠다.
스프링 부트와 JPA 활용1
정리
- 스프링 부트와 JPA를 사용해 전체적인 개발 흐름을 살펴볼 수 있던 것이 좋았다.
- 도메인 설계 부분에서 배울 부분이 있었다.
- 중간 테이블이 존재하는 다대다 관계에서 어떻게 엔티티들을 처리하는지, 글로벌 지연 로딩 전략 등 인상 깊은 부분이 많았다.
- 병합 메서드를 이용하기보다는 변경감지를 사용하는 부분도 잘 기억하는게 좋겠다.
이 정도로 정리할 수 있을 것 같다. 전반적으로 개발을 훑어보는 강의?라고 생각한다.
스프링 부트와 JPA 활용2
기억할 부분은 이 강의가 더 많다고 생각한다. 물론 둘 다 너무 좋고 재밌었던 강의다.
API 개발 기초 정리
- API 요청, 응답 스펙에 맞추어 별도의 DTO를 만들고 사용하자.
- 엔티티 스펙을 절대 API에 노출하지 않는다.
- 엔티티의 역할은 API 요청, 응답을 수행하는 역할이 아니다.
API 개발 지연 로딩과 조회 성능 최적화 정리
- 엔티티를 노출할 때 양방향 연관관계가 걸린 곳은 무한 루프가 걸린다. 따라서 @JsonIgnore 처리 해야 한다.
- 그러나 절대 엔티티를 API에 노출하지 말고 API 요청, 응답 스펙에 맞추어 별도의 DTO를 사용하자.
- 글로벌 지연 로딩 전략을 택하자. 그러나 이러면 N + 1 문제가 발생할 수 있다.
- 성능 최적화가 필요한 곳에는 페치 조인을 적용해 한 번에 조회하자.
- 조회 성능이 중요한 곳은 엔티티로 조회해 DTO로 변환해 응답을 하지 말고 리포지토리에서 바로 DTO로 조회하자.
이 방식은 리포지토리 재사용이 떨어지며, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점이 있다.
쿼리 방식 선택 권장 순서
- 우선 엔티티를 조회해 DTO를 반환하는 방법을 선택한다.
- 필요하면 페치 조인으로 성능일 최적화 한다.
- 그래도 성능이 안나오면 DTO로 직접 조회하는 방법을 택한다.
- 정말 안되면 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 작성한다.
API 개발 컬렉션 조회 최적화 정리
이번에는 컬렉션인 일대다 관계를 조회하고, 최적화 하는 방법에 대해 정리해보겠다.
엔티티로 조회하고 DTO로 변환 후 반환
- 마찬가지로 엔티티를 직접 API에 노출하면 안된다.
- 이전과 똑같이 리포지토리에서 엔티티로 조회하고 DTO로 반환해 응답하는 방법이 있다. 성능 최적화를 위해 페치 조인을 사용한다.
- 대신 여기에서 문제가 생기는데 컬렉션 페치 조인을 사용하면 페이징이 불가능하다.
하이버네이트는 경고 로그를 남기면서 모든 데이터를 DB에서 읽어오고, 메모리에서 페이징 해버린다. - 컬렉션을 페치 조인하면 페이징이 불가능한데,
컬렉션을 페치 조인하면 일대다 조인이 발생하므로 데이터가 예측 불가하게 증가해버리기 때문이다. - 그럼 페이징 + 컬렉션 엔티티를 함께 조회하면서 코드도 단순하고, 성능 최적화도 보장하는 방법이 있다.
바로 hibernate.default_batch_fetch_size, @BatchSize를 사용하면 된다.
hibernate.default_batch_fetch_size , @BatchSize
- 이 방식을 사용하려면 먼저 전제 조건이 필요하다.
- 먼저 ToOne 관계를 모두 페치 조인한다. 또한 컬렉션은 지연 로딩으로 조회한다.
- 이후 hibernate.default_batch_fetch_size, @BatchSize를 적용하면 된다. 전자는 글로벌 설정이고 후자는 개별 최적화이다.
- 이 옵션을 사용하면 컬렉션이나, 프로시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회한다.
- 이러면 쿼리 호출 수가 1 + N -> 1 + 1로 최적화 된다.
- 조인보다 DB 데이터 전송량이 최적화 되며, 페치 조인 방식과 비교해서 쿼리 호출 수가 약간 증가하지만, DB 데이터 전송량이 감소한다.
- 결론은 ToOne 관계는 페치 조인해도 페이징에 영향을 주지 않는다. 따라서 ToOne 관계는 페치조인으로 쿼리 수를 줄여서 해결하고, 나머지는 hibernate.default_batch_fetch_size로 최적화하자.
참고로 hibernate.default_batch_fetch_size도 적당한 사이즈를 골라야 하는데, 100 ~ 1000사이가 적당하다고 한다.
물론 계속 모니터링 하면서 애플리케이션에 적당한 값을 찾는 것이 제일 좋다.
DTO로 조회 후 반환
- 첫 번째 방법은 루트가 되는 DTO 쿼리 1번, 컬렉션 DTO N번 호출하는 방식이다.
row 수가 증가하지 않는 ToOne 관계는 조인으로 최적화 하기 쉬우므로 한 번에 조회하고, ToMany 관계는 최적화가 하기 어려우므로 별도의 메서드로 조회한다. - 보통 단건 조회에서 이 방식을 많이 사용한다.
- 두 번째 방식은 루트 DTO 1번, 컬렉션 DTO 1번 호출되는 방식이다. 데이터를 한꺼번에 처리할 때 많이 사용한다.
- ToOne 관계를 먼저 조회하고, 여기서 얻은 식별자 id로 ToMany 관계 엔티티를 한꺼번에 조회한다.
- Map을 사용해서 매칭 성능을 향상 시켰다.
- 세 번째 방식은 쿼리를 1번만 호출하면 된다.
- 대신 쿼리는 1번이지만 조인으로 인해 DB에서 애플리케이션에 전달하는 데이터에 중복 데이터가 추가되므로 상황에 따라 두 번째 방식보다 느릴 수 있다. 그리고 페이징이 불가능하다.
쿼리 방식 선택 권장 순서
- 엔티티 조회 방식으로 우선 접근한다.
- 페치 조인으로 쿼리 수를 최적화
- 컬렉션 최적화
- 페이징이 필요하면 hibernate.default_batch_fetch_szie로 최적화 한다.
- 페이징이 필요 없다면 페치 조인을 사용한다.
- 엔티티 조회 방식으로 해결이 안되면 DTO 조회 방식을 사용한다.
- DTO 조회 방식으로 해결이 안되면 NativeSql이나 스프링 JdbcTemplate를 사용한다.
진정한 결론은 은탄환이라는 것은 없다는 것이다. 적절하게 잘 판단해서 좋은 방법을 선택하자.
OSIV와 성능 최적화
OSVI란 Open Session In View의 약어로 영속성 컨텍스트를 뷰까지 열어둔다는 뜻이다.
이건 하이버네이트 용어이며, JPA에서는 Open EntityManager in View라는 뜻이다.
영속성 컨텍스트가 살아있으면 엔티티는 영속 상태로 유지되는데, 따라서 뷰에서도 지연 로딩을 사용할 수 있다.
프로젝트에서 별도의 설정을 건드리지 않는다면 spring.jpa.open-in-view: true가 기본 값이다.
그러면 프로젝트가 시작하면서 아래와 같은 warn 로그를 남긴다.
경고 로그가 뜨는 것은 이유가 있다.
OSIV 전략이 true라면 요청이 들어오면 트랜잭션 시작처럼 데이터 베이스 커넥션을 얻고
서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트를 만들면서 트랜잭션을 시작하고 요청이 끝날 때 데이터베이스 커넥션과 영속성 컨텍스트를 함께 종료한다. 이렇게 하면 영속성 컨텍스트가 처음부터 끝까지 살아있으므로 조회한 엔티티도 영속 상태를 유지한다.
이 전략에는 문제가 있는데 너무 오랜 시간동안 데이터베이스 커넥션 리소스를 사용하기 때문에,
실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자랄 수 있다. 이것은 결국 장애로 이어진다.
따라서 spring.jpa.open-in-view: false로 설정해 OSIV를 끄는 편이 좋다.
OSIV를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, 데이터베이스 커넥션도 반환한다.
따라서 커넥션 리소스를 낭비하지 않는다. OSIV를 끄면 모든 지연로딩을 트랜잭션 안에서 처리해야 한다.
결론적으로 트랜잭션이 끝나기 전에 지연 로딩을 강제로 호출해야 한다.
커멘드와 쿼리 분리
- 실무에서는 OSIV를 끈 상태로 복잡성을 관리하는 좋은 방식이 있는데 명령과 쿼리를 분리하는 것이다.
- 예전에 공부한 CQRS 모델과 동일한 것 같다.
- 보통 복잡한 화면을 출력하기 위한 쿼리의 성능을 최적화 하는 것이 실무에서 제일 중요하다.
- 이런 복잡성에 비해 핵심 비즈니스에는 크게 영향을 주지 않는다.
- 따라서 관심사를 분리한는 것이 유지보수 관점에서 의미가 있다.
이상으로 포스팅을 마칩니다. 감사합니다.
댓글