TIL

확장 기능

사용자 정의 레포지토리 구현

사용자 정의 구현 클래스 만들기

public interface MemberRepositoryCustom {
		List<Member> findMemberCustom();
}
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {

		private final EntityManager em;

		@Override
		public List<Member> findMemberCustom() {
		    return em.createQuery("select m from Member m")
                        .getResultList();
		}
}
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}

XXXRepository 하나에 핵심 비즈니스 로직이나 화면에 맞춘 통계성 쿼리를 모두 선언하고 (사용자 정의 레포지토리로) 구현하면 한 레포지토리가 너무 비대해진다. 영한님은 통계성 복잡한 쿼리(dto 등을 사용하는)를 사용하는 레포지토리와 핵심 비즈니스 로직이 있는 레포지토리를 분리하신다고 한다.

참고: 항상 사용자 정의 리포지토리가 필요한 것은 아니다. 그냥 임의의 리포지토리를 만들어도 된다. 예를 들어 MemberQueryRepository를 인터페이스가 아닌 클래스로 만들고 스프링 빈으로 등록해서 그냥 직접 사용해도 된다. 물론 이 경우 스프링 데이터 JPA와는 아무런 관계 없이 별도로 동작한다

Auditing

스프링 데이터 JPA 사용

설정

@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
		public static void main(String[] args) {
		    SpringApplication.run(DataJpaApplication.class, args);
		}
}
@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 스프링 빈 등록

@Bean
public AuditorAware<String> auditorProvider() {
    return () -> Optional.of(UUID.randomUUID().toString());
}

실무에서는 세션 정보나, 스프링 시큐리티 로그인 정보에서 ID를 받음

Web 확장 - 도메인 클래스 컨버터

@RestController
@RequiredArgsConstructor
public class MemberController {

		private final MemberRepository memberRepository;

		@GetMapping("/members/{id}")
		public String findMember(@PathVariable("id") Member member) {
				return member.getUsername();
		}
}

주의: 도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 단순 조회용으로만 사용해야 한다. (트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다.)

Web 확장 - 페이징과 정렬

페이징과 정렬 예제

@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
		Page<Member> page = memberRepository.findAll(pageable);
		return page;
}

요청 파라미터

page 정보만 주면 size 기본값은 20개로 설정된다.

기본값

spring.data.web.pageable.default-page-size=20 /# 기본 페이지 사이즈/
spring.data.web.pageable.max-page-size=2000 /# 최대 페이지 사이즈/

@PageableDefault

@RequestMapping(value = "/members_page", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = username, direction = Sort.Direction.DESC) Pageable pageable) {
 ...
}

접두사

public String list(
		@Qualifier("member") Pageable memberPageable,
		@Qualifier("order") Pageable orderPageable, ...

Page 내용을 DTO로 반환하기

@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
		Page<Member> page = memberRepository.findAll(pageable);
		Page<MemberDto> pageDto = page.map(MemberDto::new);
		return pageDto;
}

Page를 1부터 시작하기

  1. Pageable, Page를 파리미터와 응답 값으로 사용히지 않고, 직접 클래스를 만들어서 처리한다. 그리고 직접 PageRequest(Pageable 구현체)를 생성해서 리포지토리에 넘긴다. 물론 응답 값도 Page 대신에 직접 만들어서 제공해야 한다.
  2. spring.data.web.pageable.one-indexed-parameters를 true 로 설정한다. 그런데 이 방법은 web에서 page 파라미터를 -1 처리 할 뿐이다. 따라서 응답 값인 Page 에 모두 0 페이지 인덱스를 사용하는 한계가 있다.

Page<Member> 객체를 그대로 리턴하는 경우 페이징 내용 정보 뿐만 아니라 아래와 같은 메타 데이터도 함께 보낸다. 이 메타 데이터는 1부터 시작한다는 걸 반영하지 않고 0으로 시작한다는 가정 하에 보내진다. 즉 page=1로 보내 받은 응답 메타 데이터에 pageNumber는 아래와 같이 0으로 되어 있다.

{ 
  "content": [
 ...
 ],
	 "pageable": {
	 "offset": 0,
	 "pageSize": 10,
	 "pageNumber": 0 //0 인덱스
 }, 
  "number": 0, //0 인덱스
  "empty": false
}