상세 컨텐츠

본문 제목

Spring 6.2의 Duration 포맷 지원 (오픈소스 기여까지!)

공부/Spring

by seungpang 2024. 12. 16. 15:00

본문

반응형

스프링 6.2부터 Duration을 다양한 포맷 스타일을 지원하는 새로운 기능이 추가되었다.

Kotlin에서는 이미 Duration.parse를 통해 다양한 표현식을 제공하고 있다.

val duration1 = Duration.parse("1.5h")
val duration2 = Duration.parse("1h 30m")

이와 비슷하게 스프링에서도 DurationFormatUtils를 통해 Duration의 다양한 표현식을 지원하게 되었다.

기존에는 밀리초 단위로만 입력했었는데 이제는 다양하게 표현할 수 있다.

Duration의 다양한 표현식


크게 3가지 형태로 나눌 수 있다.

  1. ISO8601
    • 기존 Duration.parse 로직과 동일하고 표준 ISO-8601 포맷(PT1H30M25)을 사용한다.
    • 예: PT1H(1시간), PT1H30M(1시간 30분)
  2. SIMPLE
    • 단순한 표현식으로 숫자와 짧은 단위 접미사를 사용한다.
    • 지원 단위:ns, us, ms, s, m, h, d
    • 예: 2h(2시간), 30m(30분), -3ms(3밀리 초 전)
  3. COMPOSITE
    • 여러 단위를 조합하여 표현하는 방식으로 더 직관적이고 가독성이 뛰어나다.
    • 예: 1h 30m(1시간 30분), -(2h 15m) (2시간 15분 전)

다양한 표현식이 가져올 수 있는 이점?


@Scheduled 애노테이션에 개선

@Component
public class MyScheduledTask {

    @Scheduled(fixedRateString = "2h 15m")
    public void taskWithFixedRate() {
        System.out.println("2시간 15분 간격으로 실행됩니다.");
    }

    @Scheduled(fixedDelayString = "10m")
    public void taskWithFixedDelay() {
        System.out.println("이전 작업이 종료된 후 10분 후에 실행됩니다.");
    }

    @Scheduled(initialDelayString = "5s", fixedRateString = "1h")
    public void taskWithInitialDelay() {
        System.out.println("5초 후에 시작하며, 매 1시간 간격으로 실행됩니다.");
    }
}

이제 @Scheduled 애노테이션에서 다양한 스타일을 인식하고 문자열로 표현된 초기 지연, 고정 지연 및 고정 속도를 파싱할 수 있다.

구성 파일에서 시간 설정

yaml파일이나 properties 파일에서 SIMPLE 또는 COMPOSITE 스타일을 사용해 유지보수성과 가독성을 향상시킬 수 있다.

task:
  rate: "1h 15m"
  delay: "30s"

동적 설정

다양한 비즈니스 로직에서 파싱 기능을 활용해 동적으로 시간을 설정할 수 있다.

    String durationString = fetchFromDatabase();
    Duration duration = DurationFormatterUtils.detectAndParse(durationString);
    scheduleTask(duration);

내부 동작 간단하게 살펴보기


DurationFormatterUtils 클래스를 보면 다양한 포맷 스타일을 처리하기 위해 다음과 같이 동작한다.

public static DurationFormat.Style detect(String value) {
    Assert.notNull(value, "Value must not be null");
    // warning: the order of parsing starts to matter if multiple patterns accept a plain integer (no unit suffix)
    if (ISO_8601_PATTERN.matcher(value).matches()) {
        return DurationFormat.Style.ISO8601;
    }
    if (SIMPLE_PATTERN.matcher(value).matches()) {
        return DurationFormat.Style.SIMPLE;
    }
    if (COMPOSITE_PATTERN.matcher(value).matches()) {
        return DurationFormat.Style.COMPOSITE;
    }
    throw new IllegalArgumentException("'" + value + "' is not a valid duration, cannot detect any known style");
}

public static Duration parse(String value, DurationFormat.Style style, DurationFormat.@Nullable Unit unit) {
    return switch (style) {
        case ISO8601 -> parseIso8601(value);
        case SIMPLE -> parseSimple(value, unit);
        case COMPOSITE -> parseComposite(value);
    };
}
  • 처음에는 입력 문자열이 어떤 스타일(ISO-8601, SIMPLE, COMPOSITE)인지 감지하기 위해 정규식을 사용한다.
  • parse 부분에서 style에 따라 파싱 메서드가 호출된다.

나머지 부분들은 각 스타일에 맞게 분리하여 Duration 객체로 변환하는 작업이다.

이 기능들을 살펴보고 테스트하는중 버그를 발견할 수 있었고 바로 스프링 프로젝트에 기여했다.

버그 발견 후 기여까지


DurationFormatterUtils을 테스트하는 도중에 parseComposite(value) 부분에서 빈 문자열("", " ")이 전달되면 예외를 던지지 않고 기본값으로 PT0S를 반환했다.

이는 빈 문자열이 유효하지 않은 입력임에도 불구하고 오해를 불러일으킬 수 있는 동작이여서 PR을 올리고 무사히 병합되었다.

내가 추가한 부분은 아주 간단했다.

    public static Duration parse(String value, DurationFormat.Style style, DurationFormat.@Nullable Unit unit) {
        Assert.hasText(value, () -> "Value must not be empty");
        return switch (style) {
            case ISO8601 -> parseIso8601(value);
            case SIMPLE -> parseSimple(value, unit);
            case COMPOSITE -> parseComposite(value);
        };
    }

parse메서드에 Assert.hasText(value, () -> "Value must not be empty");를 추가한게 전부다.

    @ParameterizedTest
    @EnumSource(DurationFormat.Style.class)
    void parseEmptyStringFailsWithDedicatedException(DurationFormat.Style style) {
        assertThatIllegalArgumentException()
                .isThrownBy(() -> DurationFormatterUtils.parse("", style))
                .withMessage("Value must not be empty");
    }
    @ParameterizedTest
    @EnumSource(DurationFormat.Style.class)
    void parseNullStringFailsWithDedicatedException(DurationFormat.Style style) {
        assertThatIllegalArgumentException()
                .isThrownBy(() -> DurationFormatterUtils.parse(null, style))
                .withMessage("Value must not be empty");
    }

그리고 그에 따른 테스트코드도 추가했다.

마치며


Spring 6.2에서 추가된 Duration 포맷 스타일 기능은 기존의 불편함을 해소하고 코드를 더욱 가독성 있게 만든다.

특히 다양한 포맷 스타일과 직관적인 표현 방식을 통해 개발자들은 더 생산적인 코드를 작성할 수 있다.

Spring의 새롭게 커밋된 내용을 보다가 새로운 기능도 알게되고 스프링 기여까지 하게 되었다.

Spring 코드들을 보다보면 의외로 다양하게 기여할 기회가 은근히 많은 것 같다.

다들 기회를 잡아보시길!

해당 기능에 대한 PR은 여기서 확인 가능하다.

기여한 PR은 여기서 확인 가능하다.

관련글 더보기