
많은 테이블들에 작업자 컬럼이 존재한다. CreatedBy(생성자), LastModifiedBy(수정자) 컬럼이 있는 경우가 많다.
일일이 다 입력해주는 건 번거롭다. 이때 JPA와 AuditorAware를 통해 특정 필드에 지금 데이터 생성/수정자 정보를 자동으로 입력 해줄 수 있다.
그런데 만약, 서버 to 서버 API이기 때문에 session이 아닌 requestBody 등을 통해 컨트롤러/서비스 단에서 작업자를 변경해줘야 한다면?
작업자 값을 변경하기 위해 다음 방법들을 고려할 수 있다.
방법 1 : AuditorAware의 @LastModifiedBy 가 적용된 칼럼의 값은 더티 체킹이 먹히지 않는 문제가 발생했다.
방법 1, 2 : 커스텀으로(요청 바디값으로) 변경될 작업자 값이 변경이 전파되는 모든 엔티티에 적용된다. 따라서 모든 연관 엔티티에 직접 접근해 값을 수정해줘야 한다. 직접 전부 세팅해주는 공수가 많이 들고, 실수할 가능성이 커진다.
방법 3 : 존재하지 않는 헤더를 재설정 하는 게 찜찜하다.
따라서 방법 4로 작업자를 등록했다.
@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());
    }
}
@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 단위로 관리된다.)
즉 동시성 문제가 생길 수 있다.
/**
 * 작업자(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 주입이 가능한 곳에선 어디든 변경이 가능하다.
다음 고민들을 아래 코드로 해결했다.
@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 이름 세팅이 가능해졌다.