본문 바로가기
JPA

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

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

Spring Data JPA - 1편

인터페이스만 있고 구현체는 없는데 어떻게 동작하는거지?


Spring Data JPA 가 구현 클래스를 대신 만들어 주기 때문에 동작하는 것이다.

실제로 해당 인터페이스의 클래스를 출력해보면 프록시가 나온다.

memberRepository.getClass(); => class com.sun.proxy.$ProxyXXX

또한 @Repository를 생략할 수 있다.

  • 컴포넌트 스캔을 스프링 데이터 JPA 가 자동으로 처리
  • JPA 예외를 스프링 예외로 변환하는 과정도 자동으로 처리

파라미터가 3개 이상이면...

메서드 명을 통해서 쿼리를 자동 생성해주는 기능은 정말 편하다.
하지만 조건이 많아지만 메서드 명이 너무 길어지는 문제가 있다.
영한님은 파라미터가 2개를 넘어가거나 쿼리가 복잡하면 JPQL 을 작성하는 방법으로 해결한다고 하심

컬렉션 파라미터 바인딩 (실무에서 많이 씀)

Collection 타입으로 in 절 지원

@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") Collection<String> names);

반환 타입

스프링 데이터 JPA 는 유연한 반환 타입을 제공

  • List findByName(String name); // 컬렉션
  • Member findByName(String name); // 단건
  • Optional findByName(String name); // Optional 단건

조회 결과가 없으면

  • 컬렉션은 빈 컬렉션을 반환한다. (null 체크 필요가 없음)
  • 단건은 null 을 반환한다.

참고로 단건 조회를 했는데 결과가 2건 이상이면, JPA 에러 (NonUniqueResultException) 가 발생하는데,
해당 에러가 발생하면 Spring Data JPA 가 스프링 에러 (IncorrectResultSizeDataAccessException) 로 바꿔서 발생시킨다.
이렇게 하는 이유는 클라이언트 코드가 JPA 에 의존하지 않고 스프링이 추상화 한 예외에 의존하게 되므로
다른 DB 로 바꿔도 클라이언트 코드를 수정할 필요가 없다는 장점이 있기 때문이다.

페이징

페이징 처리가 쉽지가 않은데, 스프링 데이터 JPA 를 사용하면 정말 편리하게 페이징을 할 수 있다.

@Query("select m from Member m left join m.team t")
Page<Member> findByAge(int age, Pageable pageable)

해당 메서드를 사용하면 totalCount 까지 같이 가져온다. (보통 페이징할때 totalCount 도 사용하므로)

하지만 위와 같이 사용하면 totalCount 조회 쿼리에 성능 문제가 발생한다.
totalCount 는 굳이 조인을 할 필요가 없는데 위 쿼리로 count 처리를 하기 때문이다.

최적화 하려면 아래와 같이 countQuery 를 추가 해주면 해결된다.

@Query(value = "select m from Member m left join m.team t"
         countQuery = "select count(m) from Member m")
Page<Member> findByAge(int age, Pageable pageable)

주의 : Page 는 1부터 시작이 아니라 0 부터 시작이다.

특별한 반환타입 : Page VS Slice VS List

Page<Member> findByAge(int age, Pageable pageable)

Page 방식은 페이징 데이터전체 데이터 개수 (totalCount) 결과를 가져온다.

Slice<Member> findByAge(int age, Pageable pageable)

Slice 방식은 페이징 데이터 + 1 의 결과를 가져온다. (전체 데이터 개수는 가져오지 않는다)
참고 ) 데이터를 1개 더 가져오는 이유는 클라이언트에서 페이지가 하나 더 있으면 더보기 등의 기능을 제공하기 위해서다.

List<Member> findByAge(int age, Pageable pageable)

List 방식은 페이징 데이터 결과를 가져온다. (전체 데이터 개수는 가져오지 않는다)

페이징 된 엔티티는 외부로 노출해서는 안된다.

API 의 스펙이 변하기 때문에 DTO 로 변환해서 넘겨야 한다.
스프링 데이터 JPA 는 page.map() 으로 편리하게 엔티티를 DTO 로 변환할 수 있게 지원한다.

Page<Member> page = memberRepository.findByAge(age, pageRequest);

Page<MemberDto> toMap = page.map(member -> new MemberDto(member.getId(), member.getUsername(), member.getTeam().getName()));

벌크성 수정 쿼리

JPQL 방식

public int bulkAgePlus(int age) {
    return em.createQuery("update Member m set m.age = m.age + 1 where m.age >= :age")
             .setParameter("age", age)
             .executeUpdate();
}

Spring Data JPA 방식

@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
  • 반환 타입은 int 로 해야한다.
  • @Modifying 어노테이션이 있어야 executeUpdate() 로 업데이트 된 개수를 반환한다.
    해당 어노테이션을 안쓰면 getSingleResult() 로 처리되어 InvalidDataAccessApiUsageException 에러가 발생한다.

JPA 에서 벌크연산 사용 시 주의할 점

memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 19));
memberRepository.save(new Member("member3", 20));
memberRepository.save(new Member("member4", 21));
memberRepository.save(new Member("member5", 40));

int resultCount = memberRepository.bulkAgePlus(20); // 벌크 연산

Member member = memberRepository.findByUsername("member5");
assertThat(member.getAge()).isEqualTo(41);

member 1 ~ 5 는 save 를 통해 영속성 컨텍스트에 저장이 된다.
하지만 벌크연산은 영속성 컨텍스트를 통해 update 처리가 되는 것이 아니라, 직접 DB에 update 쿼리를 날린다.

member5 의 회원을 조회 시 영속성 컨텍스트에 담긴 회원의 나이는 40이다.
하지만 DB는 member5 의 나이가 41 로 반영되어 있다.

벌크연산 문제 해결법

벌크 연산 수행 후 회원을 조회하기 전에, 영속성 컨텍스트의 내용을 전부 날리면 된다.

memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 19));
memberRepository.save(new Member("member3", 20));
memberRepository.save(new Member("member4", 21));
memberRepository.save(new Member("member5", 40));

int resultCount = memberRepository.bulkAgePlus(20); // 벌크 연산

em.clear();

Member member = memberRepository.findByUsername("member5");
assertThat(member.getAge()).isEqualTo(41);

참고 ) save 로 영속성 컨텍스트에 저장되어 있는 회원은 update 쿼리를 만나면 (벌크연산 등..)
em.flush() 를 통해 DB 에 반영을 한 후 update 연산을 진행한다.

Spring Data JPA 는 업데이트 쿼리 후 em.clear() 를 자동으로 나가도록 하는 기능을 제공한다.
@Modifying(clearAutomatically = true)

@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

@EntityGraph

N + 1 문제를 해결하기 위해 fetch join 을 통해서 해결을 하게 된다.
Spring Data JPA 에서는 @EntityGraph 를 이용하여 간단하게 fetch join 을 할 수 있다.

@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();

만약 JPQL 을 작성하면서 fetch join 을 쓰고 싶다면 아래와 같이 할 수 있다.

@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();

메서드 이름으로 fetch join 을 쓰고 싶다면 아래와 같이 할 수 있다.

@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(@Param("username") String username);

참고 ) @NamedEntityGraph() 라는 것으로도 할 수 있는데, 잘 사용하지는 않음.

영한님의 경우, 간단한 조인의 경우에는 @EntityGraph 를 사용하고,
복잡한 쿼리의 경우에는 JPQL 을 사용한다고 하심.

JPA QueryHints

@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);

하이버네이트가 제공하는 기능으로, 해당 쿼리를 조회용으로만 쓸 때 성능 최적화를 위해 사용한다.

기존에는 조회 쿼리를 날리면 더티 체킹을 위해 스냅샷을 등록해야 해서 결국 객체 2개를 만들어야 한다.
해당 쿼리를 조회용으로만 쓰게된다면 결국 자원 낭비이기 때문에 QueryHints 를 통해 성능 최적화를 하는 것이다.

그러면 조회용으로만 쓰는 메서드에는 전부 Hint 를 남겨야 할까?

전체 100 중에 성능에 영향을 미치는 것은 정말 복잡한 쿼리가 잘못나가서 나가는 경우이다.
단순 조회용으로 쓰는 쿼리에 Hint 를 남긴다고해서 드라마틱하게 성능 향상이 되지는 않는다.
따라서 성능 테스트를 충분히 해보고 Hint 를 적용할지를 고민해야한다.

JPA Lock

select for update
select 할 때 다른 애들은 update 하지 못하도록 하게 함

@Lock(LockModeType.PESSIMISTIC_WRITE)
Member findLockByUsername(String username);

해당 메서드를 실행하면 아래 쿼리가 나간다.

select
    member0_.member_id as member_i1_0_,
    member0_.age as age2_0_,
    member0_.team_id as team_id4_0_,
    member0_.username as username3_0_ 
from
    member member0_ 
where
    member0_.username=? for update

실시간 트래픽이 많은 서비스에서는 가급적 Lock 을 걸면 안된다.
(Optimastic Lock (실제 Lock 을 거는게 아니라 버저닝 메커니즘으로 해결하는 방법) 이나
Lock 을 걸지 않는 다른 방법으로 적용해야한다.)

돈과 관련된 중요한 정보에 Lock 을 사용함

참고

  • 실전! 스프링 데이터 JPA
반응형

댓글