강의를 들으며 생각 정리 + "자바 ORM 표준 JPA 프로그래밍" 책 참고
사용자 정의 리포지토리 구현
스프링 데이터 JPA는 리포지토리 인터페이스만 정의하고 구현체는 스프링이 자동 생성한다.
그러나 다양한 이유로 인터페이스의 메서드를 직접 구현하고 싶다면?
ex)
JPA 직접 사용
스프링 JDBC Template
MyBatis
Querydsl
이를 위해 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많다.
그래서 스프링 데이터 JPA는 사용자 정의 인터페이스를 제공한다.
다음과 같은 인터페이스와 구현체가 있다고 하자.
</repository/MemberRepositoryCustom>
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
</repository/MemberRepositoryImpl>
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m")
.getResultList();
}
}
이렇게 순수 JPA를 직접 사용하는 구현체를 스프링 데이터 JPA에서 사용하고 싶다면 다음과 같이 인터페이스를 추가로 상속받는다.
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
이렇게 되면 스프링 데이터 JPA가 자동으로 사용자 정의 리포지토리를 인식하게 된다.
실무에서는 보통 Querydsl이나 JdbcTemplate를 함께 사용하기 위해 이 방식을 사용한다.
<사용자 정의 리포지토리 규칙>
사용자 정의 리포지토리의 이름은 마음대로 지어도 되지만 그 구현체는 리포지토리 인터페이스 이름 + 'Impl'을 따라야 한다.
예시에서는 MemberRepositoryImpl을 사용했다.
+) 스프링 데이터 2.x 부터는 사용자 정의 인터페이스명 + 'Impl' 방식도 지원한다.
예시에서는 MemberRepositoryCustomImpl이 되겠다. 이 방식을 사용하는 것이 더 명시적이고 스프링 데이터 JPA 리포지토리 입장에서 인터페이스도 여러 개 상속 받을 수 있기 때문에 이 방식을 권장한다.
+) 항상 사용자 정의 리포지토리가 필요한 것은 아니다. 그냥 임의의 리포지토리로 만들어도 된다. 예를 들어 MemberQueryRepository를 클래스로 만들고 스프링 빈으로 등록해서 사용해도 된다. 물론 이 경우 스프링 데이터 JPA와는 아무런 관계 없이 별도로 동작한다.
-> 특히, 핵심 비즈니스 로직과 화면을 위한 복잡한 로직은 분리하는 것이 좋다. 모든 것을 사용자 정의 하려 하지 말자.
Auditing
엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적하고 싶다면?
-> 등록일, 수정일, 등록자, 수정자
+) 특히 실무에서는 등록일, 수정일을 표기해주는 것이 도움이 된다.
<순수 JPA 사용>
순수 JPA를 사용하면 다음과 같다.
</entity/JpaBaseEntity>
@MappedSuperclass
@Getter
public class JpaBaseEntity {
@Column(updatable = false)
private LocalDateTime createdDate;
private LocalDateTime updatedDate;
@PrePersist
public void prePersist() {
LocalDateTime now = LocalDateTime.now();
createdDate = now;
updatedDate = now;
}
@PreUpdate
public void preUpdate() {
updatedDate = LocalDateTime.now();
}
}
</entity/Member>
public class Member extends JpaBaseEntity {...
이렇게 @MappedSuperclass를 생성하고 Member 엔티티에서 상속 받으면 Member Table을 생성할 때, 부모 클래스의 필드들을 컬럼으로 추가하게 된다.
@PrePersist와 @PreUpdate는 JPA의 이벤트 어노테이션이다.
<스프링 데이터 JPA 사용>
먼저 스프링 부트 설정 클래스에 @EnableJpaAuditing을 적용한다.
<DataJpaApplication>
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
}
그리고 @EntityListeners(AuditingEntityListener.class)를 엔티티에 적용한다.
</entity/BaseEntity>
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
@CreatedDate, @LastModifiedDate 어노테이션으로 보다 편리하게 Auditing을 사용할 수 있다.
이제 등록자, 수정자 기능도 추가해보자.
@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;
}
이 때, 등록자, 수정자를 처리해주는 AuditorAware 스프링 빈을 등록한다.
<DataJpaApplication>
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.of(UUID.randomUUID().toString());
}
}
등록, 수정될 때마다 AuditorAware 메서드를 호출해서 등록자, 수정자를 처리한다.
+) 예제에서는 랜덤 UUID를 받았지만 실제로는 세션 정보나, 시큐리티 로그인 정보에서에서 ID를 꺼내 사용한다.
참고로 실무에서 대부분의 엔티티는 등록시간, 수정시간이 필요하지만 등록자, 수정자는 없을 수도 있다. 그래서 다음과 같이 Base 타입을 분리하고, 원하는 타입을 선택해서 상속한다.
<entity/BaseTimeEntity>
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime updatedDate;
}
<entity/BaseEntity>
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
+) @MappedSuperclass가 아닌 임베디드 값 타입을 사용하면 안될까?
-> 이것은 상속을 사용하냐 위임을 사용하냐의 차이다.
객체지향의 일반적인 법칙에 따르면 상속보다는 위임이 좋겠지만, JPA에서는 상속을 사용하는게 더욱 편리하다.
예를 들어 임베디드 타입이 다음과 같이 있다고 하자.
class TraceDate {
TYPE createdDate;
TYPE updatedDate;
}
만약 JPQL을 사용한다면 항상 임베디드 타입을 적어주어야 하는 불편함이 있다.
select m from Member m where m.traceDate.createdDate > ?
@MappedSuperclass, 상속을 사용하면 다음과 같이 간단하고 쉽게 풀린다.
select m from Member m where m.createdDate > ?
Web 확장 - 도메인 클래스 컨버터
도메인 클래스 컨버터는 HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 조회해서 바인딩하는 기법이다.
<도메인 클래스 컨버터 사용 전>
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Long id) {
Member member = memberRepository.findById(id).get();
return member.getUsername();
}
<도메인 클래스 컨버터 사용 후>
@GetMapping("/members2/{id}")
public String findMember2(@PathVariable("id") Member member) {
return member.getUsername();
}
두 상황 모두 같은 select 쿼리가 나간다. 도메인 클래스 컨버터도 리포지토리를 사용해서 엔티티를 찾는다.
+) 주의
도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 단순 조회용으로만 사용해야 한다.
(트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다)
또한 PK를 인자로 받으려면 외부에 공개해야 되기 때문에 자주 사용하는 기능은 아니다.
Web 확장 - 페이징과 정렬
스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있다.
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
스프링 MVC 파라미터로 Pageable을 받을 수 있다. Pageable은 인터페이스이고, 실제로 PageRequest 객체를 생성해서 요청 파라미터 정보를 받는다.
<요청 파라미터>
ex) /members?page=0&size=3&sort=id,desc&sort=username,desc
page: 현재 페이지, 0부터 시작한다.
size: 한 페이지에 노출할 데이터 건수
sort: 정렬 조건을 정의한다
반환 타입 역시 Page를 지원한다.
ex) 요청 파라미터 /members?page=0
<@RestController에서 Page 타입 리턴 - JSON>
{
"content": [
{
"id": 1,
"username": "user0",
...
},
{
"id": 2,
"username": "user1",
...
},
...
],
"pageable": {
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"offset": 0,
"pageSize": 5,
"pageNumber": 0,
"paged": true,
"unpaged": false
},
"last": false,
"totalPages": 20,
"totalElements": 100,
"size": 5,
"number": 0,
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"first": true,
"numberOfElements": 5,
"empty": false
}
이렇게 Page의 속성들을 같이 반환할 수 있다. Rest API 사용시 유용하다.
<기본값>
기본적으로 페이징의 default size는 20으로 설정되어 있다. 이 기본값은 변경할 수 있다.
글로벌 설정
<application.yml - 내용 추가>
data:
web:
pageable:
default-page-size: 10
max-page-size: 2000
개별 설정
@PageableDefault 애노테이션 사용
@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size = 5) Pageable pageable) {...
개별 설정이 글로벌 설정보다 우선순위가 높다.
+) 접두사
페이징 정보가 둘 이상이면 접두사로 구분한다.
@Qualifier에 접두사명 추가
ex) /members?member_page=0&order_page=1
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable, ...
<DTO 변환>
Page<Member>는 엔티티를 그대로 API로 노출하기 때문에 다양한 문제가 발생한다. 그래서 엔티티를 꼭 DTO로 변환해서 반환해야 한다.
@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size = 5) Pageable pageable) {
return memberRepository.findAll(pageable)
.map(MemberDto::new);
}
<Page를 1부터 시작하기>
스프링 데이터는 Page를 0부터 시작한다. 먄약 1부터 시작하려면?
1. Pageable, Page를 파라미터와 응답 값으로 사용하지 않고, 직접 클래스를 만들어서 처리한다.
2. spring.data.web.pageable.one-indexed-parameters: true 설정을 추가한다.
그런데 이 방법은 Page의 다른 속성들은(pageSize, pageNumber, ...) page가 0부터 시작한다고 가정한 값이 저장되어 있기 때문에 한계가 있다.
-> 제일 깔끔한 방법은 그냥 페이지를 0부터 시작하는 것이다.
'java > jpa' 카테고리의 다른 글
[Spring Data JPA] 나머지 기능들 (0) | 2021.05.31 |
---|---|
[Spring Data JPA] 스프링 데이터 JPA 분석 (0) | 2021.05.30 |
[Spring Data JPA] 쿼리 메소드 기능 (0) | 2021.05.24 |
[Spring Data JPA] 공통 인터페이스 기능 (0) | 2021.05.23 |
[Spring Data JPA] 예제 도메인 모델 (0) | 2021.05.23 |