자동화 QA (품질 보증)수석 자동화 QA 엔지니어

시간에 의존하는 비즈니스 워크플로우의 검증을 자동화하기 위한 아키텍처 접근 방식을 제안하시오. 이 방식은 전 세계 시간대 경계를 초월하여 결정론적 실행을 보장하고, 일광 절약 시간제(DST) 이상을 적절히 처리하며, 시스템 시계 수정을 의존하지 않고 윤초(leap second) 이벤트를 시뮬레이션해야 합니다.

Hintsage AI 어시스턴트로 면접 통과

질문에 대한 답변

배경: 시간에 의존하는 로직 테스트는 전통적으로 System.currentTimeMillis() 호출이나 Thread.sleep() 문에 의존하여 만들어졌으며, 이는 취약하고 느린 테스트를 초래하였으며, 자정 근처에 실행할 경우 간헐적으로 실패하였습니다. 초기 자동화 프레임워크는 Docker 컨테이너 내에서 OS 시스템 시계를 조작하려고 했으나, 이는 공유 CI/CD 인프라 전반에 걸쳐 연쇄적인 실패를 초래했습니다. 현대적 접근 방식은 시간을 데이터베이스나 HTTP 서비스와 유사한 의존성으로 취급해야 하며, 추상화 계층을 통해 결정론적 제어를 가능하게 합니다.

문제: 분산 마이크로서비스는 지역 시간이 건너뛰거나 반복되는 DST 전환, UTC에 추가 시간이 삽입되는 윤초, 존재하지 않는 시간을 참조할 수 있는 cron 표현식을 처리해야 합니다. 적절한 격리가 없으면 "월말" 처리를 위한 테스트는 시간 경계 근처에서 불안정해집니다. 더불어, 40개 이상의 전 세계적인 시간대에서의 동작을 검증하려면 수천 개의 테스트 조합을 실행해야 하며, 이는 실제 시간 진행을 사용하여 수년이 걸릴 수 있습니다.

해결책: TimeProvider 추상화를 구현하여 JavaClock 인터페이스를 사용하여 동결된, 오프셋된 또는 가속된 시간 소스를 주입할 수 있게 합니다. 실제 데이터베이스 인스턴스를 실행하는 TestContainers와 결합하되, 애플리케이션 시계를 컨테이너 OS 시계가 아닌 추상화를 통해 제어합니다. JUnit 매개변수화된 테스트를 사용하여 시간대 전환 데이터 세트를 순회하여 일관된 동작을 보장합니다.

public interface TimeProvider { Instant now(); ZonedDateTime nowInZone(ZoneId zone); } public class MutableClock implements TimeProvider { private Instant frozenInstant; public void setTime(Instant instant) { this.frozenInstant = instant; } @Override public ZonedDateTime nowInZone(ZoneId zone) { return frozenInstant.atZone(zone); } } public class BillingScheduler { private final TimeProvider clock; public BillingScheduler(TimeProvider clock) { this.clock = clock; } public boolean isEndOfBillingCycle(LocalDate date, ZoneId zone) { ZonedDateTime now = clock.nowInZone(zone); return now.toLocalDate().equals(date) && now.getHour() == 0; } } @Test public void testDSTSpringForward() { MutableClock clock = new MutableClock(); clock.setTime(Instant.parse("2024-03-10T07:30:00Z")); BillingScheduler scheduler = new BillingScheduler(clock); // 검증 로직은 여기 }

실제 상황 예시

세부 예시: 글로벌 핀테크 플랫폼은 @Scheduled(cron = "0 0 2 * * ?")로 구성된 Spring Boot 스케줄러를 사용하여 일일 초과 인출 수수료를 계산했습니다. 2023년 3월 미국에서의 DST 전환 동안, 동부 시간대의 고객은 "구시각" 2:00 AM (EST)과 "신시각" 2:00 AM (EDT) 모두에서 작업이 실행되었기 때문에 두 번 요금을 부과 받았습니다. QA 팀은 이 같은 일이 다시 발생하지 않도록 방지하고, 수정이 다른 12개 국제 시장에서도 잘 작동하는지 확인해야 했습니다.

문제 설명: 기존 테스트 스위트는 실제 시간 진행을 기다리기 위해 Awaitility에 의존하였으며, 특정 날짜의 2:00 AM에서 수동 실행 없이는 DST 테스트가 불가능했습니다. 팀은 quartz 스케줄러가 "상실된 시간"을 존중하는지 그리고 데이터베이스 타임스탬프가 UTC에서 로컬 비즈니스 날짜에 올바르게 매핑되는지 검증해야 했습니다.

고려된 다양한 해결책:

해결책 1: 특권 컨테이너 시계 조작 팀은 --privileged 플래그를 가진 Docker 컨테이너에서 시스템 날짜를 수정하기 위해 date 명령을 사용하는 것을 고려했습니다. 이는 실제 JVM 시간대 데이터베이스와 OS 수준의 cron 동작을 테스트할 것입니다. 장점: 프로덕션 인프라와 최대의 충실도; 실제 libc 시간대 처리를 검증합니다. 단점: 호스트 시계 변경으로 인해 모든 컨테이너에 영향을 미친다; Kubernetes 보안 컨텍스트 위반이 필요하다; 시계 조정 중 경합 조건으로 인해 불안정한 테스트를 생성합니다.

해결책 2: 관점 지향 프로그래밍 인터셉션 AspectJ를 사용하여 java.time.Instant.now() 호출을 가로채고, 애플리케이션 코드를 수정하지 않고 테스트 제어 소스로 리디렉션합니다. 장점: 레거시 모놀리스를 위한 리팩토링이 필요 없다; 표준 시간 API를 사용하는 타사 라이브러리와 호환됩니다. 단점: 복잡한 바이트코드 다림질 구성; 새로운 JDK의 Java 모듈 시스템 (JPMS)과 호환되지 않음; Jackson 직렬화기에서 사용자 정의 시간 파싱 로직을 테스트하지 않습니다.

해결책 3: 의존성 주입을 통한 아키텍처 리팩토링 모든 시간 인식 구성 요소를 구성자 주입을 통해 Clock 인터페이스를 수용하도록 리팩토링 하며, 프로덕션에서는 시스템 시계를 제공하고 JUnit 테스트에서는 테스트 더블을 제공합니다. 장점: 결정론적이고 즉각적인 테스트 실행; 여러 시간대를 동시에 병렬로 테스트를 지원; 비상 시나리오와 같은 불가능한 시나리오를 테스트합니다. 단점: 정적 LocalDateTime.now() 호출을 리팩토링하기 위한 초기 개발 노력이 필요하다; 개발자가 추상화를 우회하지 않도록 팀 교육이 필요합니다.

선택한 해결책 및 이유: 우리는 결정론적 피드백을 수 밀리초 내에 제공할 수 있는 해결책 3을 선택했습니다. 팀은 Javajava.time.Clock를 사용하여 TimeContext 클래스를 구현하고 두 스프린트에 걸쳐 150개 이상의 서비스 클래스를 리팩토링했습니다. 우리는 이를 AWS 계정 내에서 격리된 "시공간 혼란" 테스트와 함께 사용하여 인프라 수준의 문제를 포착했습니다.

결과: 이 프레임워크는 생산 배포 전 브라질 시간대 처리에서 7개의 핵심 버그를 식별했습니다. 스케줄링 모듈의 테스트 실행 시간은 4시간에서 45초로 단축되었습니다. 이 솔루션은 이전에 특정 천문 이벤트를 기다려야 했던 "윤초" 시나리오 검증을 가능하게 했습니다.