본문 바로가기
TIL/Spring

23.07.27

by J1-H00N 2023. 7. 27.

엔티티의 이벤트를 감시하는 Auditing

Auditing 을 사용하면 엔티티를 누가 언제 생성/마지막 수정 했는지 자동으로 기록되게 할 수 있다.

 

Auditing 적용 방법

  1. 메인 애플리케이션 위에 @EnableJpaAuditing 추가
  2. 엔티티 클래스 위에 @EntityListeners(AuditingEntityListener.class) 추가
  3. AuditorAware 구현체 만들기
    • createdAt, modifiedAt 은 구현체 없이 동작하지만 createdBy, modifiedBy 는 구현체가 필요하다.
    • SpringSecurity 의 SecurityContextHolder 에서 인증정보안에 담긴 UserDetailsImpl 을 사용하여 user 객체를 가져와서 넣어준다.
  4. @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. 개발할때 같은 공통적인 문자열이 있을때 한군데에서 수정이 일어나면 모두 수정해야한다.
  3. 잘못된 코드가 있더라도 문자열 자체를 컴파일러가 검사 하지는 않기 때문에 컴파일 시점에 잡지못한다.
  4. 이로인해 버그가 있더라도 메소드를 실행하는 시점인 런타임시점에 버그가 발생한다.
  5. 런타임 시점에 발생한 버그는 서비스 정합성에 영향을 주며 원인을 찾기도 어렵다.

해결방법,

  • 문자열을 포함하여 구현된 기능들은 객체화 또는 함수화 해서 컴파일시 체크되도록 한다.
  • 문자열로 선언된 변수들은 상수로 선언하여 공통적으로 관리한다. (상수 클래스 선언 추천 👍)

 

1-2. @Qeury (repository interface)

  • @Query 의 인자값으로 간단하게 쿼리를 작성할 수 있습니다.
    • 쿼리를 작성할때는 테이블명이 아니라 Entity 명으로 조회하게 됩니다.
  • 변수 바인딩은 2가지 방법으로 할 수 있습니다.
    1. ?변수순번 사용
    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의 기능

  1. 원하는 필드만 지정해서 조회 가능
  2. 여러필드 합쳐서 재정의 필드(Alias) 조회 가능 (Nested 프로젝션)
  3. 조회 필드 그룹을 인터페이스 또는 클래스로 만들어놓고 재사용 가능

 

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("중복된 사용자가 존재합니다.");
        }

'TIL > Spring' 카테고리의 다른 글

23.08.03  (0) 2023.08.03
23.07.28  (0) 2023.07.28
23.07.26  (0) 2023.07.26
23.07.25  (0) 2023.07.25
23.07.12  (0) 2023.07.12