많은 테이블들에 작업자 컬럼이 존재한다. 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 이름 세팅이 가능해졌다.