TIP
- 어플리케이션 실행도 안 하고 테스트 먼저 돌리면, h2 DB에 엔티티 테이블이 아직 형성되어있지않으므로 테스트 통과 못한다.
- 테스트 도중 발생한 에러 해결1) https://www.inflearn.com/questions/15495/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8F%84%EC%A4%91-%EC%97%90%EB%9F%AC-%EB%B0%9C%EC%83%9D
2) https://olivejua-develop.tistory.com/58
3) 나의 경우, Gradle clean하고 다시 하면 실행되었다.(캐싱 문제) - 테스트에서 반복적으로 실행하고 싶은 건 @BeforeEach 활용
@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
로그가 많으면 안 좋나요?
- 성능 저하: 로그 많다 -> 파일 I/O 작업 증가 -> 시스템 성능 저하
(서버가 로그를 기록하는 데 시간과 자원을 더 많이 소비 - 실제 비즈니스 로직을 처리하는 데 필요한 성능에 영향) - 디스크 공간 소비: 로그 파일은 저장 공간을 차지 -> 과도한 로깅은 디스크 공간을 빠르게 소진 -> 디스크가 가득 차서 시스템이 제대로 작동X
- 로그 관리의 복잡성: 문제 해결이나 시스템 모니터링을 위해 로그를 분석할 때 어려움
- 보안 및 개인정보 문제: 로그 레벨이 너무 높으면 민감한 데이터가 로그에 기록될 수 있으며, 이는 정보 유출로 이어질 수 있습니다.
엔티티 클래스 관련 정리
@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)
- 데드락: 두 개 이상의 트랜잭션이 서로의 작업이 끝나기를 무한히 기다리게 되어, 결국 어느 쪽도 진행할 수 없게 되는 상황
'Dev > Spring Boot' 카테고리의 다른 글
| [실전! 스프링 부트와 JPA 활용1] 7. 웹 계층 개발 정리 (2) | 2024.03.12 |
|---|
댓글