확장 기능
사용자 정의 REPO 구현
- 현재 스프링 데이터 JPA를 사용하는 REPO에서 JPA를 직접 사용하거나 JDBC Template, MyBatis 를 사용하는 등 스프링 데이터 JPA가 아닌 메서드를 직접 구현하고 싶을 때의 방법 (특히 QueryDSL을 사용해야 할 때)
- 만약 해당 방법을 사용하지 않고 현재 REPO의 구현체를 만들어 메서드를 추가한다면? → Spring Data Jpa가 구현해주는 모든 메서드들을 직접 구현해준 후에 메서드를 추가해주어야 함! 너무나 복잡!
- 사용자 정의 Repository 구현
-
사용자 정의 인터페이스 생성
public interface MemberRepositoryCustom { List<Member> findMemberCustom(); }
List<Member> findMemberCustom();
: Spring Data Jpa 를 사용하지 않은 사용자 정의 메서드 → JPA를 직접 사용하거나 QueryDSL 등을 사용하고자 하는 메서드
-
사용자 정의 인터페이스의 구현체 생성 (구현체의 이름은 정해진 규칙에 따라 생성)
@RequiredArgsConstructor public class MemberRepositoryImpl implements MemberRepositoryCustom { private final EntityManager em; @Override public List<Member> findMemberCustom() { return em.createQuery("select m from Member m") .getResultList(); } }
- 해당 Interface의 메서드를 구현 (
@Override
) - 해당 구현체는 JPA를 직접 사용(JPQL)하기 위한 메서드
- 구현체의 이름은 현재 Spring Data JPA를 사용(JpaRepository<>)하는 Interface(
MeberRepository
)에 “Impl
” 이라는 키워드를 붙여줘야함 (MeberRepositoryImpl
) → 정해진 규칙 (규칙: 리포지토리 인터페이스 이름 + Impl or 사용자 정의 인터페이스 명 + “Impl” →MeberRepositoryCustomImpl
) - 이렇게 규칙에 따라 구현 클래스를 만들어주면 따로 스프링 빈으로 등록할 필요 없이 스프링 데이터 JPA가 인식하여 스프링 빈으로 등록! → 따라서 규칙에 따라야하는 것은 필수!!
- 해당 Interface의 메서드를 구현 (
-
해당 사용자 정의 인터페이스를 현재 Spring Data JPA를 사용(JpaRepository<>)하는 Interface에 상속을 줌
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {}
-
사용자 정의 메서드 호출 및 사용
List<Member> result = memberRepository.findMemberCustom();
- 이질감 없이 그냥 바로 memberRepository 에서 호출해서 사용 가능
-
Auditing
- Entity를 생성하고 변경할 때 자동으로 변경한 사람과 시간을 추적하게 해줄 수 있는 기능
- 등록일, 수정일, (등록자, 수정자) 는 실무에 있어서 필수 Col→ 추적을 위해
- 즉, 이들은 공통적으로 들어가는 Field 이고, 이들에 대해서 자동으로 기록해주는 수단이 필요! → 해당 Entity가 등록되기 직전, 자동으로 등록일과 등록자를 기록해주고, 수정되기 직전, 자동으로 수정일과 수정자를 기록해주는 수단
-
순수 JPA에서의 Auditing (
@PrePersist
,@PreUpdate
)@MappedSuperclass @Getter public class JpaBaseEntity { @Column(updatable = false) private LocalDateTime createdDate; private LocalDateTime updatedDate; @PrePersist public void prePersist() {...} @PreUpdate public void preUpdate() {...} }
JpaBaseEntity
@MappedSuperclass
: 해당 Class의 Field를 공통 Field로 만들겠다는 뜻. → 다른 Entity들은 해당 Class 를 상속받고 공통 Field를 사용할 수 있음- 등록일과 수정일 Field 보유 (공통 Field)
- ex)
public class Member extends JpaBaseEntity {}
→ Member는 createdDate, updatedDate Field를 보유하게 됨
@PrePersist
- 해당 class 를 상속받은 Entity가 저장되기 직전에 해당 어노테이션이 달린 메서드(createdDate, updatedDate 설정) 실행
@PreUpdate
- 해당 class 를 상속받은 Entity가 수정되기 직전에 해당 어노테이션이 달린 메서드(updatedDate 설정) 실행
- JPA 주요 이벤트 어노테이션
- 저장 관련 :
@PrePersist
,@PostPersist
- 수정 관련 :
@PreUpdate
,@PostUpdate
- 저장 관련 :
- 스프링 데이터 JPA 에서의 Auditing (
@CreatedDate
,@LastModifiedDate
) 사용-
스프링 부트 설정 클래스에
@EnableJpaAuditing
적용 (추가로 등록자, 수정자 처리를 원한다면,AuditorAware
를 스프링 빈 등록)@EnableJpaAuditing // Auditing기능을 사용하기 위한 설정 @SpringBootApplication public class DataJpaApplication { ... @Bean // 등록자, 수정자를 처리해주는 AuditorAware 스프링 빈 등록 public AuditorAware<String> auditorProvider() { return () -> Optional.of(UUID.randomUUID().toString()); } // 내부 getCurrentAuditor() 를 람다로 구현한 것 }
- 여기서는 UUID를 통해서 진행했지만, 실무에선 세션 정보나, 스프링 시큐리티 로그인 정보에서 ID를 받아와 저장해줌 (
@CreatedBy
,@LastModifiedBy
가 달린 Field에)
- 여기서는 UUID를 통해서 진행했지만, 실무에선 세션 정보나, 스프링 시큐리티 로그인 정보에서 ID를 받아와 저장해줌 (
-
Auditing을 사용하고자 하는 (등록일, 수정일 등을 가진) 엔티티에
@EntityListeners(AuditingEntityListener.class)
(이벤트 기반으로 동작한다는 것을 명시) 적용한 후@CreatedDate
(등록일),@LastModifiedDate
(변경일),@CreatedBy
(등록자),@LastModifiedBy
(변경자)@EntityListeners(AuditingEntityListener.class) @MappedSuperclass @Getter public class BaseEntity { @CreatedDate @Column(updatable = false) private LocalDateTime createdDate; @LastModifiedDate private LocalDateTime lastModifiedDate; @CreatedBy @Column(updatable = false) private String createdBy; @LastModifiedBy private String lastModifiedBy; }
- 이렇게 설정하게 되면 순수 JPA처럼 직접 Method를 짤 필요 없이 자동으로 등록, 수정과 관련된 필드를 마치 순수 JPA에서의 Auditing 동작과 동일하게 저장해줌
- 등록자, 변경자 등은 필수적이지 않는 경우가 더 많음 → Base 타입을 분리하고, 원하는 타입을 선택해서 상속하는 방법으로 진행하면 효율적
-
페이징과 정렬 - web 확장
- 스프링 데이터에서 제공하는 Paging과 Sorting 기능을 스프링 MVC에서 편리하게 사용 가능
- 스프링 MVC에서 파라미터를 Pageable로 받고, 해당 Pageable을 그대로 사용하여 조건에 맞는 페이지 결과를 얻어낼 수 있음
-
예시)
@RestController @GetMapping("/members") public Page<MemberDto> list(Pageable pageable) { Page<Member> page = memberRepository.findAll(pageable); return page.map(MemberDto::new); }
- 파라미터 :
Pageable
- Request로써 받아올 수 있음
- Pageable은 Interface이고, 요청파라미터를 넣어주게 되면 스프링부트에서 자동으로 이에 대한 구현체인
PageRequest
객체를 생성해줌 (PageRequest 는 이전 페이징과 정렬에서PageRequest.of(page,size,Sort)
의 그 PageRequest) - 참고 : Spring Data Jpa 에서의 “공통 인터페이스 기능(method)들, 메서드이름으로 쿼리 생성”은 모두 PageRequest를 인자로 받을 수 있음
- 반환 :
Page<MemberDto>
- 조건에 따른 결과 List Content 포함
- 부가적인 페이지 정보 포함
- 주의 : 항상 Controller의 반환은 Entity가 아닌 DTO로 해야되는 점!
- 요청 파라미터 예시
/members?page=0&size=3&sort=id,desc&sort=username,desc
- page
- focus할 페이지 (주의 : 0부터 시작)
- 값을 넣지 않으면 default=0
- size
- 해당 페이지에 가져올 데이터의 개수
- 값을 넣지 않으면 default = 20
- sort
- 정렬 조건
- 값을 넣지 않으면 정렬 X
- 파라미터 :
- 요청 파라미터 Default 값 설정
- Global
spring.data.web.pageable.max-page-size=2000
: 최대 페이지 사이즈 설정spring.data.web.pageable.default-page-size=20
: size default 값 설정
- 개별 설정
-
@PageableDefault
어노테이션 사용@GetMapping public String list( @PageableDefault(size = 12, sort = “username”, direction = Sort.Direction.DESC) Pageable pageable)
-
- Global
- 접두사
@Qualifier
- 페이징 정보가 둘 이상일 때 사용
@Qualifier
에는 접두사명 부여(@Qualifier("member")
), 요청파라미터에는 접두사로 구분("{접두사명}_xxx”
)- ex)
- 요청 파라미터 :
/members?member_page=0&order_page=1
-
Controller 파라미터
public String list( @Qualifier("member") Pageable memberPageable, @Qualifier("order") Pageable orderPageable, ...) {}
- 요청 파라미터 :
부가 기능
Projections
- 엔티티 대신, DTO를 편리하게 조회할 때 사용
- 단순한 DTO 조회 시 유용
- 단점 : 연관관계를 포함한 DTO 조회 시 최적화 불가능 + LEFT OUTER JOIN만 사용 가능
- 예시)
- 회원 이름만 조회하고 싶은 상황
-
일반 스프링 데이터 JPA
@Query("select m.name from Member m where m.name = :name") List<String> findNamesByName(@Param("name") String name);
@Query
를 통해 직접 jpql문을 작성해줘야 하고, 바인딩도 직접해주는 번거러움 존재- 다른 조건으로 검색 시 JPQL을 또 name으로 반환할 수 있도록 작성해주어야 함
- 스프링 데이터 JPA에서 Projections 사용 (Closed Projections)
-
Interface 생성
public interface NameOnly { String getName(); }
- 해당 Interface로 Projection의 결과를 받음
getName()
을 통해서 이름만 받아옴
-
MemberRepository에 메서드 추가
List<NameOnly> findProjectionsByName(String name);
- 반환 타입을 방금 생성한 Interface로 설정
- Member Entity의 모든 Field를 select해서 가져오는 것이 아닌 name field만 select해서 가져옴 → 최적화
- 반환 타입으로 인지하기에
find…By
~ 에서 … 은 자유
-
-
스프링 데이터 JPA에서 Projections 사용 (Open Proejctions)
public interface UsernameOnly { @Value("#{target.username + ' ' + target.age + ' ' + target.team.name}") String getUsername(); }
@Value
를 통해서 직접 select 해올 field를 지정하고 해당 결과를 어떻게 표현해 낼지 지정 가능- 하지만 이렇게 할 경우 DB에서 엔티티 필드를 다 조회해온 다음에 계산 → JPQL SELECT 절 최적화 불가능
-
DTO class Projection
@Getter public class NameOnlyDto { private final String name; private final int age; public NameOnlyDto(String name, int age) { this.name= username; this.age = age; } }
- 구체적인 DTO 형식으로 Projection
- 생성자의 파라미터 이름으로 매칭 →
public NameOnlyDto(String name, int age)
- 동적 class Projections
-
MemberRepository의 Spring Data JPA의 method에서 Generic Type을 부여해주면 동적으로도 프로젝션 변경 가능
<T> List<T> findProjectionsByUsername(String username, Class<T> type);
-
사용
List<UsernameOnly> result = memberRepository.findProjectionsByUsername("m1", UsernameOnly.class);
-
네이티브 쿼리
- SQL 언어로 직접 query를 작성하는 것 (Spring Data Jpa의 페이징 기능 지원)
- 네이티브 쿼리는 가급적 사용하지 않는게 좋음 → 반환타입도 제한되고 제약조건이 많음
- 반환 타입
- Object[], Tuple, DTO(스프링 데이터 인터페이스 Projections 지원)
- 제약
- Sort 파라미터를 통한 정렬이 정상 동작하지 않을 수 있음
- JPQL처럼 애플리케이션 로딩 시점에 문법 확인 불가
- 동적 쿼리 불가
- 반환 타입
- 네이티브 쿼리 + Projections
- 네이티브 쿼리가 Projections를 통해 DTO로 어렵지 않게 반환 가능 → 그나마 유용한 기능
-
MemberProjection Interface
public interface MemberProjection { Long getId(); String getName(); String getTeamName(); }
-
NativeQuery with Projections
@Query(value = "SELECT m.member_id as id, m.username, t.name as teamName " + "FROM member m left join team t", countQuery = "SELECT count(*) from member", nativeQuery = true) Page<MemberProjection> findByNativeProjection(Pageable pageable);