[Spring] CreatedBy, LastModifiedBy 필드를 Controller에서 바꿔치기 하는 법(feat.템플릿-콜백 패턴)

헌치·2023년 2월 28일
3

Spring

목록 보기
13/13
post-thumbnail

0. intro

많은 테이블들에 작업자 컬럼이 존재한다. CreatedBy(생성자), LastModifiedBy(수정자) 컬럼이 있는 경우가 많다.

일일이 다 입력해주는 건 번거롭다. 이때 JPA와 AuditorAware를 통해 특정 필드에 지금 데이터 생성/수정자 정보를 자동으로 입력 해줄 수 있다.

그런데 만약, 서버 to 서버 API이기 때문에 session이 아닌 requestBody 등을 통해 컨트롤러/서비스 단에서 작업자를 변경해줘야 한다면?

작업자 값을 변경하기 위해 다음 방법들을 고려할 수 있다.

  1. 더티체킹을 통해 작업자 값 변경
  2. (Spring data jpa 사용 가정) 직접 레포지토리에 값을 집어넣기
    2-1. @Modifying, @Query 를 통해 작업자 값을 집어넣는다.
    2-2. RepositoryImpl에서 QueryDsl로 작업자 값을 집어넣는다.
  3. 요청 헤더 값 중 하나를 requestBody 속 작업자 값으로 변경
  4. 중간에 작업자 값을 변경할 수 있도록 커스텀 AuditorAware 클래스를 만들기

방법 1 : AuditorAware의 @LastModifiedBy 가 적용된 칼럼의 값은 더티 체킹이 먹히지 않는 문제가 발생했다.

방법 1, 2 : 커스텀으로(요청 바디값으로) 변경될 작업자 값이 변경이 전파되는 모든 엔티티에 적용된다. 따라서 모든 연관 엔티티에 직접 접근해 값을 수정해줘야 한다. 직접 전부 세팅해주는 공수가 많이 들고, 실수할 가능성이 커진다.
방법 3 : 존재하지 않는 헤더를 재설정 하는 게 찜찜하다.

따라서 방법 4로 작업자를 등록했다.

1. Configuration으로 AuditorAware를 빈으로 등록한다.


@Configuration
public class FrontApiConfiguration {

    private final AuditorOverrideService auditorOverrideService;

    public FrontApiConfiguration(AuditorOverrideService auditorOverrideService) {
        this.auditorOverrideService = auditorOverrideService;
    }

    @Bean
    public AuditorAware<String> defaultAuditorAware() {
        return () -> Optional.of(auditorOverrideService.getCustomAuditor());
    }
}

2. ThreadLocal로 요청 별 AuditorAware을 분리한다.

@Service
public class AuditorOverrideService {
    private static final String INIT_AUDITOR = BmartAuditor.UNKNOWN;

    private final ThreadLocal<String> customField = ThreadLocal.withInitial(() -> INIT_AUDITOR);

    public String getCustomAuditor() {
        return customField.get();
    }

    private void updateCustomField(String auditor) {
        if (auditor == null || auditor.isEmpty()) {
            return;
        }
        customField.set(auditor);
    }

    private void resetCustomField() {
        customField.remove();
        customField.set(INIT_AUDITOR);
    }
}

해당 클래스(AuditorOverrideService)에서 자원의 할당, 해제를 관리한다.
Auditor는 각 요청 별로 관리되어야 하기 때문에 ThreadLocal을 사용한다.
@RequestScope를 사용하는 방법도 있지만, 속도 향상 및 호환성을 위해 이번엔 ThreadLocal을 사용하게 되었다.

static 대신 빈 내부 필드로 지정했다.

관련 논의:스택오버플로우
관련 논의:페이스북

주의점이 있다.
ThreadLocal은 비교적 빠른 대신 직접 자원 해제가 필요하며, 요청 별 별개의 value를 유지하기 어렵다.
(자원이 request 단위가 아닌 thread 단위로 관리된다.)
즉 동시성 문제가 생길 수 있다.

3. 템플릿-콜백 패턴을 활용해 자동 자원 할당-해제

/**
 * 작업자(createdBy, LastModifiedBy) 를 원하는 값으로 변경하는 기능을 담당.
 */
@Service
public class AuditorOverrideService {

    public <T> T runWithAuditor(String auditor, Supplier<T> supplier) {
        updateCustomField(auditor);
        T result = supplier.get();
        resetCustomField();
        return result;
    }
    
    private void resetCustomField() {
        customField.remove();
        customField.set(initAuditor);
    }
}

작업자 명을 받으면, bean 내부에서 언제든 auditor를 원하는 값으로 변경 가능하다.
템플릿-콜백 패턴을 사용했다.

이제 특정 요청에서 auditor 세팅을 변경하고 싶다면

@RestController
@RequestMapping
public class AController {

(...)

    @PostMapping("/)
    public ResponseEntity<Response> addExhibition(@RequestBody Request request) {
        return auditorOverrideService.runWithAuditor(request.getCreatedBy(), () -> {
            Response response = appService.register(request);
            return ResponseEntity.ok().body(response);
        });
        auditorOverrideService.resetCustomField();
    }

다음 방식으로 작업자 값을 컨트롤러 단에서 변경할 수 있다.
Bean 주입이 가능한 곳에선 어디든 변경이 가능하다.

4. 편의성 개선

  1. AuditorOverrideService 를 꼭 서비스로 만들 필요 없이, Auditor를 implement해도 되지 않을까? 그러면 초기 Auditor 명까지 주입받을 수 있다.
  2. 비즈니스 로직 도중 예외가 터질 시, threadLocal 자원 해제가 되지 않고 있다. 어떻게 자동으로 자원할당-해제를 관리할 수 있을까? (try-finally를 사용하면 된다.)
  3. Supplier 만 사용해야 할까? 리턴값이 필요하지 않을 시 runnable로 대체 가능하다.

다음 고민들을 아래 코드로 해결했다.

@EnableJpaAuditing
@Slf4j
public class OverrideAuditorAware implements AuditorAware<String> {
    private final String initAuditor;

    private final ThreadLocal<String> customField;

    public OverrideAuditorAware(String initAuditor) {
        this.initAuditor = initAuditor;
        this.customField = ThreadLocal.withInitial(() -> initAuditor);
    }

    @Override
    @NonNull
    public Optional<String> getCurrentAuditor() {
        return Optional.of(customField.get());
    }

    public <T> T supplyWithAuditor(String auditor, Supplier<T> supplier) {
        try {
            updateCustomField(auditor);
            return supplier.get();
        } finally {
            resetCustomField();
        }
    }

    public void runWithAuditor(String auditor, Runnable runnable) {
        try {
            updateCustomField(auditor);
            runnable.run();
        } finally {
            resetCustomField();
        }
    }

    private void updateCustomField(String auditor) {
        if (auditor == null || auditor.isEmpty()) {
            log.error("auditor 값은 비어있지 않아야 합니다. 기본 auditor 값인 '{}' 로 대체되었습니다.", initAuditor);
            return;
        }
        customField.set(auditor);
    }

    private void resetCustomField() {
        customField.remove();
        customField.set(initAuditor);
    }
}
@RestController
@RequestMapping
public class AController {

(...)

    @PostMapping("/)
    public ResponseEntity<Response> addExhibition(@RequestBody Request request) {
        return auditorOverrideService.runWithAuditor(request.getCreatedBy(), () -> {
            Response response = appService.register(request);
            return ResponseEntity.ok().body(response);
        });
    }

이제 직접 ThreadLocal 자원해제를 관리할 필요도, 도중 터지는 예외를 두려워하지 않아도 된다. 또한 모듈별 커스텀 초기 Auditor 이름 세팅이 가능해졌다.

5. 참고자료

토비의 스프링 3장 요약, 실습

profile
🌱 함께 자라는 중입니다 🚀 rerub0831@gmail.com

0개의 댓글