JPA에 대해 - 활용

쿠우·2023년 1월 7일
0

공부하면서 요약한 부분만 정리해놨고 내용이 많아 보기좋게 다듬지는 못함. 추 후 수정 요망

API 개발 기본

회원 등록 API

  • JPA를 끼고서 API 만드는 것은 고려해야할 점이 많다.
  • postman 이용해서 api 방식으로 확인

엔티티에 @NotEmpty 를 쓰면 문제점

  • 문제점 1
    상황 : 엔티티의 필드(컬럼)명을 바꿔야한다면?
    기존 : 화면 > JSON(API) > DTO > DTO.get~~ 으로 entity에 정보 주입
    ,따라서 entity의 필드명이 바뀌어도 들어가는 값에 대한 변경은 없음 api의 수정소요가 없다.

바로 엔티티로 넣는다면 :
화면 > JSON(API) > entity 데이터의 흐름이면 entity필드명이 바뀔시에 이에 맞춰 api도 이름을 맞춰야하는 변경소요 발생

  • 문제점 2
    상황 : api가 주고 받아야하는 필요한 여러가지 정보들이 있다.

엔티티로 넣는다면 :
엔티티가 가진 값들만을 이용해 주고받아야한다 -> 비효율적, 불가능 각각의 api가 요구하는 데이터는 모두 다를 것 /
필요 없는 값까지 api가 받아야함 -> 보내면 안되는 결정적인 값까지 보내는 보안문제도 발생 -> api에서 값을 바꿔주거나 엔티티 측에서 컬럼에 @JsonIgnore -> 화면에 종속적인 코드가 되어버렷

기존 : DTO를 사용하면 그 이상의 값을 받아도 DTO로 받고 분배를 잘해서 엔티티들에게 정리해서 넣어줄 수 있다.
또한 엔티티에게서 뽑아낸 값을 잘 조합해서 api에 필요한 값들을 보낼 수 있다.

회원 수정 API

  • service에 void로 만들어서 update로직을 만든다. 엔티티 반환으로하면 엔티티 부분에 의존적인 코드가 되어버린다. 분리하기 위해!
  • service에 update를 DTO로 받게 되면 controller에서 생성한 웹DTO에 의존적인 코드가 되어버린다. 분리하기 위해!

회원 조회 API

  • 회원 등록 API 에서 문제점 2에 문제들이 그대로 적용된다.
  • 반환타입을 List나 컬렉션으로 바로 내어버리면 API스펙상의 유연성이 확 떨어진다. 따라서 객체로 한 번 감싸서 반환하자.
    (객체 (LIST(DTO), 기타 정보들~~) 이런식 )
    (배열로 묶고 그 안에서 오브젝트나 기본타입의 정보들을 넣는거 보다 오브젝트 안에서 오브젝트를 담고있는 배열이나 기본타입 정보로 들어가는 것이 유연성이 높음)

API 개발 고급

API 개발 고급 소개

  • 실무에서 성능문제에 직면 했을 때 무엇이 문제였는지 알려주기 위해 준비한 내용들이다.
  • 다 조회에 관련한 내용들이다.

조회용 샘플데이터 입력

  • 예제여서 무식하게 넣는 방식(무식 하긴하다..)

지연로딩과 조회성능 최적화

  • 주문 + 배송정보 + 회원을 조회하는 API를 통하여 지연로딩 때문에 발생하는 성능 문제를 확인
  • 최적화: DB 쿼리문 남발로 인한 성능문제 줄이기 및 유지보수성 향상

ManyToOne , OneToOne 문제점

  • 양방향 연관관계에 대해서 조회를 하면 서로 관계가 되어있기때문에 무한루프에 걸린다.
    따라서, 한쪽에 @JsonIgnore 설정해놔야 다음 단계로 넘어 갈 수 있다.
    그런데, 다음 단계는 또 지연 로딩에서 문제가 일어난다. 실제 엔티티 대신 프록시가 존재하고 jackson 라이브러리는 기본적으로 이 프록시 객체를 json으로 어떻게 생성해야 하는지 몰라서 예외 발생

1. hibernate5Module 이용해서 직접노출

  • 일단 이렇게 사용하면 안된디. 엔티티를 직접 노출 시키는건 옳지 않디! (가급적 필요한 데이터만 노출해야함)
  • 문제점: 엔티티의 데이터가 그대로 노출된다. api스펙에도 맞지 않다.
  • hibernate5Module 이용하여 기본적으로 초기화 된 프록시 객체만 노출, 초기화 되지 않은 프록시 객체는 노출 안함
  • 지연로딩이 이러한 문제점이 있다고 즉시로딩으로 바꾸지말자..
    와다다다 다 쿼리가 날라가서 어디서 무엇이 나오는지 모르게 뒤죽박죽이 되어버린다.

2. 엔티티를 DTO로 변환

  • 필요한 데이터만 노출하는 것은 해결했다.
  • 문제점: 허나 2개의 물품을 주문을 2번 하게 만들면 5번의 쿼리가 나감 (N+1 번 오류)
    (Order를 처음 가져올때 1번째 + 회원 N + 배송 N)

3. 엔티티를 DTO로 변환 + 페치 조인을 통한 최적화

  • 한번에 DB에서 싹 긁어 온다. 여러번 나가는 쿼리를 최적화 시켰다
  • 셀렉트문으로 엔티티를 다 가지고 온다는 작은 단점이 있다.

4. JPA에서 DTO로 바로 조회

  • DTO를 사용하여 JPQL을 작성한다. 일반적인 SQL을 사용할 때 처럼 JPQL이 정리된 메소드를 통해 원하는 값을 선택해서 조회
  • DTO를 이용해 조회했기 때문에 update 같은건 안된다.
  • DTO를 이용했기에 원하는 부분만을 골라 select한다는 장점이 있다.
    (SELECT 절에서 원하는 데이터를 직접 선택하므로 DB 애플리케이션 네트웍 용량 최적화(생각보다 미비)
  • 3번하고 별로 성능차이가 안난다.
  • 단점으로는 DTO를 이용함으로 리포지토리 재사용성 떨어지고 , repository에 api스펙에 맞춘 코드가 들어감으로 의존성이 생긴다. (코드도 길고 복잡해짐)
  • 만약에 써야한다면 따로 패키지와 클래스를 이용하여 성능을 위한 부분이라는 것을 알게끔 구분하여 명시하고 사용한다.

어떤 방법으로 성능 최적화하는 것이 좋을까?

쿼리 방식 선택 권장 순서

  1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
  2. 필요하면 페치 조인으로 성능을 최적화 한다. 대부분의 성능 이슈가 해결된다.
  3. 그래도 안되면 DTO로 직접 조회하는 방법을 사용한다.
  4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.

API 개발 고급 - 컬렉션 조회 최적화

  • 지연로딩과 조회성능 최적화 부분에서는 OneToOne, ManyToOne 이었다면 이번에는 OneToMany의 관계이다.
    즉, 뻥튀기에 대해서 고려해야함

엔티티 직접 노출 (하지마라)

  • 엔티티가 변하면 API 스펙이 변한다. (그니까 하지마라)
  • Hibernate5Module 모듈 등록 , 양방향 관계 문제 발생 -> @JsonIgnore 를 이용해 해결한다. (쓰지마라)

엔티티를 DTO로 변환

  • DTO 속에 필드인 컬렉션의 제네릭 속에도 DTO로 바꿔야 한다.

엔티티를 DTO로 변환 - 페치 조인 최적화

  • DB와 JPA영속성 컨텍스트 간의 조인과 중복의 의미가 다른 것을 인지해야한다.
  • distinct를 통해 JPA에서 동일객체에 중복을 줄여준다. 단, 페이징이 불가능하다.
    (메모리에서 페이징처리를 먼저 해버린다. / 이렇게 되는 이유는 일대다 페치조인을 해버린 순간 기준이 다 틀어진다.
    왜? DB에 조인된 테이블이랑 엔티티의 조인이랑 컬럼 개수가 다르니까 어디서 짜를지 모르는 것)
  • 컬렉션 페치 조인은 1개만 사용해야한다. 컬렉션 둘 이상 페치조인하면 데이터 정합성이 완전히 깨져버림

컬렉션 페치 조인 -> 페이징 한계를 돌파

  • 문제점 정리: 일다대에서 일(1)을 기준으로 페이징을 하는 것이 목적이다. 그런데 데이터는 다(N)를 기준으로 row가 생성된다. 따라서 JPA와 DB의 컬럼수는 다르고 페이징 처리에 문제가 생김.
    하이버네이트는 경고 로그를 남기고 모든 DB 데이터를 읽어서 메모리에서 페이징을 시도 -> 장애로 이어진다.

페이징 + 컬렉션 엔티티 조회 문제는 이 방법으로 해결

  • 컬렉션 페치 조인은 사용하지 않고 따로 조회한다.
  • BatchSize를 통해 해결한다. 건수만큼 추가 SQL을 날리지 않고,
    조회한 엔티티의 기본키들을 모아서 SQL IN 절을 날린다.
 - hibernate.default_batch_fetch_size: 글로벌 설정 (전체)
- @BatchSize: 개별 최적화(디테일하게)
- 이 옵션을 사용하면 컬렉션이나, 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회한다.

다(N)의 입장에서 조인되는 것을 분리시키고 Batchsize를 적용시키는 것에 장점
- 쿼리 호출 수가 1 + N 는 1 + 1(where절에 in을 추가 해서 N개를 한번에 조회) 로 최적화 된다.
- 컬렉션 페치조인은 중복을 제거하고 나면 페이징이 불가능했지만 이 방법은 페이징도 가능 
  • 결론적으로 :
  1. 먼저 ToOne(OneToOne, ManyToOne) 관계를 모두 페치조인 한다.
  2. 컬렌션의 따로 지연로딩으로 조회한다.
  3. 지연 로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size , @BatchSize 를 적용한다.
    (size는 1000개 넘어가면 부하 걸릴 확률이 있다. 적당한 사이즈(100~1000)로 하자)

권장 순서

권장 순서
1. 엔티티 조회 방식으로 우선 접근

  1. 페치조인으로 쿼리 수를 최적화
  2. 컬렉션 최적화
1. 페이징 필요 hibernate.default_batch_fetch_size , @BatchSize 로 최적화 (여기서 충분히 가능)
2. 페이징 필요X 페치 조인 사용
  1. 엔티티 조회 방식으로 해결이 안되면 DTO 조회 방식 사용
    (성능상 크게 문제가 있지않는이상 엔티티 조회 방식으로 해결 가능)
  2. DTO 조회 방식으로 해결이 안되면 NativeSQL or 스프링 JdbcTemplate

Tip. 엔티티는 캐시하면 안되고 DTO로 변환해서 캐시해야한다.
Cache란 자주 사용하는 데이터나 값을 미리 복사해 놓는 임시 장소를 가리킨다. (예로 Redis)

OSIV와 성능 최적화

  • 예전 표준이 되기전에 하이버네이트에서는 EntityManager를 Session 이라고 불렀다. 표준으로 명칭이 바뀌어도 관례상 OSIV라고 함
Open Session In View: 하이버네이트
Open EntityManager In View: JPA
  • Spring 프로젝트를 동작시키면 애플리케이션 시작 시점에 spring.jpa.open-in-view에 대해 warn 로그를 남기는 것은 이유가 있음

warn의 원인은 JPA가 데이터 베이스 커넥션을 언제 사용하는지 알아야 한다.

  • 트랜잭션 시작할 때 영속성컨텍스트가 DB커넥션 구해온다.
  • DB커넥션을 돌려주는 것에는 차이가 있다
    (OSIV가 켜져있는지 꺼져있는지에 따라 나눠진다.)
  • OSIV 전략은 트랜잭션 시작처럼 최초 데이터베이스 커넥션 시작 시점부터 API 응답이 끝날 때 까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지
  • 지연 로딩은 영속성 컨텍스트가 살아있어야 가능하고, 영속성 컨텍스트는 기본적으로 데이터베이스 커넥션을 유지

OSIV 전략이 영속성 컨텍스트와 DB커넥션을 끝까지 유지시켜주는 것에 대한 문제점은?

  • 오랜시간동안 데이터베이스 커넥션 리소스를 사용하기 때문에, 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자랄 수 있다 -> 장애
    (끝까지 들고있다가 고객한테 응답을 주는 타이밍에 반환한다.)
    (예를 들면, 서 컨트롤러에서 외부 API를 호출하면 외부 API 대기 시간 만큼 커넥션 리소스를 반환하지 못하고, 유지)

OSIV 전략 끈다면(false)?

  • OSIV를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, 데이터베이스 커넥션도 반환한다. 따라서 커넥션 리소스를 낭비하지 않는다.
  • 단점으로는 OSIV를 끄면 모든 지연로딩을 트랜잭션 안에서 처리해야 한다. controller인 view 영역에서 동작을 하지 못하고 트랜잭션이 끝나기 전에 지연로딩을 강제로 호출해둬야 한다.

실무에서 OSIV를 끈 상태로 복잡성을 관리하는 좋은 방법은?

  • Command와 Query를 분리
    (참고: https://en.wikipedia.org/wiki/Command–query_separation)

    명령 쿼리 책임 분리 ( CQRS )는 읽기 작업과 쓰기 작업을 서로 분리하여 처리하는 것을 의미.
    읽기 작업과 쓰기 작업을 서로 다른 시스템에서 처리할 수 있기 때문에 확장성이 좋아짐.

  • Service에서 조회로직을 분리한다.
~~~Service: 핵심 비즈니스 로직
~~~QueryService: 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션 사용 @Transactional(readOnly=true))(조회로직만 분리해서 CQRS패턴)
  • 보통 서비스 계층에서 트랜잭션을 유지한다. 두 서비스 모두 트랜잭션을 유지하면서 지연 로딩을 사용할 수 있다.
  • 화면에 맞춘 서비스와 핵심비즈니스로직의 라이프 사이클이 다른 것을 고려한다.

결국에 결론은?

  • OSIV를 켜면 유지보수성에 이득을 볼 수 있고 끄면 성능상에서 이득을 볼 수 있다.
  • 선생님은 고객 서비스의 실시간 API는 OSIV를 끄고, ADMIN 처럼 커넥션을 많이 사용하지 않는 곳에서는 OSIV를 켠다

의문점 : Repository도 같은 패턴으로 나눈다???!
Entity를 찾을 때와 특정화면에 맞춘 쿼리를 사용할 때를 나누기 때문에 ㅇㅋ.

profile
일단 흐자

0개의 댓글