profile image

L o a d i n g . . .

728x90

최근에 개발한 배치 프로그램이 로컬에서 실행했을 때와 배포 환경에서 실행했을 때 조회하고 생성하는 데이터 간에 차이가 발생하는걸 확인했습니다. 동일한 설정으로 배포했는데 대체 어떻게 문제가 발생하는 건지... 원인을 찾아내기까지 오랜 시간이 걸렸습니다. 문제는 배치 프로그램에서 사용하는 LocalDate.now()였습니다. 이번 포스팅에서는 LocalDate.now()가 어떻게 문제를 유발했는지, 그리고 어떻게 해결했는지에 대해 공유하고자 합니다. 

 

원인 분석 

배치 프로그램은 05:30, 07:30, 09:30에 실행하도록 스케줄링 돼있습니다. 배치 프로그램에 date와 관련된 일자를 인자로 넘기지 않으면 해당 배치 프로그램은 LocalDate.now()를 기준으로 배치 실행 일자를 결정하게됩니다.

그런데 신기한 건 "2023-08-30"일자에 실행된 3번의 배치에서 09:30은 정상적으로 "2023-08-30"일자의 데이터를 조회하는데, 05:30과 07:30에 실행된 배치는 "2023-08-29"일자의 데이터를 조회하고 있었습니다. 대체 원인이 무엇이냐.... 그것은 바로 배치가 실행되는 환경(쿠버네티스를 사용하고 있습니다)의 타임존이 UTC이기 때문이었습니다. 배치 실행 시간을 UTC로 변환하면 아래와 같습니다. 

배치 실행 시간(UTC+09:00)  UTC 변환 
2023-08-30 05:30 2023-08-29 20:30
2023-08-30 07:30 2023-08-29 22:30
2023-08-30 09:30 2023-08-30 00:30

UTC로 변환됐기 때문에 앞선 2번의 배치는 "2023-08-29"일자의 데이터를 조회하고 마지막에 실행된 배치는 "2023-08-30"일자의 데이터를 조회한게 원인이었습니다. 

 

문제 해결

문제를 해결하기 위해 LocalDate.now()가 아닌 LocalDate.now(ZoneId zone) 메서드를 사용하도록 변환하였습니다. 쿠버네티스 환경의 타임존이 UTC 기준이더라도 ZoneId를 인자로 넘겨주면 ZoneId을 고려해 일자를 결정합니다. 

ZoneId를 설정한 경우

문제는 해결됐지만 한 가지 의문이 남았습니다. 로컬 개발환경과 배포환경에서의 시스템 시간이 다를 때 어떻게 테스트를 수행해야 하는지 궁금해졌습니다. 그래서 LocalDate에 대해 더 분석한 결과 LocalDate의 javadoc에서 그 실마리를 찾을 수 있었습니다. Javadoc에서는 LocalDate.now()와 LocalDate.now(ZoneId zone)를 사용하면 테스트 시 문제가 발생할 수 있음을 경고하고 있었습니다. 

LocalDate now(ZoneId zone) Javadoc

그럼 어떤 메서드를 사용하면 위 문제를 해결할 수 있을까요? 바로 Clock을 인자로 받는 LocalDate의 메서드를 활용하는 것입니다. 

Dependency injection을 활용해 테스트를 할 수 있다는데 Spring에서는 어떻게 코드를 작성해야 하는지 살펴보겠습니다. 

@Configuration
class ClockConfig {

    @Bean
    public Clock clock() {
        return Clock.system(ZoneId.of("Asia/Tokyo"));
    }
}

@Service
class LocalDateTimeService {

    private final Clock clock;

    public LocalDateTimeService(Clock clock) {
        this.clock = clock;
    }

    public LocalDateTime now() {
        return LocalDateTime.now(clock);
    }
}

위 코드는 Clock을 @Bean으로 설정해서 LocalDateTimeService가 의존성으로 주입받는 코드입니다. 그럼 LocalDateTimeService의 now()를 테스트해 보겠습니다. 

@ExtendWith(MockitoExtension.class)
class LocalDateTimeServiceTest {

    @Mock
    Clock clock;

    @InjectMocks
    LocalDateTimeService service;

    @Test
    void localDateTimeServiceTest() {
        when(clock.instant()).thenReturn(Instant.parse("2023-09-01T00:00:00Z"));

        when(clock.getZone()).thenReturn(ZoneId.of("UTC"));
        System.out.println(service.now());

        when(clock.getZone()).thenReturn(ZoneId.of("Asia/Seoul"));
        System.out.println(service.now());
    }
}

테스트 결과

테스트 결과를 보면 service.now()를 호출 시 clock의 ZoneId에 따라 결과가 달라지는 것을 확인할 수 있습니다. 시스템에서 시간과 관련된 데이터가 필요할 때 LocalDateTimeService와 같은 시간과 관련 메서드를 제공하는 클래스를 두고 Clock을 주입받아 사용하면 타임존이 다른 플랫폼에서 안정적으로 동작하는 애플리케이션을 만들 수 있습니다. 

728x90
복사했습니다!