사용자 정의 리포지토리 구현
스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동생성
스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많음
만약 인터페이스의 메서드를 직접 구현하고 싶다면?
- JPA 직접 사용(EntityManager)
- 스프링 JDBC Template 사용
- MyBatis 사용
- 데이터베이스 커넥션 직접 사용 등등
- Querydsl 사용
사용자 정의 인터페이스
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
사용자 정의 인터페이스 구현 클래스
@RequiredArgsConstructor
//사용자 정의 인터페이스명 + 'Impl'
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m")
.getResultList();
}
}
규칙: (리포지토리 인터페이스 이름 or 사용자 정의 인터페이스명 ) + 'Impl'
→ 스프링 데이터 JPA가 인식해서 스프링 빈으로 등록
사용자 정의 인터페이스 상속
//추가
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
실행
List<Member> result = memberRepository.findMemberCustom();
참고
주로 QueryDSL이나 SpringJdbcTemplate을 함께 사용할 때 사용자 정의 리포지토리 기능 자주 사용
항상 사용자 정의 리포지토리가 필요한 것은 아님(핵심 비니지스 레포지토리랑 단순 화면 맞춘 레포지터리 클래스 나눔)
Auditing
테이블 만들때 등록일, 수정일 기본적으로 남김
엔티티 생성, 변경할 때 변경한 사람과 시간을 추적하고 싶다면?
- 등록일, 등록자, 수정일, 수정자
순수 JPA 사용
-entity.JpaBaseEntity.java
@MappedSuperclass
@Getter
public class JpaBaseEntity {
@Column(updatable = false)
private LocalDateTime createDate;
private LocalDateTime updateDate;
@PrePersist //persist 하기전 이벤트 발생
public void prePersist() {
LocalDateTime now = LocalDateTime.now();
createDate = now;
updateDate = now; //수정할 때 편하게
}
@PreUpdate //update 하기전 이벤트 발생
public void preUpdate() {
updateDate = LocalDateTime.now();
}
}
-실제 엔티티에서 상속
public class Member extends JpaBaseEntity {}
확인
@Test
public void jpaEventBaseEntity() throws Exception {
//given
Member member = new Member("member1");
memberRepository.save(member); //@PrePersist
Thread.sleep(100);
member.setUsername("member2");
em.flush(); //@PreUpdate
em.clear();
//when
Member findMember = memberRepository.findById(member.getId()).get();
//then
System.out.println("findMember.createdDate = " + findMember.getCreateDate());
System.out.println("findMember.updatedDate = " + findMember.getUpdateDate());
}
JPA 주요 이벤트 어노테이션
- @PrePersist, @PostPersist
- @PreUpdate, @PostUpdate
스프링 데이터 JPA 사용
설정
@EnableJpaAuditing //스프링 부트 설정 클래스 적용
@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;
}
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
//등록자, 수정자를 처리해주는 AuditorAware 스프링 빈 등록
@Bean
public AuditorAware<String> auditorProvider() {
//실제론 세션정보나 스프링 시큐리티 로그인 정보에서 유저ID 받음
return () -> Optional.of(UUID.randomUUID().toString());
}
}
AuditorAware<T> 구현체를 만들어서 @Bean으로 등록하면 Spring이 이 빈을 보고 현재 사용자 정보를 JPA에 넘겨줌
참고.
등록자, 수정자는 없을 수도 있기 때문에 타입 분리함
원하는 타입만 선택 상속
public class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
public class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
등록시점에 수정 데이터도 저장하게 편함
@EntityListeners(AuditingEntityListener.class) 생략하고 엔티티 전체 적용
-META-INF/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>
Web 확장 - 도메인 클래스 컨버터
(가볍게 읽기)
HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩
@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();
}
//도메인 클래스 컨버터 사용 후
@GetMapping("/members2/{id}")
public String findMember2(@PathVariable("id") Member member) {
return member.getUsername();
}
@PostConstruct
public void init() {
memberRepository.save(new Member("memberA"));
}
}
스프링이 중간에서 컨버팅하는 과정을 끝내고 멤버를 파라미터 결과 인젝션 해줌
HTTP 요청은 회원 id를 받지만 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환
도메인 클래스 컨버터도 리파지토리를 사용해서 엔티티를 찾음
주의
도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 단순 조회용으로만 사용해야 함
(트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다.)
-> 간단할 경우만 쓸수있어 거의 못씀
Web 확장 - 페이징과 정렬
스프링 데이터 제공 페이징,정렬 기능을 스프링 MVC에서 편리하게 사용 가능
사용
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
컨트롤러 메서드에 Pageable 파라미터가 있으면,
Spring MVC가 HTTP 요청 파라미터(page, size, sort)로부터
자동으로 Pageable 구현 객체(PageRequest)를 생성해 주입해줌
/members?page=0&size=2&sort=id,desc&sort=username,desc 접속
결과
{
"content": [
{
"id": 100,
"username": "user99",
"age": 99,
"team": null
},
{
"id": 99,
"username": "user98",
"age": 98,
"team": null
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 2,
"sort": {
"empty": false,
"unsorted": false,
"sorted": true
},
"offset": 0,
"unpaged": false,
"paged": true
},
"last": false,
"totalPages": 50,
"totalElements": 100,
"first": true,
"size": 2,
"number": 0,
"sort": {
"empty": false,
"unsorted": false,
"sorted": true
},
"numberOfElements": 2,
"empty": false
}
요청 파라미터
- page: 현재 페이지, 0부터 시작
- size: 한 페이지에 노출할 데이터 건수
- sort: 정렬 조건 정의
예) 정렬 속성,정렬 속성...(ASC | DESC), 정렬 방향을 변경하고 싶으면 'sort' 파라 미터 추가 가능('asc' 생략 가능)
기본값
글로벌 설정 - 스프링 부트
(/members만 했을때)
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 = 5, sort = "username", direction = Sort.Direction.DESC)
Pageable pageable) {
...
}
접두사
페이징 정보가 둘 이상이면 접두사로 구분
@Qualifier에 접두사명 추가 "{접두사명}_xxx"
예제: /members?member_page=0&order_page=1
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable, ...
Page 내용을 DTO로 변환하기
엔티티를 API로 노출하면 보안위험 등 다양한 문제 발생
➡︎엔티티 꼭 DTO 변환해 반환할 것
* Page는 map()을 지원해 내부 데이터 다른 것으로 변경가능
-MemberDTO
@Data
public class MemberDto {
private Long id;
private String username;
public MemberDto(Member m) {
this.id = m.getId();
this.username = m.getUsername();
}
}
-Page.map() 사용
@GetMapping("/members")
public Page<MemberDTO> list(Pageable pageable) {
return memberRepository.findAll(pageable).map(MemberDTO::new);
}
Page를 1부터 시작하기
스프린 데이터는 Page 0부터 시작하는데 1부터 시작하려면?
방법 1. 직접 PageRequest(Pageable 구현체)를 새로 정의해 리포지토리에 넘기기
방법 2. spring.data.web.pageable.one-indexed-parameters = true로 설정
→ 이 방법은 page파라미터를 -1 처리 할 뿐. 따라서 응답값인 Page에 모두 0 페이지 인덱스를 사용하는 한계가 있음
(그냥 페이지 인덱스 0으로 처리하는게 제일 깔끔...)
'Spring Data JPA' 카테고리의 다른 글
| [Spring Data JPA] 나머지 기능들 (0) | 2025.05.13 |
|---|---|
| [Spring Data JPA] 스프링 데이터 JPA 분석 (0) | 2025.05.12 |
| [Spring Data JPA] 페이징, 정렬, 성능 최적화 기능 (0) | 2025.05.09 |
| [Spring Data JPA] 쿼리 메소드 기능 (0) | 2025.05.08 |
| [Spring Data JPA] 공통 인터페이스 기능 (0) | 2025.05.02 |