비즈니스 애플리케이션에서 작업 시간은 단순한 타임스탬프를 넘어서, 로그 추적, 거래 일관성, 감사(Audit) 요구사항 등에 핵심적인 역할을 한다. 특히 여러 처리 단계를 포함하는 비즈니스 유닛 내에서 시간의 일관성은 데이터 신뢰도를 결정짓는 중요한 요소다.
비즈니스 처리 유닛과 시간 흐름 특성
일반적으로 비즈니스 작업은 다음과 같은 두 가지 시간적 특성을 갖는다:
- 순차성 (Sequential Flow)
작업의 순서에 따라 시간이 점진적으로 기록된다. 예를 들어, 제출 후 승인 과정은 아래와 같이 나타난다:2024-01-01 12:12:01 요청 제출 2024-01-01 12:12:02 승인 완료 - 유닛 내 일관성 (Unit-level Consistency)
단일 비즈니스 작업 안에서 발생하는 모든 하위 이벤트는 동일한 기준 시간을 가져야 한다. 예를 들어 배치 처리 시나리오:2024-01-01 12:12:01 배치 B001 제출 2024-01-01 12:12:01 배치 B002 제출 2024-01-01 12:12:02 배치 B001 승인 2024-01-01 12:12:02 배치 B002 승인동일한 작업 유닛 내에서는 시간이 동기화되어 있어야 의미 있는 분석과 추적이 가능하다.
문제가 되는 구현 패턴
시간 일관성을 해칠 수 있는 일반적인 실수는 다음과 같다:
1. DAO 레이어에서 직접 데이터베이스 함수로 시간 기록
// 비추천
public void insertOrder() {
String sql = "INSERT INTO orders (created_at) VALUES (GETDATE())";
jdbcTemplate.execute(sql);
}
이 방식은 DB 서버의 시간 정책, 네트워크 지연, 클러스터 환경에서의 시간 차이로 인해 예측 불가능한 결과를 초래할 수 있다.
2. 서비스 내부에서 각 메서드마다 독립적으로 현재 시간 캡처
// 비추천
public void processPayment() {
logPayment("start", LocalDateTime.now());
executeBilling();
logPayment("complete", LocalDateTime.now()); // 다른 순간!
}
메서드 호출 사이에 짧은 지연만으로도 타임스탬프가 달라져, 하나의 논리적 작업임에도 불구하고 시간 왜곡이 발생한다.
권장되는 아키텍처 설계
비즈니스 유닛의 진입점(entry point)에서 **단일 기준 시간**을 확보하고, 이를 하위 계층에 전파하는 것이 핵심이다.
예시: 결제 처리
@RestController
public class PaymentController {
@PostMapping("/pay")
public ResponseEntity<Void> pay() {
LocalDateTime requestTime = localDateProvider.currentDateTime();
paymentService.executePayment(requestTime);
return ResponseEntity.ok().build();
}
}
@Service
public class PaymentService {
public void executePayment(LocalDateTime optTime) {
accountService.deduct(optTime);
orderService.confirm(optTime);
logService.recordPayment(optTime);
}
}
모든 하위 서비스는 동일한 optTime을 사용하므로, 계좌 차변, 주문 확정, 로그 기록이 모두 동일한 시각으로 처리된다.
확장 가능한 시간 제공자 설계
서버 로컬 시간(LocalDateTime.now())에 의존하는 것은 다국적 서비스나 UTC 기반 시스템에서 치명적이다. 대신 추상화된 시간 공급 인터페이스를 도입해야 한다.
public interface TimeProvider {
LocalDateTime currentDateTime();
}
구현 예시:
@Component
public class ConfigurableTimeProvider implements TimeProvider {
private final boolean useUtc;
private final Clock clock;
public ConfigurableTimeProvider(AppConfig config) {
this.useUtc = config.isUseUtc();
this.clock = useUtc ? Clock.systemUTC() : Clock.systemDefaultZone();
}
@Override
public LocalDateTime currentDateTime() {
return LocalDateTime.now(clock);
}
}
또는 NTP 서버 동기화, 테스트용 고정 시간 주입(Fixed Clock), 또는 메시지 큐의 이벤트 발생 시간 기반 처리 등 다양한 전략을 적용할 수 있다.
이처럼 TimeProvider를 DI 컨테이너를 통해 주입받아 사용하면, 시스템 전체의 시간 정책을 설정 파일이나 구성 옵션으로 통제할 수 있어 유지보수성과 유연성이 크게 향상된다.