사용자 정의 리포지토리
스프링 데이터 JPA는 인터페이스만 정의하고 구현체를 스프링이 자동으로 생성해준다. 이 인터페이스를 직접 구현하려면 구현해야 하는 기능이 너무 많다.
- 하지만 다양한 이유로 인터페이스의 메서드를 직접 구현하고 싶다?
- JPA 직접 사용 - EntityManager
- 스프링 JDBC Tempalte 사용
- MyBatis 사용
- 데이터베이스 커넥션 직접 사용
- QueryDsl 사용 등등..
사용자 정의 리포지토리
1
2
3
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
1
2
3
4
5
6
7
8
9
10
11
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m")
.getResultList();
}
}
이런식으로 custom interface와 그 interface를 구현하는 클래스를 만든 후,
1
2
3
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
//...
}
기존 스프링 데이터 JPA 리포지토리에 extends로 해당 interface를 상속해주면 구현한 코드를 실행시킬 수 있다.
주로 QueryDsl을 사용할 때 이런식으로 확장한다. 쿼리를 직접 넣어야하기 때문이다.
따라서 간단한 부분은 스프링 데이터 JPA가 기본적으로 제공하는 기능을 사용하고 필요 시 확장하면 된다.
주의 사항
규칙이 존재한다.
MemberRepositoryCustom 인터페이스를 구현하는 클래스의 네이밍은 규칙이 있다.
- 리포지토리 인터페이스 이름 + Impl
- MemberRepositoryImpl
- 이를 스프링 데이터 JPA가 인식해 스프링 빈으로 등록해주기 때문이다.
스프링 데이터 2.x 부터는 또 바뀌었다.
- 반드시 리포지토리 인터페이스 이름 + Impl일 필요 없다.
- 사용자 정의 인터페이스 이름 + Impl 방식도 가능하다.
- MemberRepositoryCustomImpl (O)
기존 규칙보다 사용자 정의 인터페이스 이름과 구현 클래스 이름이 비슷해 더 직관적이다. 추가로 여러 인터페이스를 분리해 구현하는 것도 가능하기 때문에 변경된 방식을 사용하는 것이 권장된다.
조심해야할 부분
항상 사용자 정의 리포지토리로 해결하려고 할 필요 없다.
단순히 임의의 리포지토리를 만들어도 된다. 예를 들면 어떤 복잡한 쿼리 작업이 필요한 리포지토리인 MemberQueryRepository를 클래스로 만들고 스프링 빈으로 등록해 사용해도 되는 것이다.
이렇게 하면 스프링 데이터 JPA하고는 별개로 동작하지만, 유지보수성 측면에서 오히려 더 나을 수 있다.
Auditing
- 엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적하고 싶을 때 사용하는 기능
- 등록일
- 수정일
- 등록자
- 수정자
순수 JPA 사용 방법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@MappedSuperclass
public abstract 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();
}
}
1
2
3
4
@Entity
public class Member extends JpaBaseEntity {
//...
}
- @MappedSuperclass (복습)
- 상속관계 매핑이 아닌 단순하게 공통 속성을 담는 클래스
- 테이블에 해당 필드들이 추가된다.
- 따로 조회가 불가능하고 직접 생성해 사용할 일이 없으므로 추상클래스로 만드는 것이 권장된다.
- 상속관계 매핑이 아닌 단순하게 공통 속성을 담는 클래스
스프링 데이터 JPA 사용 방법
- @EnableJpaAuditing - 스프링 부트 설정 클래스에 적용
- class DataJpaApplication
- @EntityListeners(AuditingEntityListener.class) - 엔티티에 적용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
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;
}
- 애노테이션으로 간단히 설정할 수 있는 것을 볼 수 있다.
- 등록자, 수정자는 등록자와 수정자를 등록해주어야 한다.
등록자, 수정자 등록
1
2
3
4
5
6
7
8
9
10
11
12
13
@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());
}
}
실제로는 UUID가 아닌 스프링 시큐리티 컨텍스트에서 세션정보를 꺼내오거나 HTTP 세션에서 정보를 꺼내와 사용하게 된다.
참고
보통 등록 시간, 수정 시간은 반드시 필요하고 등록자, 수정자는 필요한 테이블이 있고 아닌 테이블이 있기 때문에
BaseTimeEntity(등록시간, 수정시간)를 만들어두고 그것을 상속받는 BaseEntity(등록자, 수정자 + extends 등록,수정시간)를 만들어 분리하는 방법을 선택할 수 있다.
Web 확장 - 도메인 클래스 컨버터
1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Long id) {
Member member = memberRepository.findById(id).get();
return member.getUsername();
}
}
위와 같이 컨트롤러에 메서드가 구현되어있다고 가정하자.
1
2
3
4
@GetMapping("/members2/{id}")
public String findMember(@PathVariable("id") Member member) {
return member.getUsername();
}
- HTTP 요청은 회원 id를 받는 것이다.
- 하지만 도메인 클래스 컨버터가 중간에 동작해 회원 엔티티 객체를 반환해주어 개발자 입장에서는 Member member로 받을 수 있는 것이다.
- 도메인 클래스 컨버터도 똑같이 리포지토리를 사용해 엔티티를 찾는 것이고 그 과정만 생략해주는 것이다.
주의
도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 해당 엔티티는 단순 조회용으로만 사용해야 한다.
트랜잭션이 없는 범위 내에서 엔티티를 조회했기 때문에 변경 감지가 일어나지 않아 DB에 반영되지 않는다.
Web 확장 - 페이징과 정렬
1
2
3
4
5
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
이런식으로 사용 후
1
http://loaclhost:8080/members?page=0&size=3&sort=id,desc
이런식으로 파라미터로 넘겨주면 원하는 대로 커스텀이 가능하다.
- page: 현재 페이지, 0부터 시작
- size: 한 페이지에 노출할 데이터 건 수
- sort: 정렬 조건 정의
pageRequest라는 객체를 생성해 값을 채워 pageable에 인젝션 해주는 방식으로 작동된다.
글로벌 설정
1
2
spring.data.web.pageable.default-page-size=20 # 기본 페이지 사이즈
spring.data.web.pageable.max-page-size=2000 # 최대 페이지 사이
개별 설정
- @PageableDefault 애노테이션을 사용.
1
2
3
4
@GetMapping("/members_page")
public String list(@PageableDefault(size = 12, sort = "username", direction = Sort.Direction.DESC) Pageable pageable) {
//...
}
접두사
- 페이징 정보가 둘 이상이면 접두사로 구분
- @Qualifier에 접두사 명 추가
- /members?member_page=0&order_page=1
1
2
3
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable, ...
Page 내용을 DTO로 변환 (복습)
1
2
3
4
5
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
이 방법은 Member 엔티티 그대로를 노출시키고 있다. 엔티티가 수정되면 API 스펙이 바뀌기 때문에 문제가 발생한다.
항상 API를 반환할때는 DTO로 변환해야 한다는 것을 명심하자.
1
2
3
4
5
@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page.map(member -> new MemberDto(member.getId(), member.getUsername(), null));
}
1
2
3
4
5
6
7
8
9
public class MemberDto {
//...
public MemberDto(Member member) {
this.id = member.getId();
this.username = member.getUsername();
//...
}
}
1
2
3
4
5
@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
return memberRepository.findAll(pageable)
.map(MemberDto::new);
}
Dto 내에서는 엔티티를 봐도 괜찮기 때문에 위와 같은 방법이 더 낫다.
참고: 페이지를 1부터 시작하는 방법
현재 페이지는 0부터 시작하게 되어 있다.
1부터 시작하려면 어떻게 해야 할까?
- Pageable, Page를 파라미터와 응답 값으로 사용하지 않고, 직접 클래스를 만들어 처리한다. 그리고 직접 PageRequest를 생성해 리포지토리에 넘긴다. 물론 응답값도 Page 대신 직접 만들어 제공해야 한다.
- 혹은 spring.data.web.pageable.one-indexed-parameters를 true로 설정한다.
- 이 방법은 web에서 page 파라미터를 -1 처리하는 것이기 때문에 응답 값인 Page에 모두 0 인덱스를 사용한다는 한계가 있다.
권장하는 방법은 그냥 0부터 쓰는 것이다.