실전! 스프링 데이터 JPA
IntelliJ Lombok 설정
Setting > Annotaion Prosessors 검색 > Enable Annotation Processing 체크
예제 도메인 모델
예제 도메인 모델과 동작 확인
JPA는 기본적으로 Default 생성자, 파라미터가 없는 생성자가 필요
@NoArgsConstructor(access = AccessLevel.PROTECTED)
접근자 레벨 : protected까지 사용
@ToString(of = {"id", "username", "age"})
*연관관계에 있는 내용도 출력할 경우 무한 루프에 빠질 수 있음
em.flush(); // 강제로 DB에 insert 콜 날림
em.clear(); // JPA 영속성 컨텍스트의 캐시 날림
공통 인터페이스 기능
순수 JPA 기반 리포지토리 만들기
Spring Data JPA는 결국 JPA 기술을 활용해서 편하게 사용하는게 목적
JPA 기술 선행이 필수
JPQL은 SQL과 비슷하나, 객체를 대상으로 날리는 Query
Entity 대신에 테이블을 조회 할 때, SQL로 번역 후 실제 DB에서 데이터를 가져옴
공통 인터페이스 설정
JavaConfig 설정
- 스프링 부트 사용시 생략 가능
- 위치 : 프로젝트 생성 시 맨 처음에 만들어진 ‘프로젝트명Application.java’에서 진행
public interface MemberRepository extends JpaRepository<Member, Long>
> Spring Data JPA에서 만든 JPARepository를 상속받은 인터페이스
빌드 시 Spring Data JPA가 JpaRepository를 보고서 스캔하여 구현체 생성
(개발자가 구현체를 만들지 않아도 알아서 만듦)
@Repository
생략 가능
어노테이션의 기능
- 컴포넌트 스캔
- JPA 예외를 어떻게 공통적으로 처리할 수 있는 예외로 변환하는 기능
공통 인터페이스 적용
공통 인터페이스 분석
쿼리 메소드 기능
메소드 이름으로 쿼리 생성
유저 이름과 나이보다 많은 멤버 찾는 로직
순수 JPA의 경우(MemberJpaRepository)
public List<Member> findByUsernameAndAgeGreaterThen(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();
}
> 일일이 Query를 작성해야하는 귀찮음 동반
interface MemberRepository 경우
List<Member> findByUsernameAAndAgeGreaterThan(String username, int age);
> Ultimate 버전에서 찾으려는 조건을 메서드명으로 선언
- 메서드 이름으로 DB에서 데이터 조회
- 찾으려는 property(컬럼) 이름이 다를 경우 오류 발생
- 메서드명 선언은 spring.io > spring data > Document 쿼리 생성 부분 확인
- 파라미터 1~2개까지를 사용해야 적당
JPA NamedQuery
엔티티에서 @NamedQuery 어노테이션 선언
@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username"
)
순수 JPA Repository 사용 시
public List<Member> findByUsername(String username) {
return em.createNamedQuery("Entity에서 NameQuery의 name", Member.class)
.setParameter("username", "회원1")
.getResultList();
}
Spring Data 상속 인터페이스
@Query(name = "Entity에서 NameQuery의 name")
List<Member> findByUsername(@Param("username") String username);
- @Param은 @NamedQueery에서 선언한 Query의 ‘:키워드’ 를 의미
- @Query를 주석처리해도 동작 가능
- 빌드 후 애플리케이션 로딩 시점에 NamedQuery를 파싱 및 확인을 진행하여 오류 발견시 기능 정지 > 오류 찾기 수월
실무에서 거의 사용 x
- 이 기능보다 Repository Method에 바로 Query를 지정할 수 있는 기능이 매우 막강
@Query, 리포지토리 메소드에 쿼리 정의
Spring Data 상속 인터페이스
@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);
실무에서 자주 사용함
- 애플리케이션 로딩할 때 오타 발견시 오류 바로 출력
- 동적 쿼리는 어떻게?
@Query, 값, DTO 조회
단순한 값 조회
@Query("select m.username from Member m")
List<String> findUsername();
원하는 값을 찾기위한 Query 작성과 메서드 이름 선언
DTO 조회
DTO 디렉토리에 Entity의 조회하고 싶은 대상 작성
- 원하는 대상자를 이용한 생성자 작성
- @Data 사용
@Data
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;
}
}
위 Dto를 사용하여 메서드 작성
@Query("select new study.data_jpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team")
List<MemberDto> findMemberDto();
new operation을 사용하여 조회한 데이터를 새로 작성한 DTO entity에 맞는 형식으로 변환
파라미터 바인딩
위치 기반과 이름 기반 중 이름 기반을 사용하는 것을 추천(사실 둘다 사용을 잘 안함)
코드의 가독성과 버그를 줄일 수 있음
컬렉션 파라미터일 경우
반환 타입
순수 JPA 페이징과 정렬
.yml > jpa > properties > hibernate >
dialect : 사용할 DB
쿼리 실행 시 설정한 데이터베이스와 별개로 dialect에 설정한 데이터베이스의 쿼리가 실행
본인 DB에 맞는 dialect 설정이 필요
스프링 데이터 JPA 페이징과 정렬
Page<Member> findByage(int age, Pageable pageable);
메서드 이름으로 query 만들기
테스트코드
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
스프링 데이터 JPA는 페이지를 0부터 시작
Page<Member> page = memberRepository.findByage(age, pageRequest);
@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);
@Query에서 데이터를 뽑는 쿼리와 전체 데이터 수를 뽑는 쿼리를 구분할 수 있다
벌크성 수정 쿼리
순수 JPA
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();
}
스프링데이터 JPA
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
@Modifying : @Query의 select가 아닌 수정일 경우 @Modifying가 있어야 DB에 업데이트가 진행
주의
JPA에서 bulk성 연산은 영속성 컨텍스트에서 관리를 무시하고 DB에 바로 실행한다
> save()하여 영속성 컨텍스트에 있는 내용이 DB에 없는데 bulk 연산을 하게 됨

예제) DB에는 나이가 41로 변경되었지만 로그에서는 40으로 출력
결론) bulk 연산 후 영속성 컨텍스트를 다 날려야 한다
방법)
@PersistenceContext
EntityManager em;
상단에 EntityManager 선언 후
//when
int resultCount = memberRepository.bulkAgePlus(20);
em.flush();
em.clear();
영속성 컨텍스트 초기화
또는
@Modifying(clearAutomatically = true)
@Modifying 옵션 설정
@EntityGraph
JPA의 patch join 기술 선행
> 지연 로딩을 알고 이로 인해 발생하는 문제를 이해해야 함
실무에서는 patch join이 엄청 중요 > 모르면 실무 못함
LAZY로 연결되어 있는 연관관계의 데이터 중 조회를 하게 되면 Proxy라는 임시 객체를 만들어 사용할 뿐, 실제로 연관관계 대상의 데이터를 조회 할 때 또다시 쿼리를 생성 및 조회하여 데이터를 가져온다.
JPA patch join
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
entity graph 1
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
내부적으로 patch join 사용 됨
entity graph 2
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
entity graph 3
@EntityGraph(attributePaths = {"team"})
List<Member> findEntityGraphByUsername(@Param("username") String username);
JPA Hint & Lock
JPA Hint : JPA 쿼리를 날릴 때 JPA 구현체(Hibernate)에게 알려주는 힌트
객체의 데이터가 변경이 감지 될 경우 DB에 수정쿼리가 날아가게 된다.
하지만 화면상의 데이터 변경만 보고 싶을 뿐, 실제로 DB에 저장되지 않게 하고, 데이터 변경으로 데이터 변경 전/후 메모리를 사용하지 않도록 사용하기도 한다
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
위와 같이 선언하면 수정 쿼리 발생 안함
lock : 함부로 조회&수정을 하지 못하도록 선언
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findLockByUsername(String username);
꽤나 깊은 내용이므로 추가적으로 찾아보기
확장 기능
사용자 정의 리포지토리 구현
실무에서 중요
기존 스프링 데이터 JPA는 인터페이스로 구성되어 있는데, 실제 구현하게 되면 인터페이스를 상속 후 모든 실 기능을 다 구현해야한다.(구현할 기능이 많아진다면 사실상 비현실적)
내가 특정한 기능만 만들려면?
ex)
- jpa, 스프링 데이터 jpa를 쓰는게 아니라 jdbc를 활용
- 마이비틱스 라이브러리 기능을 사용
- 완전히 다른 리포지토리 기능을 사용
위와 같은 예제를 사용자 정의 리포지토리 기능을 제공받아 사용
인터페이스 생성
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
상속받는 class 생성
public class MemberRepositoryImpl implements MemberRepositoryCustom{
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m")
.getResultList();
}
}
상속받은 메서드를 순수 JPA로 구현하고 싶다면?
@RequiredArgsConstructor : 클래스에 어노테이션을 추가하거나
@PersistenceContext : EntityManager em 변수에 어노테이션을 추가하거나
클래스 생성자로 EntityManager em을 연결
위와 같이 사용자 정의 메서드를 만든 후,
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom
MemberRepository가 MemberRepositoryCustom을 상속받도록 설정
이후 MemberRepository의 findMemberCustom를 사용하면 위에서 만든 MemberRepositoryImpl의 findMemberCustom가 실행
> Spring Data JPA에 이렇게 동작되도록 엮어서 제공
@Test
public void callCustom() {
List<Member> result = memberRepository.findMemberCustom();
}
사용자 정의 구현 클래스 규칙:
- 리포지토리 인터페이스 이름 + Impl
- 스프링 데이터 JPA가 인식해서 스프링 빈으로 등록
이와 같은 방식은 언제쓸까?
- 멤버 JPA 리포지토리 인터페이스만으로 해결이 되거나
- @를 사용하여 쿼리를 짜 해결이 되거나
위 경우가 아닌 복잡하고 동적 Query를 짜야 할 경우에 사용
> QueryDSL 사용
> JDBC 템플릿 커스텀
팁
핵심 비즈니스 로직이 있는 리포지토리와 화면에 맞춘 DTO 리포지토리를 분리
Auditing
엔티티를 생성하거나 변경할 때 변경한 사람과 시간을 추적
순수 JPA 사용
public class JpaBaseEntity
생성 후 create와 update의 컬럼을 생성
public class Member extends JpaBaseEntity
필요한 Entity에 상속하여 사용
JPA 주요 이벤트 어노테이션
- @PrePersist, @PostPersist
- @PreUpdate, @PostUpdate
Spring Data JPA 사용
@EnableJpaAuditing : 프로젝트Application 클래스에 어노테이션 사용 필수
@MappedSuperclass
@Getter
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
이벤트 기반으로 동작한다는 것을 설정해주어야 함
@EntityListeners(AuditingEntityListener.class) : BaseEntity 어노테이션으로 사용
등록자 수정자
@CreatedBy
@Column(updatable = false)
private String createBy;
@LastModifiedBy
private String lastModifiedBy;
프로젝트Application에서
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.of(UUID.randomUUID().toString());
}
와 같이 설정
Web 확장-도메인 클래스 컨버터
@GetMapping("/members/{id}")
public String findmember(@PathVariable("id") Long id)
PathVariable 형태로 컨트롤러에 주소 매핑
PK를 사용하여 도메인 클래스 컨버터 기능을 사용 가능
@GetMapping("/members2/{id}")
public String findmember2(@PathVariable("id") Member member)
> 스프링이 중간에 컨버팅하는 과정을 다 끝내고 멤버를 바로 파라미터 결과에 인젝션
트랜잭션이 없는 범위인 조회용으로만 사용
Web 확장-페이징과 정렬
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Pageable의 기본 값을 이용하여 전체 데이터 중 일부분씩 조회 가능
파라미터의 기본값 설정을 변경하려면 글로벌(application.yml) 설정에서 수정
data:
web:
pageable:
default-page-size: 10
max-page-size: 2000
또는
@GetMapping("/members")
public Page<Member> list(@PageableDefault(size = 5) Pageable pageable)
절대 엔티티 정보를 그대로 프론트에 보내지 말고, DTO 데이터를 보내도록 한다
클래스 컨버터 기능을 사용하여 MemberDto 생성자를 만들어 사용하는 것도 가능
public MemberDto(Member member) {
this.id = member.getId();
this.username = member.getUsername();
}
스프링 데이터 JPA 분석
스프링 데이터 JPA 구현체 분석
merge()메서드를 사용한 업데이트 가급적 사용 금지
데이터 변경을 할 경우에는 ‘변경감지’를 이용
> 트랜잭션이 끝날 때 Entity값이 바뀌면 자동으로 변경 후 저장
> 활용편 1 확인
merge 사용은 언제?
> Entity가 영속성 상태에서 벗어나 다시 영속성 상태가 되어야 할 때
새로운 엔티티를 구별하는 방법
GenerateValue가 아닌 Custom 형태로 ID값만 사용해야 한다면 Persistable을 사용하여 새로운 Entity 확인 여부를 직접 구현해야 효과적이다
나머지 기능들
실무에서는 거의 사용하지 않으며, 대안되는 기능들이 존재
Specifications(명세)
Query By Example
Projections
네이티브 쿼리
'끄적끄적 > Spring Boot' 카테고리의 다른 글
| 개인프로젝트_서버 구상 (0) | 2024.08.11 |
|---|---|
| Spring Boot Querydsl (0) | 2024.07.26 |
| Spring Boot 프로젝트 필기 (0) | 2024.06.29 |
| Spring Boot 강의 필기 (0) | 2024.06.14 |