코알못

[JPA] JPA 정복하기 - 03. JPA 기본 사용 방법 (쿼리 메소드, JPQL, Paging, 벌크성 쿼리 제출 방법, @EntityGraph, JPQL fetch join, JPA Hint, Lock) 본문

JAVA

[JPA] JPA 정복하기 - 03. JPA 기본 사용 방법 (쿼리 메소드, JPQL, Paging, 벌크성 쿼리 제출 방법, @EntityGraph, JPQL fetch join, JPA Hint, Lock)

코린이s 2024. 1. 14. 19:01
728x90

Spring Data JPA 의 쿼리 메소드, JPQL, Paging, 벌크성 쿼리 제출 방법, @EntityGraph, JPQL fetch join, JPA Hint, Lock 대해서 알아보자!

우선 쿼리를 제출하는 방법은 세가지가 있다.

  1. 메소드 이름으로 쿼리 생성
  2. JPA Named Query
  3. JPQL

예시를 보며 차근차근 보자!

1. 메소드 이름으로 쿼리 생성

@Repository
public class MemberJpaRepository {
    @PersistenceContext
    private EntityManager em;
    
	public List<Member> findByUsernameAndAgeGreaterThan(String username, int age){
        return em.createQuery("select m from Member m where m.username = :username and m.age > :age")
                .setParameter("username", username)
                .setParameter("age", age)
                .getResultList();
    }
}

Spring Data JPA를 사용하면 아래와 같이 하면 동일 동작한다.

public interface MemberRepository extends JpaRepository<Member, Long> {

    List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}

더 많은 기능은 https://spring.io/ 사이트에서 

'Doc' 을 눌러서 확인 하면 된다.

만약 엔티티명과 변수명이 다르다면 어플리케이션 기동 시점에 오류 발생한다.

2. JPA Named Query

기존 JPA Named Query의 경우 아래와 같이 Entity에 쿼리를 작성하고 repository에서 정의한 Name으로 사용한다.

@Entity
@NamedQuery(
        name="Member.findByUsername",
        query="select m from Member m where username = :username and age=20"
)
public class Member {
..
}

repository에서 순수 JPA에서는 아래와 같이 사용하며 만약 NamedQuery에 정의한 명칭이 없다면 에러 발생한다.

public List<Member> findByUsername(String username){
        return em.createNamedQuery("Member.findByUsername", Member.class)
                .setParameter("username", username)
                .getResultList();
    }

 

Spring Data JPA 의 경우 아래와 같이 사용하면 되며 만약에 NamedQuery에 정의한 명칭이 없다면 아래 JPA 쿼리 메소드 기능으로 쿼리 실행 한다. 

@Query(name = "Member.findByUsername")
    List<Member> findByUsername(@Param("username") String username);

언제 사용하나! 쿼리를 직접 작성해야 할때(예시로 복잡한 쿼리를 작성해야 하여 메소드명으로 쿼리 작성하는것 보다 쿼리를 직접 작성하는 것이 더 나을때 등) 사용 시점이 아닌 어플리케이션 로딩 시점에 쿼리 버그가 있을시 오류가 발생하므로 일찍 버그를 찾을 수 있다는 장점이 있다. 그러나 아래 JPQL 기능도 동일하게 제공하기 때문에 Named query 는 잘 사용안하고 JPQL을 사용한다.

3. JPQL

Spring Data JPA 에서는 아래와 같이 사용한다.

@Query("select m from Member m where m.username = :username and m.age = :age")
    List<Member> findUser(@Param("username") String useranme, @Param("age") int age);

만약 Entity가 아닌 Dto로 받고 싶다면 어떻게 해야할까?

Dto를 생성한뒤

package study.datajpa.dto;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class MemberDto {
    private Long id;
    private String username;
    private String teamname;

    public MemberDto(Long id, String username, String teamname) {
        this.id = id;
        this.username = username;
        this.teamname = teamname;
    }
}

받기 위해서는 new 오퍼레이션을 사용하여 아래와 같이 repository에 작성하면 된다.

    @Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
    List<MemberDto> findMemberDto();

만약 아래와 같이 new 오퍼레이션을 사용하지 않으면

    @Query("select m.id, m.username, t.name as teamname from Member m join m.team t")
    List<MemberDto> findMemberDto();

아래와 같이 converter 오류 발생한다.

org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap] to type [study.datajpa.dto.MemberDto]

	at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:294)
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:185)
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:165)
	at org.springframework.data.repository.query.ResultProcessor$ProjectingConverter.convert(ResultProcessor.java:305)
	at org.springframework.data.repository.query.ResultProcessor$ChainingConverter.lambda$and$0(ResultProcessor.java:233)
	at org.springframework.data.repository.query.ResultProcessor$ChainingConverter.convert(ResultProcessor.java:240)
	at org.springframework.data.repository.query.ResultProcessor.processResult(ResultProcessor.java:160)

 

그러나 이곳 저곳에서 사용하기 위해서는 Entity로 받고 사용할때 stream 의 map 기능을 이용해서 List<Dto> 로 변환해서 사용하는것이 좋을것 같아 보인다.

추가로 in 쿼리는 어떻게 사용할까? 아래와 같이 @Param을 이용하여 파라미터 명을 지정하고 ':' 뒤에 파라미터명을 쓰면 된다.

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

이제 반환 타입에 대해서 알아보자!

단건, 컬렉션, Oprional 모두 가능하다. (여러 기능이 있으나 우선 자주 쓰는 세가지 타입 먼저 보자)

아래와 같이 메소드 명으로 find[List or EntiryName or Optional]By 를 사용하면 된다.

List<Member> findListByUsername(String username);
Member findMemberByUsername(String username);
Optional<Member> findOptionalByUsername(String username);

그러나 여기서 데이터가 없다면 Member는 null, Optional 제공하는 기능 isEmpty로 비교하면 되나 List 는 null이 아니고 size가 0 으로 나오기 때문에 데이터 체크를 null로 체크하면 안된다.(null 아니고 size가 0이 아닌것으로 체크 필요)

그렇기에 데이터가 없을수 있다고 하면 Optional 로 받아서 처리하는것이 나아보인다.

이제 페이징과 정렬 기능을 알아보자!

그전에 순수 JPA로 구현해보자!

아래와 같이 페이징을 위해 필요한 repository 생성하며

페이지에 맞는 데이터를 조회하는 findByPage의 매개변수 offset은 페이지 번호(0부터 시작), limit 는 페이지에 보여줄 데이터수이다.

그리고 전체 데이터 수를 알 수 있는 getTotalCount는 아래와 같이 만든다.

public List<Member> findByPage(int age, int offset, int limit){
        return em.createQuery("select m from Member m where m.age = :age order by m.username desc")
                .setParameter("age", age)
                .setFirstResult(offset)
                .setMaxResults(limit)
                .getResultList();
    }

    public long getTotalCount(int age){
        return em.createQuery("select count(m) from Member m where m.age = :age", Long.class)
                .setParameter("age", age)
                .getSingleResult();
    }

이제 테스트 코드를 작성하면 정상적으로 통과 한다.

클라이언트는 전체 데이터수에 따라서 offset, limit 를 계산해서 findByPage 를 호출하면 페이징을 구현할 수 있다.

@Test
    public void page(){
        int memberCount = 5;
        for(int i = 0 ;i < memberCount;i++){
            memberJpaRepository.save(Member.builder()
                    .username("member")
                    .age(10)
                    .build());
        }
        int age = 10;
        int offset = 0;
        int limit = 3;
        List<Member> members = memberJpaRepository.findByPage(age,offset,limit);
        long totalCount = memberJpaRepository.getTotalCount(age);

        assertEquals(members.size(), limit);
        assertEquals(totalCount, memberCount);

    }

이제 Spring Data JPA로 구현해보자!

repository 에 아래 하나만 정의하면 getTotalCount 작성하지 않아도 된다...!!

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

테스트를 진행해보자!

@Test
    public void page(){
        int memberCount = 5;
        for(int i = 0 ;i < memberCount;i++){
            memberRepository.save(Member.builder()
                    .username("member")
                    .age(10)
                    .build());
        }
        int age = 10;
        int offset = 0;
        int limit = 3;
        PageRequest pageRequest = PageRequest.of(offset,limit, Sort.by(Sort.Direction.DESC, "username"));
        Page<Member> page = memberRepository.findByAge(age, pageRequest);

        List<Member> members = page.getContent();
        long totalCount = page.getTotalElements();
        assertEquals(members.size(), limit);
        assertEquals(totalCount, memberCount);
    }

Page 로 받게 되면 아래와 같은 함수도 제공한다.

메소드 설명
page.getTotalPages() 전체 페이지 수 (현재 테스트 기준 2)
page.getNumber() 현재 페이지 넘버 (현재 테스트 기준 0)
page.isFirst() 첫번째 페이지 여부 (현재 테스트 기준 true, offset이 1이면 false)
page.hasNext() 다음 페이지 존재 여부 (현재 테스트 기준 true, offset이 1이면 false)

추가로 Page 에서도 map 기능을 제공하고 있어 위에서 정리한바와 같이 Page<Dto>로 만들기 보다는 이곳저곳 사용하기 위해서 Page<Entity>로 받고 page.map(m -> new Dto()) 로 데이터 변환해서 사용하면 좋을 것 같다.

데이터를 Slice 로 받을수 있는데 이는 페이지 번호를 선택하여 페이징 하는것이 아닌 [더보기] 눌러서 페이지를 펼치는 경우라면 다음 데이터가 있는지 보는 정도만 체크하면 되니 Slice를 사용하면 된다! (Slice가 totalcount 쿼리가 안나가기에 page 보다 부하가 적다.)

아래와 같이 정의하며 제공하는 메서드는 page와 동일하며 totalcount 메소드만 제공하지 않는다.

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

다음페이지 체크만 하면 되니 totalcount를 조회하지 않으며 limit 카운트에 +1 하여 조회하여(예시는 3으로 요청했으니 4개 조회) 다음 데이터가 있다면 다음 페이지가 있다고 리턴한다.

이처럼 용도에 맞게 사용하면 된다.

JPQL도 동일하게 Page 사용 가능한데 만약 카운트 쿼리를 다르게 작성하고 싶다면 아래와 같이 작성 가능하다.

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

이제 Bulk Insert 에 대해서 알아보자!

우선 순수 JPA 로 작성하면 아래와 같이 repository에 작성하면 되고 

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();
    }

테스트 코드로 돌려보면 정상적으로 수행 되는것을 볼 수 있다.

@Test
    void bulkAgePlus() {
        for(int i = 20 ;i < 31;i++){
            memberJpaRepository.save(Member.builder()
                    .username("member")
                    .age(i)
                    .build());
        }
        int resutlCount = memberJpaRepository.bulkAgePlus(25);
        assertEquals(6, resutlCount);
    }

이제 Spring Data Jpa 를 사용하는 방법을 알아보자!

repository에는 아래와 같이 @Modifying을 해야 순수 JPA 에서 조회, 업데이트에 따라 메소드(getResultList, executeUpdate)를 다르게 쓰듯이 조회인지 업데이트인지 구분이 가능하다.

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

이제 테스트를 통해 값을 찍어보자..

 @Test
    void bulkAgePlus() {
        for(int i = 20 ;i < 31;i++){
            memberRepository.save(Member.builder()
                    .username("member"+i)
                    .age(i)
                    .build());
        }
        int resultCount = memberRepository.bulkAgePlus(25);
        System.out.println("[resultCount]"+resultCount);
        Member member = memberRepository.findByUsername("member30").stream().findFirst().get();
        System.out.println("[getAge]"+member.getAge());
    }

아래와 같이 11, 30이 나온다... 우리가 예상한 값은 plus 된값 31인데 30이 나왔다. 

[resultCount]11
2024-01-14T15:59:33.093+09:00 DEBUG 8851 --- [           main] org.hibernate.SQL                        : 
    select
        m1_0.member_id,
        m1_0.age,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0 
    where
        m1_0.username=?
2024-01-14T15:59:33.094+09:00 TRACE 8851 --- [           main] org.hibernate.orm.jdbc.bind              : binding parameter (1:VARCHAR) <- [member30]
[getAge]30

이는 순수 JPA 에서도 동일하게 발생한다.

JPA의 영속성 context 때문에 발생하는 이슈로

트랜젝션이 끝나기 전까지 영속성 컨텍스트에 저장하고 조회하나 bulk Insert 의 경우 영속성 컨텍스트 개념 상관없이 DB에 바로 Insert 해버리기에 조회시 영속성 컨텍스트에는 반영되지 않았기에 변경되지 않은 데이터 30이 나오는것이다.

그렇기때문에 bulk Insert 뒤에 트랜젝션 끝나지 않은 상태에서 조회 하는 쿼리가 있다면 bulk Insert 이후에 영속성 컨텍스트에 있는 데이터를 제거(clear)해서 DB 데이터를 조회 할 수 있도록 해야한다.(flush 는 따로 하지 않았는데 JPQL실행전에는 flush 자동으로 하기 때무에 따로 하지 않았으며 JPQL 이 아니라면 flush 까지 진행하도록 한다.)

@PersistenceContext
    EntityManager em;
    
@Test
    void bulkAgePlus() {
        for(int i = 20 ;i < 31;i++){
            memberRepository.save(Member.builder()
                    .username("member"+i)
                    .age(i)
                    .build());
        }
        int resultCount = memberRepository.bulkAgePlus(25);
        System.out.println("[resultCount]"+resultCount);
        em.clear(); // 영속성 컨텍스트에 있는 데이터 제거하여 DB에 반영
        // 영속성 컨텍스트에 데이터가 없기에 DB에서 데이터 조회하여 정상적으로 31이 나옴
        Member member = memberRepository.findByUsername("member30").stream().findFirst().get();
        System.out.println("[getAge]"+member.getAge());
    }

이제 정상적으로 데이터가 나온다.

    select
        m1_0.member_id,
        m1_0.age,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0 
    where
        m1_0.username=?
2024-01-14T16:42:38.614+09:00 TRACE 13109 --- [           main] org.hibernate.orm.jdbc.bind              : binding parameter (1:VARCHAR) <- [member30]
[getAge]31

Spring Data JPA 를 사용하면 아래와 같이 clearAutomatically 를 true 로 옵션주면 em.clear 기능을 수행해준다.

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

clear 제거후 테스트 해보면

@Test
    void bulkAgePlus() {
        for(int i = 20 ;i < 31;i++){
            memberRepository.save(Member.builder()
                    .username("member"+i)
                    .age(i)
                    .build());
        }
        int resultCount = memberRepository.bulkAgePlus(25);
        System.out.println("[resultCount]"+resultCount);

        Member member = memberRepository.findByUsername("member30").stream().findFirst().get();
        System.out.println("[getAge]"+member.getAge());
    }

결과는 정상적으로 31이 나온다.

    where
        m1_0.username=?
2024-01-14T17:10:56.666+09:00 TRACE 16140 --- [           main] org.hibernate.orm.jdbc.bind              : binding parameter (1:VARCHAR) <- [member30]
[getAge]31

이제 @EntityGraph 에 대해 알아보자!

우선 데이터를 넣고 조회를 해보자!

[이전 시간]에 우리는 EAGER의 경우 사용시 문제가 있으니 fetch type을 LAZY 로 변경하여 사용할 시점에만 데이터를 조회하도록 하도록 했기에 아래와 같이 변경한뒤

package study.datajpa.entity;

import jakarta.persistence.*;
import lombok.*;

@Setter
@ToString(of = {"id","username","age"}) // 나중에 toString 으로 데이터 확인하기 위함 > 정의한 값만 출력
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Entity
public class Member {
  ...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id") // FK
    private Team team;
.....
}

 영속성 컨텍스트가 아닌 DB 데이터 조회하는것을 보기 위해 flush, clear 를 진행하고 조회한다.

@Test
    public void testGet(){
        Team teamA = Team.builder()
                .name("teamA").build();
        Team teamB = Team.builder()
                .name("teamB").build();
        teamRepository.save(teamA);
        teamRepository.save(teamB);
        Member member1 = Member.builder()
                .username("member1")
                .age(10)
                .team(teamA)
                .build();
        Member member2 = Member.builder()
                .username("member2")
                .age(10)
                .team(teamB)
                .build();
        memberRepository.save(member1);
        memberRepository.save(member2);

        em.flush();
        em.clear();
        
        List<Member> members = memberRepository.findAll();

        System.out.println("[members] => "+members.stream().map(s -> s.getTeam().getName()).collect(Collectors.toList()));
        System.out.println("[class]"+members.get(0).getTeam().getClass());
    }

member 를 조회하고 team 데이터를 하나씩 조회 한다. 

[class]class study.datajpa.entity.Team$HibernateProxy$6DCeofqn
2024-01-14T17:46:51.253+09:00 DEBUG 19736 --- [           main] org.hibernate.SQL                        : 
    select
        m1_0.member_id,
        m1_0.age,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0
2024-01-14T17:46:51.271+09:00 DEBUG 19736 --- [           main] org.hibernate.SQL                        : 
    select
        t1_0.team_id,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.team_id=?
2024-01-14T17:46:51.271+09:00 TRACE 19736 --- [           main] org.hibernate.orm.jdbc.bind              : binding parameter (1:BIGINT) <- [1]
2024-01-14T17:46:51.279+09:00 DEBUG 19736 --- [           main] org.hibernate.SQL                        : 
    select
        t1_0.team_id,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.team_id=?
2024-01-14T17:46:51.279+09:00 TRACE 19736 --- [           main] org.hibernate.orm.jdbc.bind              : binding parameter (1:BIGINT) <- [2]
[members] => [teamA, teamB]

여기서 class 정보를 자세히보면 '[class]class study.datajpa.entity.Team$HibernateProxy$6DCeofqn' 라고 되어있는데 Proxy 객체이다.

처음에 member 를 조회 할 때 비어있으니 대리(proxy) 객체를 만들어 두고 사용하게 되면 대리 객체에 데이터를 넣는것이다.

이는 사용할때마다 호출(member 처음에 조회후 team 이름을 출력 하려고 했기에)하는 LAZY 특성때문인데 이를 방지 하기 위해 두가지 방법이 있다.

- JPQL fetch join

- @Entity Graph

우선 JPQL fetch join 은 기존에 작성했던 쿼리에서 테이블 앞에 fetch를 아래와 같이 추가하면 된다.

@Query("select m from Member m left join fetch m.team")
    List<Member> findMemberFetchJoin();

테스트 하면

@Test
    public void testGet(){
        Team teamA = Team.builder()
                .name("teamA").build();
        Team teamB = Team.builder()
                .name("teamB").build();
        teamRepository.save(teamA);
        teamRepository.save(teamB);
        Member member1 = Member.builder()
                .username("member1")
                .age(10)
                .team(teamA)
                .build();
        Member member2 = Member.builder()
                .username("member2")
                .age(10)
                .team(teamB)
                .build();
        memberRepository.save(member1);
        memberRepository.save(member2);

        em.flush();
        em.clear();

        List<Member> members = memberRepository.findMemberFetchJoin();
        
        System.out.println("[class]"+members.get(0).getTeam().getClass());
        System.out.println("[members] => "+members.stream().map(s -> s.getTeam().getName()).collect(Collectors.toList()));
    }

EAGER 처럼 한번에 데이터를 join 해서 한번에 가져오며 그렇기에 Team의 class 명도 프록시 객체가 아니다.

2024-01-14T17:53:39.795+09:00 DEBUG 20429 --- [           main] org.hibernate.SQL                        : 
    select
        m1_0.member_id,
        m1_0.age,
        t1_0.team_id,
        t1_0.name,
        m1_0.username 
    from
        member m1_0 
    left join
        team t1_0 
            on t1_0.team_id=m1_0.team_id
[class]class study.datajpa.entity.Team
[members] => [teamA, teamB]

이렇게 되면 fetch 조인을 위해 쿼리 메소드로 편하게 못쓰고 JPQL로 정의 해야한다.

이를 개선할 수 있는것이 EntityGrapy 이다.

사용해보자!

repository에 아래와 같이 findAll을 재정의 하고 EntityGrapy에 team 테이블을 같이 조회한다고 정의하면 된다.

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

다시 테스트 해보면

@Test
    public void testGet(){
        Team teamA = Team.builder()
                .name("teamA").build();
        Team teamB = Team.builder()
                .name("teamB").build();
        teamRepository.save(teamA);
        teamRepository.save(teamB);
        Member member1 = Member.builder()
                .username("member1")
                .age(10)
                .team(teamA)
                .build();
        Member member2 = Member.builder()
                .username("member2")
                .age(10)
                .team(teamB)
                .build();
        memberRepository.save(member1);
        memberRepository.save(member2);

        em.flush();
        em.clear();

        List<Member> members = memberRepository.findAll();
        System.out.println("[class]"+members.get(0).getTeam().getClass());
        System.out.println("[members] => "+members.stream().map(s -> s.getTeam().getName()).collect(Collectors.toList()));

    }

결과는 fetch join 과 동일하게 나온다.

2024-01-14T18:05:00.952+09:00 DEBUG 21617 --- [           main] org.hibernate.SQL                        : 
    select
        m1_0.member_id,
        m1_0.age,
        t1_0.team_id,
        t1_0.name,
        m1_0.username 
    from
        member m1_0 
    left join
        team t1_0 
            on t1_0.team_id=m1_0.team_id
[class]class study.datajpa.entity.Team
[members] => [teamA, teamB]

이제 JPA Hint 와 Lock 을 알아보자!

JPA Entity 변경후 트랜젝션이 끝나게 되면 변경사항이 DB에 반영이 된다.

아래와 같이 테스트를 해보면

@Test
    public void readOnlyTest(){
        Member member = Member.builder()
                .username("member")
                .age(10)
                .build();
        memberRepository.save(member);
        em.flush();
        em.clear();
        List<Member> members = memberRepository.findByUsername("member");
        members.get(0).setUsername("member2");

        em.flush();
    }

업데이트 쿼리가 따로 나가는것을 볼 수 있다.

   from
        member m1_0 
    where
        m1_0.username=?
2024-01-14T18:57:17.094+09:00 TRACE 27355 --- [           main] org.hibernate.orm.jdbc.bind              : binding parameter (1:VARCHAR) <- [member]
2024-01-14T18:57:17.103+09:00 DEBUG 27355 --- [           main] org.hibernate.SQL                        : 
    update
        member 
    set
        age=?,
        team_id=?,
        username=? 
    where
        member_id=?
2024-01-14T18:57:17.103+09:00 TRACE 27355 --- [           main] org.hibernate.orm.jdbc.bind              : binding parameter (1:INTEGER) <- [10]
2024-01-14T18:57:17.104+09:00 TRACE 27355 --- [           main] org.hibernate.orm.jdbc.bind              : binding parameter (2:BIGINT) <- [null]
2024-01-14T18:57:17.104+09:00 TRACE 27355 --- [           main] org.hibernate.orm.jdbc.bind              : binding parameter (3:VARCHAR) <- [member2]
2024-01-14T18:57:17.104+09:00 TRACE 27355 --- [           main] org.hibernate.orm.jdbc.bind              : binding parameter (4:BIGINT) <- [1]

 

이렇게 데이터 변경을 위해서는 변경되었는지 체크를 위해 원본과 변경 데이터를 따로 저장하게 되어 공간을 차지하게 된다.

만약에 변경이 이뤄지지 않아도 되는 데이터라면 원본을 따로 저장할 필요가 없어지므로 이를 알려주기 위해 JPA Hint 를 이용하여 read only로 정의 할 수 있다.

repository 에 아래와 같이 정의 하고

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

테스트를 해보면 

@Test
    public void readOnlyTest(){
        Member member = Member.builder()
                .username("member")
                .age(10)
                .build();
        memberRepository.save(member);
        em.flush();
        em.clear();
        Member members = memberRepository.findReadOnlyByUsername("member");
        members.setUsername("member2");

        em.flush();
    }

업데이트 쿼리가 나가지 않는것을 볼 수 있다.

2024-01-14T18:55:42.325+09:00 DEBUG 27204 --- [           main] org.hibernate.SQL                        : 
    select
        m1_0.member_id,
        m1_0.age,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0 
    where
        m1_0.username=?
2024-01-14T18:55:42.326+09:00 TRACE 27204 --- [           main] org.hibernate.orm.jdbc.bind              : binding parameter (1:VARCHAR) <- [member]
2024-01-14T18:55:42.350+09:00  INFO 27204 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2024-01-14T18:55:42.352+09:00  INFO 27204 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2024-01-14T18:55:42.358+09:00  INFO 27204 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

이제 Lock 기능을 알아보자!

아래와 같이 Lock을 걸 수 있고

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

테스트 해보면

@Test
    public void lock(){
        Member member = Member.builder()
                .username("member")
                .age(10)
                .build();
        memberRepository.save(member);
        em.flush();
        em.clear();
        
        memberRepository.findLockByUsername("member");
    }

로그를 보면 lock 관련 쿼리가 제출된다.

2024-01-14T18:53:00.461+09:00 DEBUG 26924 --- [           main] org.hibernate.SQL                        : 
    select
        m1_0.member_id,
        m1_0.age,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0 
    where
        m1_0.username=? for update

이처럼 lock 관련 쿼리도 제출 할 수 있으니 사용시점에 lock 관련 기능을 찾아서 맞는걸 이용하면 된다.

끝!

728x90
Comments