✏️ 2023-02-02 Today I Learn

@mitoconcrete · February 02, 2023 · 6 min read

1. Auditing 없이 Audit을 구현해보기 - createdby, modifiedby

auditing은 스프링이 엔티티의 상태를 감시(audit)하는 것인데, 주로 엔티티의 생성일자와 수정일자를 감시하여 엔티티 생성/수정 시점을 기록하는 timestamped기능을 구현할 때 사용한다. 엔티티의 상태는 @PostConstruct, @PrePersist, @PreUpdate, @PreDelete 등을 통해 엔티티의 생성직전, 영속상태직전, 업데이트직전, 삭제직전을 제어할 수 있다. 이를통해 createdby, modifiedby를 구현가능한데, 본래는 AuditorAware이라는 것을 이용해 getCurrentAuditor을 구현하여, 이곳에서 return되는 유저의 principal을 createdby, modifiedby에 넣어주는 방식이라고 한다.

시큐리티에서 제공하는 ContextHolder가 있어서 이걸통해 쉽게 Principal를 저장할 수 있지만, 굳이 이 기능구현을 위해 스프링시큐리티를 의존성주입하고 싶지않았다. 대신 HttpRequestSevlet 생성 시 Spring에서 static하게 생성해주는 RequestContextHolder를 이용해보기로 했다. 처음에는 RequestContextHolder가 HttpRequestSevlet과 비슷한 놈이라고 생각해서 계속 헤더에 username을 key값으로 하는 value를 셋팅해주려고 했는데, 잘 되지 않았다. 따라서, setAttribute를 이용해 RequestContextHolder를 사용하기로 했다.

이 한줄을 통해 나는 전역에서 HttpRequestSevlet에 넣어진 attribute를 가져올 수 있게 되었다.

...
  private String getCurrentUserName() {
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
    return (String) request.getAttribute("username");
  }
...

그럼 이걸 어떻게 이용하면 좋을까? 이제 엔티티에서도 해당 컨텍스트에 접근이 가능해지니, @PrePersist 시에 해당 컨텍스트에서 username을 가져와 넣어주고, @PreUpdate 시에도 해당컨텍스트에서 username에 가져와 넣어주면된다.

아래와 같이 우선 내가만든 UserStamped라는 MappedSuperClass를 상속받은 Message 클래스를 만들어주고,

public class Message extends UserStamped {
    ...
}

아래와 같이 해당 엔티티안에 라이프사이클어노테이션을 이용해, 적재적소에 상속받은 클래스의 메소드를 사용할 수 있도록 만들어준다.

...
  @PrePersist
  public void PrePersist() {
    super.updateCreatedBy();
    super.updateModifiedBy();
  }

  @PreUpdate
  public void PreUpdate() {
    super.updateModifiedBy();
  }

...

이렇게되면 엔티티의 생성/수정 시점에 contextholder안에 있는 username을 가져와, createdby, modifiedby에 넣어준다.

그럼 테스트는 어떻게 해야할까?
내 어플리케이션에는 엔티티와 레파지토리만 있는 상태였는데, 나는 굳이 이 기능을 테스트하기위해 컨트롤러와 서비스를 만들고 싶지않았다. 따라서, 테스트에서 가상의 request를 생성해서, 거기에서 값을꺼내와 contextholder에 넣어주면 되지 않을까..? 라고 생각했다.

하지만, HttpRequestServlet은 또 인터페이스라 구현할 수가 없었는데, 좀 찾아보니 HttpRequestServlet을 구현한 MockHttpRequestServlet 이라는 것이 있었고, 이것을 이용해서 가상의 request를 생성한 뒤 여기에 값을 넣어주고, 그것을 RequestContextHolder로 전달하는 방법이 있었다.

아래와 같이 구현했고, 성공적으로 db에 해당값들이 저장되는 것도 확인되었다.

    MockHttpServletRequest request = new MockHttpServletRequest(); // 가상의 request생성
    request.setAttribute("username", createUsername); // attribute할당
    RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); // context에 값 할당

2. 우당탕탕 JPA 심화

  • @DynamicInsert, @DynamicUpdate 를 통해 null column은 제외하고 쿼리를 날릴 수 있다. 이를 통해 쿼리최적화가 가능해진다.
  • Projection

    • select * 은 많은 리소스를 소모한다. 따라서 조회를 원하는 필드만 선언해서 조회를 해줘야한다.
    • get필드 메소드로 정의 - 원하는 필드만 조회가 가능해서 closed projection이라고 한다.
    • @Value로 정의. 전체 필드를 조회하고 그다음 원하는 필드를 조회하는방식이다. 전체필드를 한번은 조회하기 때문에 open projection이라고한다. 이후 spring expression language를 이용하여 원하는 필드를 호출한다.
    • 인터페이스, 클래스, 다이나믹 프로젝션이 가능하다.
  • quertbyexample - 예시객체를 만들어 그걸 조건절로 이용하는것

    • repository에 QueryByExampleExecutor를 추가해준다.
    • withIgnorePaths 를 통해 원하는 필드 이외에 다른 필드의 조회를 방지할 수 있다.
    • example을 잘 넣어주면 where 절을 이용해 조회가 가능해진다.
@mitoconcrete
어제보다 조금 더 성장하기 위해 기록합니다.