본문 바로가기
Dev/Spring Boot

[실전! 스프링 데이터 JPA] ch 1 ~ ch 4요약정리

by ulmu 2024. 3. 14.

TIP

@BeforeEach
    private void member_maker(){
        for(int i = 0; i < 10; i++) {
            Member member = new Member("member" + i, 20 + i);
            memberRepository.save(member);
        }
    }

 

 

쿼리 파라미터 로그 남기기

    logging.level:
      org.hibernate.SQL: debug
      org.hibernate.type: trace
  • 예를 들면, 다음과 같은 코드가 있다고 가정하자.
    String jpql = "SELECT u FROM User u WHERE u.name = :name";
    List<User> result = entityManager.createQuery(jpql, User.class)
        .setParameter("name", "루피")
        .getResultList();
  • yml에 위 설정을 추가해주면, 해당 쿼리가 실행될 때, 어떤 값("루피")이 들어있는지 로그로 함께 출력된다.
  • 단 로그레벨을 trace로 설정하면, 더 많은 로그가 출력 -> 운영환경에선 성능테스트 후 도입
  • yml 추가(org.hibernate.type: trace)하거나 라이브러리 의존성 추가하거나
    https://da-y-0522.tistory.com/86#1%EF%B8%8F%E2%83%A3%EB%B0%A9%EB%B2%95%201%20-%20yml%20%EC%BD%94%EB%93%9C%20%EC%B6%94%EA%B0%80-1

 

 

 

로그가 많으면 안 좋나요?

  1. 성능 저하: 로그 많다 -> 파일 I/O 작업 증가 -> 시스템 성능 저하
    (서버가 로그를 기록하는 데 시간과 자원을 더 많이 소비 - 실제 비즈니스 로직을 처리하는 데 필요한 성능에 영향)
  2. 디스크 공간 소비: 로그 파일은 저장 공간을 차지 -> 과도한 로깅은 디스크 공간을 빠르게 소진 -> 디스크가 가득 차서 시스템이 제대로 작동X
  3. 로그 관리의 복잡성: 문제 해결이나 시스템 모니터링을 위해 로그를 분석할 때 어려움
  4. 보안 및 개인정보 문제: 로그 레벨이 너무 높으면 민감한 데이터가 로그에 기록될 수 있으며, 이는 정보 유출로 이어질 수 있습니다.

 

 

엔티티 클래스 관련 정리

@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {

    private Long id;
    private String username;
    private int age;
    private Team team;

    public Member(String username){
        this(username, 0);
    }

   ...
    public void changeTeam(Team team){
        this.team = team;
        team.getMemberList().add(this);
    }
}
  • @NoArgsConstructor: 기본 생성자 자동 생성
  • access = AccessLevel.PROTECTED : 기본 생성자 접근 수준을 protected (동일 클래스 or 패키지/ 다른 패키지 내 자식 클래스)
  • @ToString(of = {"id", "username", "age"}) : 클래스의 toString()메소드 자동 생성 / 가급적 연관관계 없는 내부 필드로만
  • @ToString 용도: 엔티티 상태 로깅 및 디버깅할 때, 지금 값이 무엇이 들어있는지 확인
  • of 속성으로 메소드에 포함할 필드 지정
  • (연관관계 편의메서드)Team에도 이 member를 넣고 싶을 때:  .add(this)
  • Member와 Team은 양방향 연관관계 - FK가진 Member.team이 연관관계 주인
     => so 여기서(Member.team)만 외래키 값 변경, Team.members는 읽기만 가능 

 

 

Entity가 기본생성자를 필요로 하는 이유
 JPA가 DB 값을 자바 객체로 가져와서 쓰려할 때 기본생성자를 통해 객체 먼저 만들고, Reflection을 통해 필드 값을 주입하기 때문

 

 

쿼리 메소드 필터 조건

스프링 데이터 JPA 공식문서 참조: https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html 

 

JPA Query Methods :: Spring Data JPA

As of Spring Data JPA release 1.4, we support the usage of restricted SpEL template expressions in manually defined queries that are defined with @Query. Upon the query being run, these expressions are evaluated against a predefined set of variables. Sprin

docs.spring.io

 

 

쿼리 메소드 사용시 주의점

  • 엔티티 필드명 변경되면, 인터페이스에 정의한 메서드 이름도 반드시 변경해야함
  • 애플리케이션 로딩 시점에 오류를 인지할 수 있는 것이 스프링 데이터 JPA의 매우 큰 장점

 

 

동적 쿼리 VS 정적 쿼리

//동적쿼리
public class MemberRepository{

	 public List<Member> findByUsername(String name){
        return em.createQuery("select m from Member m where m.username = :name", Member.class)
                .setParameter("username", name)
                .getResultList();
    }
}

 

//정적쿼리
@Entity
@NamedQuery(
	name="Member.findByUsername",
	query="select m from Member m where m.username = :username")
public class Member{
 ...
}


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

 

 

 

스프링 데이터 JPA로 Named 쿼리 호출(이론)

public interface MemberRepository extends JpaRepository<Member, Long>{

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

}
  • @Query 생략하고 메서드 이름만으로 Named 쿼리 호출 가능 
  • 스프링 데이터 JPA는 선언한 "엔티티 클래스 + .(점) + 메서드 이름"으로 Named 쿼리 찾아서 실행
  • 실행할 Named 쿼리 없으면 메서드 이름으로 쿼리 생성 전략 사용
  • 필요에 따라 전략 변경 가능 -> 권장 X

 

 

스프링 데이터 JPA로 Named 쿼리 호출(실무)

public interface MemberRepository extends JpaRepository<Member, Long> {

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

}
  • 실무에서 NamedQuery를 직접 등록해서 사용하는 일은 드물다.
  • 대신 @Query를 사용해서 리파지토리 메소드에 쿼리를 직접 정의한다.
  • JPA named 쿼리처럼 애플리케이션 실행 시점에 문법 오류 발견 가능
  • 실무에선 주로 이 방법을 활용해 파라미터가 많은 메서드 해결

 

 

@Query: 값 조회

@Query("select m.username from Member m")
List<String> findUsernameList();
  • JPA 값타입(@Embedded)도 이 방식으로 조회 가능

 

@Query: DTO 조회

@Query("select new com.study.datajpa.dto.MemberDto(m.id, m.username, t.name)" +
       "from Member m join m.team t")
List<MemberDto> findMemberDto();
  • DTO로 조회하려면 JPA의 new 명령어 사용할 것

 

 

컬렉션 파라미터 바인딩

@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);
  • Collection 타입으로 in 절 지원
  • where in 절
    : SELECT column_names
      FROM table_name
      WHERE column_name IN (value1, value2, ..., valueN);

 

 

반환 타입

 - 컬렉션: 반환 타입 컬렉션(결과 있음) / 빈 컬렉션 (결과 없음)

 - 단 건 조회: null (결과 없음) / 반환 타입 (결과 1개 있음) / NonUniqueResultException(결과 2개 이상)
 - 단건 조회 ex) Member findByUsernmae() / Optional<Member> findByUsernmae()

 

 

offset

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", Member.class)
            .setParameter("age",age)
            .setFirstResult(offset)
            .setMaxResults(limit)
            .getResultList();
}

 

  • .setFirstResult() : 조회할 결과의 시작 위치 ( offset 0 : 맨처음 데이터 부터 / offset 10 : 11번째 데이터부터 )
  • offset: 반환될 결과 목록의 시작 시점

 

스프링 데이터 JPA 페이징과 정렬

  • Page<Member> findByAge(int age, Pageable pageable);
    - Page 인터페이스 반환하는 메서드 2가지 쿼리 실행
    - 데이터 조회 쿼리: Pageable 객체에 정의된 페이징 조건(예: offset, limit)에 맞는 데이터 조회
    - count 쿼리: 전체 데이터 수 계산 -> "전체 count 쿼리는 꽤 무겁다"
  • Slice<Member> findByAge(int age, Pageable pageable);
    : 요청된 페이지의 데이터 + 다음 페이지 여부 알려줌
  • List<Member> findByAge(int age, Pageable pageable);
    : 해당 페이지 데이터만 조회 
  • List<Member> findByAge(int age, Sort sort);
    : 특정 기준(sort 객체)에 따라 정렬된 전체 데이터 목록 조회 / 페이징 처리는 적용X

 

 

Pageable과 PageRequest

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


//테스트 코드
@Test
    public void page_test(){
        for(int i = 1; i <5; i ++){
            Member member = new Member("최유림"+i, i*10);
            memberRepository.save(member);
        }

        PageRequest pageRequest
                = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

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

        for (Member member : page ){
            System.out.println("member: " + member);
        }
    }
  • Pageable에 들어갈 것 = PageRequest 객체
  • PageRequest 생성자: 현재 페이지, 조회할 데이터 수, 정렬정보
  • Page는 0부터 시작

 

 

Count 쿼리 분리하기

//스프링 데이터 JPA로 호출한 Count 쿼리
@Query("select count(m) from Member m")
long count();

 

@Query(value = "select m from Member m",
countQuery = "select count(m.username) from Member m")
Page<Member> findMemberAllCountBy(Pageable pageable);
  • 회원(Member) 엔티티 목록 조회 + 페이징 처리를 위해 별도의 count 쿼리 사용
  • 주로 복잡한 sql에서 사용, 데이터는 left join, 카운트는 left join 안해도 됌)

 

 

Top, First 사용 참고 자료

List<Member> findTop3ByOrderByAgeDesc();
List<Member> findFirstByOrderByAgeDesc();
  • findTopByOrderBy / findFirstByOrderBy (By 2번 써야함)

 

 

leftjoin

  • SELECT m, t FROM Member m JOIN m.team t : team에 속해있는 member만 조회 가능
  • SELECT m, t FROM Member m LEFT JOIN m.team t : 팀의 소속여부없이 일단 member 모두 조회
    - 모든 Member가 어떤 Team에 속해 있다 -> 해당 Member와 그들이 속한 Team의 정보가 결과에 포함
    - 어떤 Member가 어떤 Team에도 속하지 않는 경우 -> 해당 Member의 정보는 결과에 포함되지만, Team 관련 정보는 null로 표시

 

 

스프링부트 3 - 하이버네이트 6 left join 최적화 

  • 스프링 부트 3 이상 = 하이버네이트 6 적용 = 의미없는 left join 최적화
  • left join을 써는데 쿼리에서 left join 하는 대상 안쓴다 -> "응 left join 안써~"
@Query(value = "select m from Member m left join m.team t)
Page<Member> findByAge(int age, Pageable pageable);
  • 위 @Query = "select m from Member m"
  • 만약 select/where 절에 member가 없더라도 Member와 Team을 하나의 SQL로 조회하고 싶다.
    -> "select m from Member m left join fetch m.teat t"
  • Left join fetch와의 차이점
    - LEFT JOIN만 사용하는 경우: 관계를 통해 로드된 엔티티는 지연 로딩(Lazy Loading) 적용 -> 실제 Team 엔티티 데이터가 필요한 시점까지 데이터 로딩 지연
    - LEFT JOIN FETCH: JPQL 쿼리 실행시 관련된 엔티티를 즉시 로딩 -> 성능 최적화 / N+1문제 해결에 도움

 

 

벌크성 수정 쿼리

  • 벌크(Bulk): 대량의 데이터
  • 벌크성 수정 쿼리(Bulk Update Query): 데이터베이스의 여러 레코드를 한 번의 연산으로 수정하는 쿼리
//JPA를 사용한 벌크성 수정 쿼리
public int bulkAgePlus(int age){
        int resultCount = em.createQuery(
                "update Member m set m.age = m.age + 1" +
                        "where m.age >= :age")
                .setParameter("age", age)
                .executeUpdate();
        return resultCount;
    }

 

 

@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
  • 벌크성 수정, 삭제 쿼리는 @Modifying 사용 -> 안 사용할 시 예외(~ Not supported for DML operations) 발생
  • @Modifying(clearAutomatically = true) : 이 옵션 없이 findById로 다시 조회하면 영속성 컨텍스트에 과거 값 남음 -> 초기화!
  • 벌크 연산은 영속성 컨텍스트 무시하고 실행 -> so 영속성 컨텍스트에 있는 엔티티의 상태 != DB에 있는 엔티티의 상태
    - 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산 실행할 것
    - 부득이하게 영속성 컨텍스트에 엔티티가 있는 경우, 연산직후 영속성 컨텍스트 초기화

 

 

페치 조인의 필요성: JPA N+1문제

public interface MemberRepository extends JpaRepository<Member, Long> {
	List<Member> memberList = memberRepository.findAll();
}

//위 코드에 대한 쿼리문이 2개 실행된다.
//쿼리문1
Hibernate: 
    select
        m1_0.member_id,
        m1_0.age,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0
//쿼리문 2
Hibernate: 
    select
        t1_0.team_id,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.team_id=?
  • 우리 생각: 엥 뭔가 쿼리문 2만 실행되면 되는 거 아냐? 쿼리문 1은 뭐야?
  • Member엔티티의 team 필드에 대한 지연 로딩
    - 지연로딩: 필요할 때 부를게(Member 먼저 부르고, Team이 필요할 때 부를게~)
    -> 쿼리문 1: 모든 Member를 조회하는 쿼리
    -> 쿼리문 2: Member의 Team 정보에 접근하기 위한 쿼리
    -> JPA N+1 문제
  • 해결법: 페치 조인 = "아! 그냥 한번에 부를게요 ^^"
@Query("SELECT m FROM Member m JOIN FETCH m.team")
List<Member> findAllWithTeam();

 

 

@EntityGraph

  • 연관된 엔티티 SQL 한번에 조회하는 방법: 페치 조인
  • EntityGraph: 페치 조인의 간편 버전 - LEFT OUTER JOIN(=LEFT JOIN) 사용
  • JPQL 없이 패치조인 사용 가능
@Override
@Query(value = "select m from Member m left join fetch m.team")
List<Member> findAll();
  
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();

 

 

 

NamedEntityGraph 사용방법

//엔티티 클래스
@Entity
@NamedEntityGraph(name = "Member.all", attributeNodes = 
@NamedAttributeNode("team"))
public class Member {
}

//jpa repository
@EntityGrahp("Member.all")
@Query("select m from Member m")
List<Member> findMemberEntityGraph();

 

 

 

JPA 쿼리 힌트(Query Hint)
  - JPA 쿼리 실행 시 JPA 구현체(예: Hibernate)에 추가적인 동작 지시를 제공하는 메커니즘
  - JPA 쿼리 힌트는 JPA 제공자에게만 / SQL 자체에 직접 쓰이지X
   ex) 쿼리 결과 캐싱, 쿼리 실행 계획의 재사용, 특정 쿼리에 대한 fetch size 지정 등

@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
  • 쿼리의 결과로 반환되는 엔티티가 읽기 전용
  • @QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
    : 해당 쿼리의 실행 결과로 반환되는 엔티티가 변경되지 않음을 가정하고 최적화
    -> 즉, Hibernate는 이 엔티티에 대해 스냅샷 을 생성하지 않음 -> 플러시(flush) 시점에 이 엔티티의 변경 사항을 검사X -> 성능 ↑

 

 

 

JPA 쿼리 힌트 (Query Hint) Page

@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly",value = "true")},
            forCounting = true)
Page<Member> findByUsername(String name, Pageable pageable);
  • @QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
    : 해당 쿼리의 실행 결과로 반환되는 Page<Member>는 읽기 전용이야
  • forCounting = true : 페이징을 위한 count 쿼리도 쿼리 힌특 적용 쓸게(읽기전용으로 할게~)

 

Lock

@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);

 

  • 락 모드: 데이터베이스 레벨에서 특정 엔티티에 대한 동시성 제어 및 데이터의 일관성과 격리 수준 유지에 도움
  • @Lock(LockModeType.PESSIMISTIC_WRITE) : 해당 쿼리를 통해 조회된 엔티티에 대해 비관적 락 검
  • 비관적 락: 데이터를 읽고 변경할 때 다른 트랜잭션이 해당 데이터에 동시 접근하는 것 방지(단, 데드락 위험성 O)
  • 데드락: 두 개 이상의 트랜잭션이 서로의 작업이 끝나기를 무한히 기다리게 되어, 결국 어느 쪽도 진행할 수 없게 되는 상황

 

댓글