23.07.27
엔티티의 이벤트를 감시하는 Auditing
Auditing 을 사용하면 엔티티를 누가 언제 생성/마지막 수정 했는지 자동으로 기록되게 할 수 있다.
Auditing 적용 방법
- 메인 애플리케이션 위에 @EnableJpaAuditing 추가
- 엔티티 클래스 위에 @EntityListeners(AuditingEntityListener.class) 추가
- AuditorAware 구현체 만들기
- createdAt, modifiedAt 은 구현체 없이 동작하지만 createdBy, modifiedBy 는 구현체가 필요하다.
- SpringSecurity 의 SecurityContextHolder 에서 인증정보안에 담긴 UserDetailsImpl 을 사용하여 user 객체를 가져와서 넣어준다.
- @EnableJpaAuditing에 AuditorAware 빈 이름 설정하기.
Auditing 적용 예시
@EnableJpaAuditing
@SpringBootApplication
public class Application {
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class TimeStamp {
@CreatedDate
private LocalDateTime createdAt;
@CreatedBy
@ManyToOne
private User createdBy;
@LastModifiedDate
private LocalDateTime modifiedAt;
@LastModifiedBy
@ManyToOne
private User modifiedBy;
}
@Service
public class UserAuditorAware implements AuditorAware<User> {
@Override
public Optional<User> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return Optional.empty();
}
return Optional.of(((UserDetailsImpl) authentication.getPrincipal()).getUser());
}
}
@EnableJpaAuditing(auditorAwareRef = "userAuditorAware") // auditorAware 의 빈이름을 넣어준다.
@SpringBootApplication
public class Application {
Auditing 직접 구현해보기
- 생성일시, 생성자, 수정일시, 수정자는 결국 엔티티의 영속성이 변경될때 저장한다.
- 엔티티의 영속성이 변경되는 생성 > 수정 > 삭제 이 흐름을 엔티티 라이프 사이클 이벤트라고 한다.
- Auditing 도 이러한 엔티티의 라이프 사이클 이벤트를 통해 구현하고있다.
- 우린 엔티티 라이프 사이클을 직접 관리하여 구현할 수 있다.
- 객체가 생성되면 자동으로 실행하도록 메소드에 붙이는 @PostConstruct 의 원리와 같다.
엔티티 저장 이벤트
전 : @PrePersist : EntityManager 가 엔티티를 영속성상태로 만들기 직전에 메소드 수행
후 : @PostPersist : EntityManager 가 엔티티를 영속성상태로 만든 직후에 메소드 수행
엔티티 수정 이벤트
전 : @PreUpdate : EntityManager 가 엔티티를 갱신상태로 만들기 직전에 메소드 수행
후 : @PostUpdate : EntityManager 가 엔티티를 갱신상태로 만든 직후에 메소드 수행
엔티티 삭제 이벤트
전 : @PerRemove : EntityManager 가 엔티티를 삭제상태로 만들기 직전에 메소드 수행
후 : @PostRemove : : EntityManager 가 엔티티를 삭제상태로 만든 직후에 메소드 수행
HATEOAS
Hypermedia As The Engine of Application State 의 약자
간단히 말해서 다음 요청을 위한 하이퍼링크가 제공되어야 한다
HATEOAS 적용 예시
{
"data": {
"id": 1000,
"name": "게시글 1",
"content": "HAL JSON을 이용한 예시 JSON",
"self": "http://localhost:8080/api/post/1000", // 호출한 api 주소
"profile": "http://localhost:8080/docs#query-article", // 호출한 api 문서
"next": "http://localhost:8080/api/post/1001", // 다음 post를 조회 api 주소
"comment": "http://localhost:8080/api/post/comment", // post의 댓글 달기 api 주소
"save": "http://localhost:8080/api/feed/post/1000", // post을 내 피드로 저장 api 주소
},
}
적용 방법
- HATEOAS 의존성 추가 (spring-boot-starter-hateoas)
// 9. SpringBoot HATEOAS 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-hateoas'
- 핸들러 매개변수로 PagedResourcesAssembler 넣고 PagedModel 로 응답
@RestController
public class ChannelController {
@Autowired
ChannelRepository channelRepository;
@GetMapping("/channels")
public PagedModel<User> getUsers(Pageable pageable, PagedResourcesAssembler<User> assembler) {
var all = channelRepository.findAll(pageable);
return assembler.toModel(all);
}
}
1. JPQL : Table 이 아닌 Entity(객체) 기준으로 작성하는 쿼리를 JPQL 이라고 하며 이를 사용할 수 있도록 EntityManger 또는 @Query 구현체를 통해 JPQL 쿼리를 사용할 수 있다.
1-1. EntityMananger.createQuery()
- 쿼리 문자열과 Entity 를 직접 넣어서 쿼리를 작성한다.
- setParameter 와 같이 key, value 문자열을 통해서 쿼리 파라미터를 매핑할 수 있다.
여기서 잠깐!! 코드에 문자열이 들어가는게 왜 안좋은건가요?
(면접 질문 가능✔️)
- 문자열은 오타가 발생할 여지가 많다.
- 개발할때 같은 공통적인 문자열이 있을때 한군데에서 수정이 일어나면 모두 수정해야한다.
- 잘못된 코드가 있더라도 문자열 자체를 컴파일러가 검사 하지는 않기 때문에 컴파일 시점에 잡지못한다.
- 이로인해 버그가 있더라도 메소드를 실행하는 시점인 런타임시점에 버그가 발생한다.
- 런타임 시점에 발생한 버그는 서비스 정합성에 영향을 주며 원인을 찾기도 어렵다.
해결방법,
- 문자열을 포함하여 구현된 기능들은 객체화 또는 함수화 해서 컴파일시 체크되도록 한다.
- 문자열로 선언된 변수들은 상수로 선언하여 공통적으로 관리한다. (상수 클래스 선언 추천 👍)
1-2. @Qeury (repository interface)
- @Query 의 인자값으로 간단하게 쿼리를 작성할 수 있습니다.
- 쿼리를 작성할때는 테이블명이 아니라 Entity 명으로 조회하게 됩니다.
- 변수 바인딩은 2가지 방법으로 할 수 있습니다.
- ?변수순번 사용
- :변수명 사용 > 순서에 영향을 받지 않기 때문에 더 안전
2. QueryDSL (JPAQueryFactory)
- QueryDSL 간단 정리!
- Entity 의 매핑정보를 활용하여 쿼리에 적합하도록 쿼리 전용 클래스(Q클래스)로 재구성해주는 기술
- 여기에 JPAQueryFactory 을 통한 Q클래스를 활용할 수 있는 기능들을 제공한다.
- 그럼, JPAQueryFactory 는 뭐야?
- 재구성한 Q클래스를 통해 문자열이 아닌 객체 또는 함수로 쿼리를 작성하고 실행하게 해주는 기술
방법
및준비
build.gradle 환경설정
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
// QueryDSL JPA
implementation 'com.querydsl:querydsl-jpa'
// QFile 생성 및 가져오기
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
config 패키지
@Configuration
public class QuerydslConfiguration {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
1. 기존 JpaRepository를 구현하는 Repositroy 이름에 분별용 이름을 붙인 interface를 하나 생성한다.
ex) UserRepository > UserRepositoryCustom or UserRepositoryQuery ...
2. 여기에 QueryDSL을 이용해 구현할 기능을 추가
3. 이 기능은 해당 Repository을 상속하면서 뒤에 Impl이 추가된 클래스에서 구현
4. QueryDSL을 구현할 수 있게 private final JPAQueryFactory jpaQueryFactory;를 주입하고, 1에서 생성한 Repository를 기존 Repository에서 상속한다.
@DynamicInsert
이 어노테이션을 엔티티에 적용하게 되면 Insert 쿼리를 날릴 때 null 인 값은 제외하고 쿼리문이 만들어진다.
적용 방법
- Entity 에 @DynamicInsert 어노테이션을 붙여주면 끝!
@DynamicUpdate
이 어노테이션을 엔티티에 적용하게 되면 Update 쿼리를 날릴 때 null인 값은 제외하고 쿼리문이 만들어진다.
적용 방법
- @DynamicInsert와 마찬가지로 Entity에 @DynamicUpdate를 붙여주면 된다.
null인 값은 업데이트를 안하므로 그만큼 처리속도가 빨라진다.
JPARepository 로 find() 또는 findAll() 메소드를 사용할때 Entity 단위로 조회해온다.
이걸 속칭 별쿼리라고 합니다. SELECT * FROM USER;
이렇게 조회하면 필드가 많아질수록 느려진다.
일부 필드만 조회해서 성능을 최적화 하려면 어떻게 해야할까 > Proejction 기능을 사용해서 조회하고 싶은 필드를 지정
Projection의 기능
- 원하는 필드만 지정해서 조회 가능
- 여러필드 합쳐서 재정의 필드(Alias) 조회 가능 (Nested 프로젝션)
- 조회 필드 그룹을 인터페이스 또는 클래스로 만들어놓고 재사용 가능
Porjection 필드 사용방법
1. get필드() 메소드로 정의
- 정의한 필드만 조회하기 때문에 Closed 프로젝션 이라고 한다.
- 쿼리를 줄이므로 최적화 할 수 있다.
- 메소드 형태이기 때문에 Java 8의 메소드를 사용해서 연산을 할 수 있다.
2. @Value 로 정의
- 전체 필드를 조회할 수 밖에 없어서 Open 프로젝션 이라고 한다.
- @Value(SpEL)을 사용해서 연산을 할 수 있다.
- 스프링 빈 들의 메소드도 호출 가능하다.
- 쿼리 최적화를 할 수 없다. SpEL을 엔티티 대상으로 사용하기 때문에.
스프링 빈 메서드 호출 예시
// workersHolder 는 bean 으로 등록한 contextHolder
@Value("#{workersHolder.salaryByWorkers['George']}")
private Integer georgeSalary;
Projection 구현체 정의방법
1. 인터페이스 기반 Projection : Projection 을 Interface 처럼 사용하는 방법
2. 클래스 기반 Projection : Projection 을 DTO 클래스 처럼 사용하는 방법
3. 다이나믹 Projection : Projection 클래스를 동적으로 지정해서 사용하는 방법
Query by Example : 받고싶은 예시객체를 만들어서 조건절로 사용하는 기술로 예제 객체를 가지고 쿼리를 만드는 개념
(현업에서 잘 쓰지는 않는다.)
구성요소
Example
- Example은 Probe 과 ExampleMatcher 을 하나로 합친 것 이걸로 쿼리를 수행합니다.
Probe
- Probe는 필드에 어떤 값들을 가지고 있는 도메인 객체
ExampleMatcher
- ExampleMatcher는 Prove에 들어있는 그 필드의 값들을 어떻게 쿼리할 데이터와 비교할지 정의한 것
기능
- 별다른 코드 생성기(QClass 같은)나 애노테이션 처리기(@Qeury같은) 필요 없이 그냥 쓰면된다.
- 도메인 객체가 수정되면 같이 반영됨 (필드나 함수를 그대로 쓰기때문에)
- 독립적인 인터페이스를 가져서 영향도가 적다.
제한사항
- 여러필드 조합해서 조건만드는 nested 또는 자식 Collection 제약 조건을 못 만든다.
- 문자열은 starts/contains/ends/regex 가 가능하고 그밖에 필드는 값이 정확히 일치해야 한다
QueryByExampleExecutor
- Repository Interface 에 QueryByExampleExecutor 의존성을 추가해주면 됩니다.
public interface UserRepository extends JpaRepository<User, Long>, QueryByExampleExecutor<User> {
7월 27일 개인과제를 위해 QueryDSL을 사용해서 검색기능을 구현해보려고 한다.
다만 조건을 넣은 기능을 힘들 것 같아 일단 존재여부만 판단하는 기능을 만들어봤다.
기존에 존재하던 findByUsername기능을 구현해봤다.
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryQuery {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
}
public interface UserRepositoryQuery {
Optional<User> findByUsernameByQuery(String username);
}
@RequiredArgsConstructor
public class UserRepositoryQueryImpl implements UserRepositoryQuery{
private final JPAQueryFactory jpaQueryFactory;
@Override
public Optional<User> findByUsernameByQuery(String username) {
User foundUser = jpaQueryFactory.selectFrom(user)
.where(user.username.eq(username))
.fetchOne();
return Optional.ofNullable(foundUser);
}
}
// 회원 중복 확인
// Optional<User> checkUsername = userRepository.findByUsername(username);
Optional<User> checkUsername = userRepository.findByUsernameByQuery(username);
if (checkUsername.isPresent()) {
throw new IllegalArgumentException("중복된 사용자가 존재합니다.");
}