본문 바로가기
JPA

JPA 를 공부하면서 알게 된 내용 정리 1

by 성건희 2022. 1. 26.
반응형

JPA 를 학습하면서 알게 된 내용을 정리해보았다.

 

🎯 컨트롤러에서 엔티티를 반환하지 마라

API 를 만들때는 절대로 엔티티를 외부로 반환하면 안된다.
만약 Member 엔티티 속성으로 name 과 password 가 추가된다면, 2가지 문제가 발생한다.


  1. 패스워드가 그대로 노출되는 문제가 있다.
  2. API 스팩이 변한다.

따라서 MemberRequestDto, MemberResponseDto 등의 DTO로 변환해서 넘기는 것이 좋다.

 

🎯 클래스 내 필드 직접 접근 VS getter() 를 통한 접근

클래스 내에서는 getter 를 호출하는 것보다 필드를 직접 호출하는 것이 코드가 더 깔끔하다.
그래서 필드에 직접 접근하는 방법을 주로 사용한다.
하지만,, JPA 프록시를 많이 다루게 되면 이 부분이 중요해지는데, 조회한 엔티티가 프록시 객체인 경우 필드에 직접 접근하면 원본 객체를 가져오지 못하고 프록시 객체의 필드에 직접 접근해 버리는 문제가 있다.
일반적인 상황에서는 문제가 없는데, equals, hashcode 를 JPA 프록시 객체로 구현할 때 문제가 될 수 있다.

프록시 객체의 equals 를 호출했는데 필드에 직접 접근하면, 프록시 객체는 필드에 값이 없으므로 항상 null 이 반환된다.
그래서 JPA 엔티티에서 equals, hashcode 를 구현할 때는 getter 를 내부에서 사용해야 한다.

 

🎯 Form VS Dto

ex ) MemberForm, MemberDto

Form 이나 Dto 모두 단순히 계층간의 데이터 전달에 사용된다. 그렇기에 둘의 역할은 같다.
다만, form 이라는 것은 제약을 더 두어서 명확하게 컨트롤러 까지만 사용해야 한다는 의미를 강하게 둘 때 사용한다.
DTO 는 더 범용적으로 사용하는 단어이다.

 

🎯 컨트롤러에서 어설프게 엔티티를 생성하지 마라

스크린샷 2022-01-26 오후 8 29 21

form 객체를 Service 로 넘기는 것 보다, 컨트롤러에서 엔티티를 생성한 후 넘겨주는 방식이 낫다.
(form 을 서비스로 넘기면 서비스에서 form 객체를 엔티티로 변환하는 로직이 들어가니 지저분한 코드가 생긴다)

하지만 더 나은 설계는 필요한 데이터만 파라미터로 넘기는 방법이다.

스크린샷 2022-01-26 오후 8 29 27

스크린샷 2022-01-26 오후 8 29 34

만약 파라미터의 개수가 많다면 DTO 를 만들어 넘기는 것도 좋다.

 

🎯 커맨드와 쿼리를 분리하라 (CQS)

https://en.wikipedia.org/wiki/Command%E2%80%93query_separation

  • Command (명령) : 상태를 변경한다.
  • Query (조회)

 

스크린샷 2022-01-26 오후 8 31 15

위 코드는 문제가 있다. 더티 체킹으로 인한 update (커맨드) 와 return member 로 조회 (쿼리) 2가지 일을 하고 있다.

메서드를 호출 했을 때, 내부에서 변경이 일어나는 메서드인지, 내부에서 변경이 전혀 일어나지 않는 메서드인지 명확히 분리해야한다.

따라서 아래와 같이 커맨드와 쿼리가 분리 되어야 한다.

 

스크린샷 2022-01-26 오후 8 31 22

스크린샷 2022-01-26 오후 8 31 25

만약, 데이터 변경 관련해서 이슈가 발생했다면, 변경이 일어나는 메서드만 살펴보면 된다.
변경 메서드도 변경에만 집중하므로 유지보수가 더 좋아진다.

권장하는 방법은 아래와 같다.


  • insert 는 id 만 반환
  • update 는 아무것도 반환하지 않는다.
  • select 는 내부의 변경이 없는 메서드로 설계하면 좋다.

 

🎯 엔티티로 조회 VS DTO 로 조회

⭐️⭐️ 쿼리 방식 선택 권장 순서 ⭐️⭐️

  1. 엔티티를 DTO로 변환하는 방법을 선택한다.
  2. 필요하면 페치 조인으로 성능을 최적화 한다. => 2번으로 대부분의 성능 이슈가 해결이 된다.

스크린샷 2022-01-26 오후 8 33 54


  1. 그래도 안되면 DTO 로 직접 조회하는 방법을 사용한다.

스크린샷 2022-01-26 오후 8 34 00


  1. 최후의 방법은 JPA 가 제공하는 네이티브 SQL 이나 스프링 JDBC Template 을 사용해서 SQL 을 직접 사용한다.

 

🎯 JPA 에서의 distinct

스크린샷 2022-01-26 오후 8 35 04

DB 에서의 distinct 는 row 의 데이터가 완전 동일해야 중복을 제거한다.
하지만, JPA 에서의 distinct 는 엔티티의 식별자가 같으면 중복을 제거한다.

 

🎯 일대 다 페치 조인에서 페이징 처리

일대 다 페치 조인 시에는 페이징이 불가능하다는 문제가 있다.
'다' 기준으로 join 이 되기 때문에 데이터가 뻥튀기 되는 문제가 있기 때문이다.


실제로 페이징 쿼리를 날리게 되면 하이버네이트에서는 다음과 같은 warning 에러 로그를 찍어준다.

스크린샷 2022-01-26 오후 8 35 57

해석하면 하이버네이트가 내장 메모리로 페이징 처리를 하겠다는 로그이다.
(잘못하면 Out of Memory 에러가 발생하니 매우 위험하다.)


참고로 컬렉션 페치 조인은 1개만 사용할 수 있다.
컬렉션을 둘 이상 페치 조인을 하게되면 데이터가 부정합하게 조회될 수 있기 때문이다.


페이징 + 컬렉션 엔티티를 함께 조회하려면 어떻게 해야할까?

  1. 먼저 ToOne 관계를 모두 페치 조인한다. (ToOne 관계는 row 수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않는다.)
  2. 컬렉션은 지연 로딩으로 설정한다.
  3. 지연 로딩 성능 최적화를 위해 'hibernate.default_batch_fetch_size', '@BatchSize' 를 적용한다.
    - hibernate.default_batch_fetch_size : 글로벌 설정
    - @BatchSize : 개별 최적화
    - 해당 옵션을 사용하면 컬렉션이나 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회한다.

 

🎯 엔티티를 캐시한다?

엔티티는 직접 캐시하면 안된다.
엔티티는 영속성 컨텍스트에 의해 관리되는데, 캐시가 되면 지워지지 않기 때문에 기능이 꼬일 수 있는 문제가 있다.

따라서 엔티티를 DTO 로 변환해서 DTO를 캐시해야한다.

참고 ) 엔티티를 캐시하는 방법으로 하이버네이트의 2차 캐시가 있기는 하지만, 실무에서 적용하기 너무 까다롭다.
영한님은 주로 레디스나 로컬의 메모리 캐시를 통해 DTO를 캐시한다고 한다.

 

🎯 OSIV

이걸 모르면 프로젝트 장애로 이어질 수 있기 때문에 해당 내용은 꼭 이해해야한다.


- Open Session In View : 하이버네이트
- Open EntityManager In View : JPA (관례 상 OSIV 라고 함)


  • spring.jpa.open-in-view : true 기본 값

springBoot 실행 시 아래와 같은 warning 로그가 출력된다.

스크린샷 2022-01-26 오후 8 36 56

 

OSIV ON

스크린샷 2022-01-26 오후 8 37 23

- spring.jpa.open-in-view: true


장점
OSIV 전략은 트랜잭션 시작처럼 최초 데이터베이스 커넥션 시작 시점부터 API 응답이 끝날 때 까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지한다. 그래서 지금까지 View Template 이나 API 컨트롤러에서 지연 로딩이 가능했던 것이다.
지연 로딩은 영속성 컨텍스트가 살아있어야 가능하고, 영속성 컨텍스트는 기본적으로 데이터베이스 커넥션을 유지한다.
이것 자체가 큰 장점이다. (쿼리 서비스를 안 만들고 컨트롤러나 뷰에서 지연로딩 등을 처리할 수 있다.)


단점
너무 오랜시간동안 데이터베이스 커넥션 리소스를 사용하기 때문에, 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자를 수 있다. 이것은 결국 장애로 이어진다.
예를 들어서 컨트롤러에서 외부 API 를 호출하면 외부 API 대기 시간 만큼 커넥션 리소스를 반환하지 못하고 유지해야한다.

 

OSIV OFF

스크린샷 2022-01-26 오후 8 37 27

- spring.jpa.open-in-view: false (OSIV 종료)


장점
OSIV 를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, 데이터베이스 커넥션도 반환한다.
따라서 커넥션 리소스를 낭비하지 않는다.


단점
OSIV 를 끄면 모든 지연로딩을 트랜잭션 안에서 처리해야 한다.
따라서 모든 지연로딩 코드를 트랜잭션 안으로 넣어야 하는 단점이 있다.
그리고 view template 에서 지연로딩이 동작하지 않는다.
결론적으로 트랜잭션이 끝나기 전에 지연 로딩을 강제로 호출해 두어야 한다.


해결법
- 방법 1 ) 트랜잭션 내에서 사용하기
- 방법 2 ) 페치 조인으로 가져오기
- 방법 3 ) 쿼리용 서비스를 따로 만든다 ex) OrderQueryService


커맨드와 쿼리 분리
실무에서 OSIV 를 끈 상태로 복잡성을 관리하는 좋은 방법이 있다. 바로 Command 와 Query 를 분리하는 것이다.

보통 비즈니스 로직은 특정 엔티티 몇개를 등록하거나 수정하는 것이므로 성능에 크게 문제가 되지 않는다.
그런데 복잡한 화면을 출력하기 위한 쿼리는 화면에 맞추어 성능을 최적화 하는 것이 중요하다.

하지만 그 복잡성에 비해 핵심 비즈니스에 큰 영향을 주는 것은 아니다.
그래서 크고 복잡한 애플리케이션을 개발한다면, 이 둘의 관심사를 명확하게 분리하는 선택은 유지보수 관점에서 충분히 의미있다.

단순하게 설명해서 다음처럼 분리하는 것이다.

OrderSerivce
- OrderService : 핵심 비즈니스 로직
- OrderQueryService : 회면이나 API 에 맞춘 서비스 (주로 읽기 전용 트랜젝션 사용)


보통 서비스 계층에서 트랜잭션을 유지한다. 두 서비스 모두 트랜잭션을 유지하면서 지연 로딩을 사용할 수 있다.


그럼 OSIV 를 끄라는 건지, 켜라는 건지...
영한님은 고객 서비스의 실시간 API 는 OSIV 를 끄고
ADMIN 처럼 커넥션을 많이 사용하지 않는 곳에서는 OSIV 를 켠다고 하심.


코딩의 유지보수 관점에서는 OSIV 를 키는게 맞지만, 성능 관점에서 보면 OSIV 를 끄는게 맞다. (트레이드 오프)

 

참고

  • 실전! 스프링 부트와 JPA 활용 1 - 웹 애플리케이션 개발
  • 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화

 

반응형

'JPA' 카테고리의 다른 글

JPA 를 공부하면서 알게 된 내용 정리 3  (0) 2022.02.02
JPA 를 공부하면서 알게 된 내용 정리 2  (0) 2022.01.29
다양한 연관관계 매핑  (0) 2020.04.09
연관관계 매핑 기초  (0) 2020.04.06
엔티티 매핑  (0) 2020.04.06

댓글