확장 기능

사용자 정의 리포지토리

리포지토리에 원하는 기능만 구현하여 사용하고 싶을 때, 직접 구현하면 인터페이스에 구현해야 하는 기능이 너무 많다.(인터페이스이기 때문에 모든 메서드 구현 필요), 이를 사용자 정의 리포지토리로 해결한다.
대부분 이 방법은 복잡한 쿼리를 Querydsl로 풀 때 사용된다.

  • 사용자 정의 인터페이스 이름과 구현 클래스 이름이 비슷하므로 더 직관적
  • 여러 인터페이스를 분리해서 구현하는 것도 가능하기 때문에 새롭게 변경된 이 방식을 권장

기존 리포지토리에 이 인터페이스를 상속받아 사용

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom
//사용자 정의 인터페이스 
public interface MemberRepositoryCustom {
	List<Member> findMemberCustom();
}
//사용자 정의 인터페이스 구현
@RequiredArgsConstructor
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom{

    private final EntityManager em;

    @Override
    public List<Member> findMemberCustom() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
}

Naming 관례
사용자 정의 인터페이스: (이름)
사용자 정의 인터페이스 구현: (이름)+Impl

Auditing

유지, 보수를 위해서 서비스가 등록일, 수정일, 등록자, 수정자를 갖고 있는 것이 좋음

설정

@EnableHpaAuditing -> 스프링 부트 설정 클래스에 적용해야함

@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 스프링 빈 등록
실무에서는 세션 정보나, 스프링 시큐리티 로그인 정보에서 ID를 받음

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

사용

@EntityListeners(AuditingEntityListener.class) -> 엔티티에 적용
@MappedSuperclass -> 다른 엔티티에 값만 상속시키기 위한 부모 엔티티 어노테이션

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDateTime;

    @LastModifiedDate
    private LocalDateTime lastModifiedDateTime;
}

사용 어노테이션

  • @CreatedDate
  • @LastModifiedDate
  • @CreatedBy
  • @LastModifiedBy

    실무에서는 등록시간, 수정시간이 필요하지만 등록자, 수정자는 필요하지 않을 수 있다. 때문에 시간타입 엔티티를 부모로 두고 이를 상속받는 등록/수정자 엔티티를 분리 구현하는 것이 좋다.

전체 적용

@EntityListeners(AuditingEntityListener.class)를 생략하고 엔티티 전체에 적용하려면 orm.xml에 다음과 같이 등록한다.

<?xml version=“1.0” encoding="UTF-8”?>
<entity-mappings xmlns=“http://xmlns.jcp.org/xml/ns/persistence/orm”
	xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”
	xsi:schemaLocation=“http://xmlns.jcp.org/xml/ns/persistence/
orm http://xmlns.jcp.org/xml/ns/persistence/orm_2_2.xsd”
 	version=“2.2">
 <persistence-unit-metadata>
 	<persistence-unit-defaults>
 		<entity-listeners>
 			<entity-listener class="org.springframework.data.jpa.domain.support.AuditingEntityListener”/>
 			</entity-listeners>
 		</persistence-unit-defaults>
 	</persistence-unit-metadata>
 
</entity-mappings>

도메인 클래스 컨버터

HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아 자동 바인딩

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final MemberRepository memberRepository;

    @GetMapping("/member/{id}")
    public String getMember(@PathVariable("id") Long id) {
        Member member = memberRepository.findById(id).get();
        return member.getUsername();
    }

    @GetMapping("/member2/{id}") //도메인 클래스 컨버터
    public String getMemberConvertor(@PathVariable("id") Member member) {
        return member.getUsername();
    }

    @PostConstruct
    public void init() {
        memberRepository.save(new Member("memberA"));
    }
}

주의: 도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 트랜잭션이 없는 범위에 존재하기 때문에 수정을 하면 안된다. 단순 조회용으로만 사용한다.

@PostConstructor
Bean은 new키워드를 이용해 생성하지 않기 때문에 생성자를 호출할 수 없다.
Bean이 생성될 때 실행할 매서드를 @PostConstructor로 구현할 수 있다.

페이징과 정렬

@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
	Page<Member> page = memberRepository.findAll(pageable);
	return page;
}
  • 파라미터로 Pageable을 받을 수 있다. (모든 리포지토리 메서드 마지막 파라미터로 전달 가능)
  • Pageable은 인터페이스, 실제는 org.springframework.data.domain.PageRequest 객체 생성
  • 따라서 파라미터로 Pageable을 넘기면 스프링 데이터 JPA가 PageRequest를 생성해 주입해준다.

요청 파라미터

  • 예) /members?page=0&size=4&sort=id,desc&sort=username.desc
  • page: 현재 페이지, 0부터 시작
  • size: 한 페이지에 노출할 데이터 건수
  • sort: 정렬 조건 정의, (기본 asc)

설정

  • 글로벌 설정
    properties 또는 yml파일에 추가
spring.data.web.pageable.default-page-size=20 /# 기본 페이지 사이즈/
spring.data.web.pageable.max-page-size=2000 /# 최대 페이지 사이즈/
  • 개별 설정
    @PageableDefault 어노테이션 사용
@GetMapping("/members")
public Page<Member> list(@PageableDefault(size = 4, sort = "username") Pageable pageable) {
	Page<Member> pageList = memberRepository.findAll(pageable);
	return pageList;
}

접두사

  • 페이징 정보가 둘 이상이면 접두사로 구분
  • @Qualifier에 접두사명 추가 "{접두사명}_XXX"
  • 예제: /members?member_page=1&order_page=2
public String list{
	@Qualifier("member") Pageable memberPageable,
	@Qualifier("order") Pageable orderPageable, 
	...
}

Page 내용을 DTO로 변환하기

  • 엔티티를 API로 노출하면 다양한 문제가 발생
  • 꼭 DTO로 변환해서 반환하자!
  • Page는 map()을 지원해서 내부 데이터를 다른 것으로 변경 가능
@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable){
	Page<Member> page = memberRepository.findAll(pageable);
    Page<MemberDto> pageDto = page.map(m -> new MemberDto(m.getId(), m.getUsername(), null));
    return pageDto;
}

Page를 1부터 시작하기

비추천(유지 보수 어려움)
Pageable, Page를 파라미터, 반환값으로 사용하지 않고 직접 클래스를 만들어 처리
직접 PageRequest(Pageable 구현체)를 생성해서 리포지토리에 넘긴다.
응답값도 Page 대신 직접 만들어 제공

@GetMapping("/members")
public MyPage<MyMemberDto> list(@PageableDefault pageable pageable){
	PageRequest request = PageRequest.of(1,2);
    
    Page<MemberDto> map = memberRepository.findAll(request)
    					  .map(MemberDto::new);
    
    MyPage<MyMemberDto> myMap = ...
}

"본 포스트는 작성자가 공부한 내용을 바탕으로 작성한 글입니다.
잘못된 내용이 있을 시 언제든 댓글로 피드백 부탁드리겠습니다.
항상 정확한 내용을 포스팅하도록 노력하겠습니다."

좋은 웹페이지 즐겨찾기