고급 주제와 성능 최적화 2 + 16장

Lee Seung Jae·2022년 2월 21일
0

지난 포스팅에 이어서 15장을 쭉 정리해볼 예정이다.

성능 최적화

N + 1 문제

연관관계 조회에 있어서 가장 주의해야 하는 문제이기도 하고, 실제 스터디원 분들도 이 성능에 대한

이슈 사항들을 되게 최적화한 경험담도 듣고 해서 더욱 더 중요하게 관리해야겠다고 생각했다.

일단 간단하게 생각하는 문제는 즉시로딩의 문제이다.

@Entity
public class Member {
  @Id
  @GeneratedValue
  private Long id;

  @OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
  private List<Order> orders = new ArrayList<>();
}

@Entity
public class Order {
  @Id
  @GeneratedValue
  private Long id;

  @ManyToOne
  private Member member;
}

이런식으로 엔티티 설계를 했을 때 1:N, N:1 양방향 연관관계 매핑이 된다.

그러면서 멤버의 주문은 즉시로딩으로 설정해주었다.

MemberRepository.findById()를 하게 되면

이렇게 되었을때 연관된 것만 딱 불러오게 된다.

근데 JPQL로 할 시에 멤버하나만 조회하려고 select m from Member m 을 하게되면 멤버 하나당 가지고 있는

주문을 전부 select 조회 하게 된다.

그래서 사용할 때의 시점부터 조회되게 지연 로딩을 하려면 fetch를 EAGER로 주면 된다.

근데 여기서도 문제가

반복루프로 인해서 멤버 하나당 주문에 관련된것을 조회하게 되면 멤버 하나에 연관된 주문을 전부 조회해야 하기 때문에

또 다시 N + 1문제가 발생한다.

해결책

페치 조인

N + 1 문제를 해결하는 가장 일반적 방법이다.

SQL 조인을 사용하여 연관된 엔티티를 다 조회하기 때문에 N + 1 문제가 생기지 않는다.

@BatchSize

배치사이즈는 지정한 size만큼 In 절을 사용해서 조회한다.

지정한 사이즈보다 많은건 수를 조회한다면 한번더 추가 실행한다.

@Fetch

fetch모드 subselect를 사용하면 연관 데이터 조회시에 서브쿼리를 사용하여 조회한다.

즉시 로딩이라면 조회시점, 지연 로딩이라면 해당 엔티티를 사용하는 시점에 실행된다.

N + 1 정리

N + 1 아직 경험해보지 못했지만 현재 진행중인 프로젝트에서 만나게 될 것 같다고 생각한다.

이부분에 대해 페치조인 을 계속 생각하고 있다.

나머지 내용은 내가 생각했을때 쓸 것이 많이 없을 것 같다.

  • @OneToOne, @ManyToOne
    • 기본 페치전략: 즉시로딩
  • @OneToMany, @ManyToMany
    • 기본 페치전략: 지연로딩

이 부분에 유의해서 사용해주도록 하자.

읽기 전용 쿼리

엔티티가 영속성 컨텍스트에 의해 관리되는것은 상당히 좋은 JPA의 장점이다.

그렇지만 영속성 컨텍스트가 save 할때를 생각해보자.

변경 감지를 위해서 항상 스냅샷을 보관하고 있을텐데, 당연히 메모리를 사용하니까 많은 메모리가 소요될 수 있다.

단순하게 조회만 하는 엔티티라면?

예를 들어, 그 메서드가 조회를해서 다른 것을 조회하거나 다시 조회하지도 않을 것이고 화면에 출력만 해주면 된다.

이렇게 되면 영속성 컨텍스트에서 관리되지 않아도 될 것이다.

이럴 때, 바로 읽기전용 쿼리를 사용한다는 것이다.

다음 JPQL이 있다고 가정한다.

select m from Member m

스칼라 타입

일반 SQL에서도 *로 전체를 조회하는 것보다, 특정 칼럼만 추출한다면 속도는 빨라지게 된다.

그래서 JPQL에서도 같은 방법으로 스칼라 타입으로 조회하는 것이 있다.

스칼라 타입은 영속성 컨텍스트가 결과를 관리해주지 않는다.

select m.name, m.gender from Member m

힌트 사용

Hibernate의 전용 힌트 org.hibernate.readOnly를 사용하면 읽기 전용 조회가 가능하다.

읽기 전용이기 때문에 스냅샷 관리는 하지 않는다.

메모리는 최적화 할 수 있지만 스냅샷이 없기 때문에 영속성 컨텍스트에서 수정해도 DB에 반영되지 않는다.

TypedQuery<Order> query = em.createQuery("select o from Order o", Order.class);
query.setHint("org.hibernate.readOnly", true);

일반적인 jpa에서의 사용은 위와 같은데,

Spring Data JPA에서는 이렇게 사용한다.

힌트는 @QueryHints안에 @QueryHint를 사용해주면 된다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    @QueryHints({
        @QueryHint(name = "org.hibernate.readOnly", "true")
    })
    Optional<Member> findreadOnlyByName(String name);
}

읽기 전용 트랜잭션 사용

이부분을 @Transactional 을 많이 쓰기 때문에 주의 깊게 보았고 궁금했었다.

사실 나는 @Transactional 이것만 기본으로 주고 사용했지,

실제 readOnly 값을 주는 것은 내 코드에선 없었다.

트랜잭션에 @Transactional(readOnly = true) 이렇게 주게 되면,

프레임워크가 hibernate 세션의 플러시 모드를 MANUAL로 설정한다.

이 옵션을 주면 플러시를 인위적으로 호출하지 않으면 플러시가 일어나지 않는다.

트랜잭션이 커밋되어도 플러시는 일어나지 않는다.

메소드단위로 트랜잭션이 돌게 되는데 영속성 컨텍스트만 플러시하지 않는것

DTO로 바로 조회할 때는 어차피 스냅샷이 만들어지지 않는다고 한다.

배치 처리

배치 처리는 수많은 데이터들을 다루기 때문에 주의해주어야 한다.

대용량의 데이터를 관리하면서 메모리 부족현상을 겪지 않으려면

배치가 도는 중간 사이사이에 영속성 컨텍스트를 비워주는 것이 적절한 행동이다.

profile
💻 많이 짜보고 많이 경험해보자 https://lsj8367.tistory.com/ 블로그 주소 옮김

0개의 댓글