Auditing
엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적하고 싶으면?
등록일, 수정일, 등록자, 수정자
운영에서 테이블 단위로 기본적으로 적용시키는 기능 -> 객체 세상에서 자동화
순수 JPA
@Column(updatable = false) //insertable=true가 defalut
애플리케이션에서 update를 해도 DB에 적용되지 않는다.
<JPA 엔티티 라이프 사이클에서의 콜백 메서드>
@PrePersist: persist() 직전에 호출
@PostPersist: persist() 이후 호출. PK 사용 가능
@PreUpdate: update 연산 전에 호출
@MappedSuperClass로 정의하여 필드만 상속 받아 사용한다.
스프링 데이터 JPA
Setting
@EnableJpaAuditing: 스프링 부트 설정 클래스(@SpringBootApplication)에 적용
@EntityListeners(AuditingEntityListener.class): MappedSuperClass에 적용
사용 어노테이션
@CreatedDate
@LastModifiedDate
@CreatedBy
@LastModifiedBy
등록자, 수정자를 처리해주는 AuditorAware 스프링 빈 등록
(예제에서는 임의의 랜덤 값 생성. 실무에서는 세션 정보나, 스프링 시큐리티 로그인 정보에서 ID를 받음)
@Bean
public AuditorAware<String> auditorProvider() {
return new AuditorAware<String>(){
@Override
public Optional<String getCurrentAuditor(){
return Optional.of(UUID.randomUUID().toString();
}
}
}
엔티티 등록과 수정이 이루어질 때마다 auditorProvider()를 호출하여 필드 값을 채운다.
참고: 실무에서 대부분의 엔티티는 등록시간, 수정시간이 필요하지만, 등록자, 수정자는 없을 수도 있다. 그래서 Base 타입을 분리하고, 원하는 타입을 선택해서 상속한다.
class BaseTimeEntity{ 등록시간, 수정시간 }
class BaseEntity extends BaseTimeEntity { 등록자, 수정자 }
참고: 최초 저장시점에 등록일, 등록자는 물론이고, 수정일, 수정자도 같은 데이터가 저장된다. 데이터가 중복 저장되는 것 같지만, 이렇게 해두면 변경 컬럼만 확인해도 마지막에 업데이트한 유저를 확인 할 수 있으므로 유지보수 관점에서 편리하다. 이렇게 하지 않으면 변경 컬럼이 null 일때 등록 컬럼을 또 찾아야 한다.
참고로 저장시점에 저장데이터만 입력하고 싶으면 @EnableJpaAuditing(modifyOnCreate = false) 옵션을 사용하면 된다.
Web 확장 - 도메인 클래스 컨버터
private final MemberRepository memberRepository;
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Member member) {
...
}
HTTP 요청은 회원 id 를 받지만 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환
도메인 클래스 컨버터도 리파지토리를 사용해서 엔티티를 찾음
pk를 외부에 공개해서, pk로 매핑하는 것? 좋은 방법일까
주의: 도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 단순 조회용으로만 사용해야 한다. (트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다. -> 영속성 컨텍스트 대한 범위가 애매)
Web 확장 - 페이징과 정렬
스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있다.
@GetMapping("/members")
public Page list(Pageable pageable) { //페이징 조건 (PageRequest)
Page page = memberRepository.findAll(pageable);
return page; //페이징 결과
}
요청 파라미터
PageRequest 객체 생성 (자동 바인딩)
예) /members?page=0&size=3&sort=id,desc&sort=username,desc
-page: 현재 페이지, 0부터 시작 (기본값: 0)
-size: 한 페이지에 노출할 데이터 건수 (기본값: 20)
-sort: 정렬 조건을 정의한다.
예) 정렬 속성,정렬 속성, ..., (ASC | DESC), 정렬 방향을 변경하고 싶으면 &sort 파라미터 추가 ( asc 생략 가능)
요청 파라미터 기본값
-글로벌 설정: 스프링 부트 (application.yml)
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) {... }
접두사 페이징 정보가 둘 이상이면 접두사로 구분
@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() 을 지원해서 내부 데이터를 다른 것으로 변경할 수 있다.
Page page = memberRepository.findAll(pageable);
Page pageDto = page.map(MemberDto::new);
return pageDto;
참고: Page<Entity>는 API 반환 X, Page<DTO>는 OK
Page를 1부터 시작하기
스프링 데이터는 Page를 0부터 시작한다.
Q. 만약 1부터 시작하려면?
A1. Pageable, Page를 파리미터와 응답 값으로 사용히지 않고, 직접 클래스를 만들어서 처리한다.
그리고 직접 PageRequest(Pageable 구현체)를 생성해서 리포지토리에 넘긴다. 물론 응답값도 Page 대신에 직접 만들어서 제공해야 한다.
A2. spring.data.web.pageable.one-indexed-parameters 를 true 로 설정한다. 그런데 이 방법은 web에서 page 파라미터를 -1 처리 할 뿐이다. 실데이터 Page(반환값)는 0 페이지 인덱스를 사용하는 한계가 있다.
스프링 데이터 JPA 분석
스프링 데이터 JPA 구현체 분석
스프링 데이터 JPA가 제공하는 공통 인터페이스의 구현체 org.springframework.data.jpa.repository.support.SimpleJpaRepository
대부분 그냥 JPA 내부 기능을 활용
@Repository: JPA 예외를 스프링이 추상화한 예외로 변환 -> 하부 구현 기술을 변경해도 기존 비지니스 로직에 영향 최소화.
@Transactional: 클래스 레벨은 readOnly = true. 변경 메서드(등록, 수정, 삭제)는 false.
-JPA의 모든 변경은 트랜잭션 안에서 동작
-스프링 데이터 JPA(리포지토리)는 변경 메서드를 트랜잭션 안에서 처리
-서비스 계층에서 트랜잭션을 시작하지 않으면 리파지토리에서 트랜잭션 시작
-서비스 계층에서 트랜잭션을 시작하면 리파지토리는 해당 트랜잭션을 전파 받아서 사용
-그래서 스프링 데이터 JPA를 사용할 때 트랜잭션이 없어도 데이터 등록, 변경이 가능했음 (사실은 트랜잭션이 리포지토리 계층에 걸려있는 것임)
참고: @Transactional의 readOnly 옵션. 내부적으로 true이든 false이든 같은 방식으로 동작하나, true일 경우 트랜잭션이 끝날 때 영속성 컨텍스트의 flush(DB와 동기화)를 호출하지 않는다.
*readOnly = true, 데이터베이스 커넥션에 setAutoCommit(false)
매우 중요!!!
save() 메서드
새로운 엔티티면 저장( persist )
새로운 엔티티가 아니면 병합( merge )
새로운 엔티티를 구별하는 방법
식별자가 객체일 때, null 로 판단
식별자가 자바 기본 타입(primitive)일 때, 0 으로 판단
Persistable 인터페이스를 구현해서 판단 로직 변경 가능
식별자 생성 전략
@GeneratedValue
save() 호출 시점에 식별자가 없으므로 새로운 엔티티로 인식해서 정상 동작한다.
직접 초기화
이미 식별자 값이 있는 상태로 save() 를 호출한다. 따라서 이 경우 merge() 가 호출된다.
merge() 는 우선 DB를 호출해서 값을 확인하고, DB에 값이 없으면 새로운 엔티티로 인지하므로 매우 비효율적이다.
따라서 Persistable 를 사용해서 새로운 엔티티 확인 여부(isNew())를 직접 구현하는 게 효과적이다.
참고로 등록시간( @CreatedDate )을 조합해서 사용하면 이 필드로 새로운 엔티티 여부를 편리하게 확인할 수 있다. (@CreatedDate에 값이 없으면 새로운 엔티티로 판단)
<Persistable 구현 예제>
@Entity
@EntityListeners(AuditingEntityListener.class)
public class EntityClass implements Persistable<T>{
@Id
private T id;
@CreatedDate
private LocalDateTime createdDate;
public boolean isNew() {
return createdDate == null;
}
...
}
'Advance I > Spring Data JPA' 카테고리의 다른 글
| 22.07.06 (0) | 2022.07.06 |
|---|---|
| 22.07.02 (0) | 2022.07.02 |
| 22.07.01 (0) | 2022.07.01 |
| 22.06.30 (0) | 2022.06.30 |
| 22.06.28 :: 실전! 스프링 데이터 JPA (0) | 2022.06.30 |