'java.time.format.DateTimeParseException' 처리 방법

JavaBeginner
지금 연습하기

소개

Java 애플리케이션에서 날짜 및 시간 데이터를 다룰 때, 개발자는 종종 java.time.format.DateTimeParseException 예외를 마주하게 됩니다. 이 예외는 애플리케이션이 문자열을 날짜 또는 시간 객체로 파싱하려고 시도하지만, 문자열 형식이 예상 패턴과 일치하지 않거나 유효하지 않은 값을 포함할 때 발생합니다.

이 Lab 에서는 DateTimeParseException의 원인을 식별하고, 이를 해결하기 위한 효과적인 솔루션을 구현하며, Java 애플리케이션에서 안정적인 날짜 및 시간 파싱을 위한 모범 사례를 채택하는 방법을 배우게 됩니다.

DateTimeParseException 이해하기

java.time.format.DateTimeParseException는 Java 애플리케이션에서 날짜 및 시간 문자열을 파싱할 때 발생하는 일반적인 런타임 예외입니다. 이 예외를 효과적으로 처리하려면, 무엇이 원인인지 그리고 어떻게 식별하는지 이해해야 합니다.

DateTimeParseException 의 원인

DateTimeParseException은 일반적으로 다음 이유 중 하나로 발생합니다.

  1. 형식 불일치 (Format mismatch): 입력 문자열 형식이 DateTimeFormatter에 지정된 패턴과 일치하지 않습니다.
  2. 잘못된 날짜/시간 값 (Invalid date/time values): 입력 문자열에 유효하지 않은 날짜 또는 시간을 나타내는 값 (예: 2 월 30 일) 이 포함되어 있습니다.
  3. 누락되거나 추가된 요소 (Missing or extra elements): 입력 문자열에 필수 요소가 누락되었거나 예상치 못한 추가 요소가 포함되어 있을 수 있습니다.

전형적인 DateTimeParseException 시나리오를 보여주기 위해 간단한 Java 프로그램을 만들어 보겠습니다. 먼저, WebIDE 를 열고 새 Java 파일을 만듭니다.

  1. 왼쪽 사이드바에서 파일 탐색기 섹션으로 이동합니다.
  2. /home/labex/project 폴더를 마우스 오른쪽 버튼으로 클릭합니다.
  3. "New File"을 선택하고 이름을 ParseExceptionDemo.java로 지정합니다.

이제 다음 코드를 복사하여 파일에 붙여넣습니다.

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

public class ParseExceptionDemo {
    public static void main(String[] args) {
        // Example 1: Format mismatch
        String dateStr1 = "2023/05/15";

        try {
            // Trying to parse with incorrect format (expects yyyy-MM-dd)
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
            LocalDate date = LocalDate.parse(dateStr1, formatter);
            System.out.println("Parsed date: " + date);
        } catch (DateTimeParseException e) {
            System.out.println("Example 1 - Format mismatch error:");
            System.out.println("Exception: " + e.getClass().getName());
            System.out.println("Message: " + e.getMessage());
            System.out.println("Error position: " + e.getErrorIndex());
        }

        System.out.println("\n----------------------------------------\n");

        // Example 2: Invalid date value
        String dateStr2 = "2023-02-30";

        try {
            // February 30th is an invalid date
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
            LocalDate date = LocalDate.parse(dateStr2, formatter);
            System.out.println("Parsed date: " + date);
        } catch (DateTimeParseException e) {
            System.out.println("Example 2 - Invalid date error:");
            System.out.println("Exception: " + e.getClass().getName());
            System.out.println("Message: " + e.getMessage());
            System.out.println("Input string: " + e.getParsedString());
        }
    }
}

이 프로그램을 컴파일하고 실행하려면 WebIDE 에서 터미널을 열고 다음 명령을 실행합니다.

cd ~/project
javac ParseExceptionDemo.java
java ParseExceptionDemo

다음과 유사한 출력을 볼 수 있습니다.

Example 1 - Format mismatch error:
Exception: java.time.format.DateTimeParseException
Message: Text '2023/05/15' could not be parsed at index 4
Error position: 4

----------------------------------------

Example 2 - Invalid date error:
Exception: java.time.format.DateTimeParseException
Message: Text '2023-02-30' could not be parsed: Invalid date 'February 30'
Input string: 2023-02-30

DateTimeParseException 분석

예외 정보를 분석해 보겠습니다.

  1. 첫 번째 예제에서, 슬래시 (/) 가 있는 날짜 (2023/05/15) 를 하이픈 (yyyy-MM-dd) 을 예상하는 포맷터로 파싱하려고 시도했기 때문에 예외가 발생합니다. error index 4는 파싱이 실패한 지점인 첫 번째 슬래시 문자를 가리킵니다.

  2. 두 번째 예제에서, 2 월 30 일은 어떤 해에도 유효한 날짜가 아니기 때문에 예외가 발생합니다. 형식은 일치하지만 실제 날짜 값은 유효하지 않습니다.

DateTimeParseException은 파싱 문제를 진단하는 데 도움이 되는 유용한 정보를 제공합니다.

  • 오류 메시지는 무엇이 잘못되었는지 설명합니다.
  • getParsedString() 메서드는 파싱할 수 없었던 입력 문자열을 반환합니다.
  • getErrorIndex() 메서드는 파싱이 실패한 위치를 반환합니다.

이러한 세부 사항을 이해하는 것은 애플리케이션에서 날짜 파싱 문제를 효과적으로 디버깅하고 해결하는 데 필수적입니다.

형식 불일치 문제 해결

DateTimeParseException의 가장 흔한 원인 중 하나는 입력 문자열 형식과 DateTimeFormatter가 예상하는 형식 간의 불일치입니다. 이 단계에서는 이러한 유형의 문제를 해결하는 방법을 배우겠습니다.

올바른 형식 패턴 사용

DateTimeFormatter를 사용할 때는 입력 문자열의 실제 형식과 일치하는 패턴을 지정하는 것이 중요합니다. Java 는 날짜와 시간이 정확히 어떻게 형식화되는지 정의할 수 있는 유연한 패턴 구문을 제공합니다.

형식 불일치 문제를 해결하는 방법을 보여주기 위해 새 파일을 만들어 보겠습니다.

  1. WebIDE 에서 /home/labex/project 폴더를 마우스 오른쪽 버튼으로 클릭합니다.
  2. "New File"을 선택하고 이름을 FormatMismatchSolution.java로 지정합니다.
  3. 다음 코드를 복사하여 붙여넣습니다.
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

public class FormatMismatchSolution {
    public static void main(String[] args) {
        // Different date string formats
        String[] dateStrings = {
            "2023/05/15",  // format: yyyy/MM/dd
            "15-05-2023",  // format: dd-MM-yyyy
            "May 15, 2023" // format: MMMM d, yyyy
        };

        for (String dateStr : dateStrings) {
            parseWithCorrectFormatter(dateStr);
            System.out.println("----------------------------------------");
        }
    }

    private static void parseWithCorrectFormatter(String dateStr) {
        System.out.println("Trying to parse: " + dateStr);

        // Try with yyyy/MM/dd pattern
        try {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
            LocalDate date = LocalDate.parse(dateStr, formatter);
            System.out.println("Success using 'yyyy/MM/dd': " + date);
            return;
        } catch (DateTimeParseException e) {
            System.out.println("Failed with 'yyyy/MM/dd' pattern");
        }

        // Try with dd-MM-yyyy pattern
        try {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
            LocalDate date = LocalDate.parse(dateStr, formatter);
            System.out.println("Success using 'dd-MM-yyyy': " + date);
            return;
        } catch (DateTimeParseException e) {
            System.out.println("Failed with 'dd-MM-yyyy' pattern");
        }

        // Try with MMMM d, yyyy pattern
        try {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM d, yyyy");
            LocalDate date = LocalDate.parse(dateStr, formatter);
            System.out.println("Success using 'MMMM d, yyyy': " + date);
            return;
        } catch (DateTimeParseException e) {
            System.out.println("Failed with 'MMMM d, yyyy' pattern");
        }

        System.out.println("Could not parse date string with any of the available formatters");
    }
}

이 프로그램을 컴파일하고 실행합니다.

cd ~/project
javac FormatMismatchSolution.java
java FormatMismatchSolution

다음과 유사한 출력을 볼 수 있습니다.

Trying to parse: 2023/05/15
Success using 'yyyy/MM/dd': 2023-05-15
----------------------------------------
Trying to parse: 15-05-2023
Failed with 'yyyy/MM/dd' pattern
Success using 'dd-MM-yyyy': 2023-05-15
----------------------------------------
Trying to parse: May 15, 2023
Failed with 'yyyy/MM/dd' pattern
Failed with 'dd-MM-yyyy' pattern
Success using 'MMMM d, yyyy': 2023-05-15
----------------------------------------

DateTimeFormatter 패턴 이해

DateTimeFormatter는 날짜 또는 시간의 다른 부분을 나타내기 위해 패턴 문자를 사용합니다. 다음은 가장 일반적인 패턴 문자 중 일부입니다.

  • y: 연도 (예: yyyy는 2023)
  • M: 월 (예: MM은 05, MMM은 May, MMMM은 May)
  • d: 일 (예: dd는 15)
  • H: 시 (0-23)(예: HH는 14)
  • m: 분 (예: mm은 30)
  • s: 초 (예: ss는 45)

이러한 패턴 문자를 슬래시, 하이픈, 공백 및 쉼표와 같은 리터럴과 결합하여 날짜 문자열의 정확한 형식과 일치시킬 수 있습니다.

유연한 날짜 파서 만들기

이제 여러 형식을 자동으로 처리할 수 있는 더 유연한 날짜 파서를 만들어 보겠습니다.

  1. WebIDE 에서 FlexibleDateParser.java라는 새 파일을 만듭니다.
  2. 다음 코드를 복사하여 붙여넣습니다.
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.List;

public class FlexibleDateParser {
    // List of common date formats
    private static final List<DateTimeFormatter> FORMATTERS = Arrays.asList(
        DateTimeFormatter.ofPattern("yyyy-MM-dd"),
        DateTimeFormatter.ofPattern("yyyy/MM/dd"),
        DateTimeFormatter.ofPattern("MM/dd/yyyy"),
        DateTimeFormatter.ofPattern("dd-MM-yyyy"),
        DateTimeFormatter.ofPattern("d MMM yyyy"),
        DateTimeFormatter.ofPattern("MMMM d, yyyy")
    );

    public static void main(String[] args) {
        // Test with different date formats
        String[] dates = {
            "2023-05-15",
            "2023/05/15",
            "05/15/2023",
            "15-05-2023",
            "15 May 2023",
            "May 15, 2023"
        };

        for (String dateStr : dates) {
            try {
                LocalDate date = parseDate(dateStr);
                System.out.println("Successfully parsed '" + dateStr + "' to: " + date);
            } catch (IllegalArgumentException e) {
                System.out.println("Failed to parse '" + dateStr + "': " + e.getMessage());
            }
        }
    }

    /**
     * Tries to parse a date string using multiple formatters
     * @param dateStr the date string to parse
     * @return the parsed LocalDate
     * @throws IllegalArgumentException if the string cannot be parsed with any formatter
     */
    public static LocalDate parseDate(String dateStr) {
        if (dateStr == null || dateStr.trim().isEmpty()) {
            throw new IllegalArgumentException("Date string cannot be null or empty");
        }

        for (DateTimeFormatter formatter : FORMATTERS) {
            try {
                // Try to parse with this formatter
                return LocalDate.parse(dateStr, formatter);
            } catch (DateTimeParseException e) {
                // This formatter didn't work, continue to the next one
                continue;
            }
        }

        // If we get here, none of the formatters worked
        throw new IllegalArgumentException("Cannot parse date: " + dateStr +
                                          ". Supported formats: yyyy-MM-dd, yyyy/MM/dd, " +
                                          "MM/dd/yyyy, dd-MM-yyyy, d MMM yyyy, MMMM d, yyyy");
    }
}

이 프로그램을 컴파일하고 실행합니다.

cd ~/project
javac FlexibleDateParser.java
java FlexibleDateParser

다음과 같은 출력을 볼 수 있습니다.

Successfully parsed '2023-05-15' to: 2023-05-15
Successfully parsed '2023/05/15' to: 2023-05-15
Successfully parsed '05/15/2023' to: 2023-05-15
Successfully parsed '15-05-2023' to: 2023-05-15
Successfully parsed '15 May 2023' to: 2023-05-15
Successfully parsed 'May 15, 2023' to: 2023-05-15

이 유연한 날짜 파서는 여러 날짜 형식을 처리할 수 있으므로 다양한 소스에서 날짜 입력을 처리할 때 애플리케이션의 견고성을 높입니다. 작동하는 형식을 찾거나 모든 가능성을 소진할 때까지 각 포맷터를 순서대로 시도합니다.

알 수 없거나 가변적인 형식으로 날짜를 파싱해야 할 때는 이 방식을 사용하십시오.

잘못된 날짜 값 처리

DateTimeParseException의 또 다른 일반적인 원인은 입력 문자열에 2 월 30 일 또는 9 월 31 일과 같이 논리적으로 유효하지 않은 날짜 값이 포함된 경우입니다. 이 단계에서는 이러한 경우를 효과적으로 처리하는 방법을 살펴보겠습니다.

잘못된 날짜의 문제점

날짜 문자열의 형식이 지정된 패턴과 일치하더라도 날짜 자체가 유효하지 않으면 파싱이 실패합니다. 이는 Java 의 날짜 - 시간 API 가 유효한 날짜만 생성할 수 있도록 유효성 검사를 수행하기 때문입니다.

잘못된 날짜 처리를 실험하기 위해 새 파일을 만들어 보겠습니다.

  1. WebIDE 에서 /home/labex/project 폴더를 마우스 오른쪽 버튼으로 클릭합니다.
  2. "New File"을 선택하고 이름을 InvalidDateHandler.java로 지정합니다.
  3. 다음 코드를 복사하여 붙여넣습니다.
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.Month;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

public class InvalidDateHandler {
    public static void main(String[] args) {
        // Array of date strings - some valid, some invalid
        String[] dateStrings = {
            "2023-04-30", // Valid - April has 30 days
            "2023-04-31", // Invalid - April has only 30 days
            "2023-02-28", // Valid - February 2023 has 28 days
            "2023-02-29", // Invalid - 2023 is not a leap year
            "2024-02-29", // Valid - 2024 is a leap year
            "2023-13-01"  // Invalid - Month 13 doesn't exist
        };

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

        System.out.println("Testing dates with simple parsing:");
        for (String dateStr : dateStrings) {
            try {
                LocalDate date = LocalDate.parse(dateStr, formatter);
                System.out.println("Valid date: " + dateStr + " => " + date);
            } catch (DateTimeParseException e) {
                System.out.println("Invalid date: " + dateStr + " => " + e.getMessage());
            }
        }

        System.out.println("\nTesting dates with manual validation:");
        for (String dateStr : dateStrings) {
            if (isValidDate(dateStr)) {
                try {
                    LocalDate date = LocalDate.parse(dateStr, formatter);
                    System.out.println("Valid date: " + dateStr + " => " + date);
                } catch (DateTimeParseException e) {
                    // This should not happen if isValidDate returns true
                    System.out.println("Unexpected error for " + dateStr + ": " + e.getMessage());
                }
            } else {
                System.out.println("Invalid date detected: " + dateStr);
            }
        }
    }

    /**
     * Validates a date string by checking if it represents a valid date.
     *
     * @param dateStr the date string in yyyy-MM-dd format
     * @return true if the date is valid, false otherwise
     */
    public static boolean isValidDate(String dateStr) {
        try {
            // Split the date string into components
            String[] parts = dateStr.split("-");
            if (parts.length != 3) {
                return false;
            }

            int year = Integer.parseInt(parts[0]);
            int month = Integer.parseInt(parts[1]);
            int day = Integer.parseInt(parts[2]);

            // Check basic ranges
            if (year < 1 || month < 1 || month > 12 || day < 1 || day > 31) {
                return false;
            }

            // Validate the date by trying to create it
            LocalDate.of(year, month, day);
            return true;
        } catch (DateTimeException | NumberFormatException e) {
            // DateTimeException is thrown for invalid dates
            // NumberFormatException is thrown if parts aren't numbers
            return false;
        }
    }
}

이 프로그램을 컴파일하고 실행합니다.

cd ~/project
javac InvalidDateHandler.java
java InvalidDateHandler

다음과 유사한 출력을 볼 수 있습니다.

Testing dates with simple parsing:
Valid date: 2023-04-30 => 2023-04-30
Invalid date: 2023-04-31 => Text '2023-04-31' could not be parsed: Invalid date 'April 31'
Valid date: 2023-02-28 => 2023-02-28
Invalid date: 2023-02-29 => Text '2023-02-29' could not be parsed: Invalid date 'February 29' as '2023' is not a leap year
Valid date: 2024-02-29 => 2024-02-29
Invalid date: 2023-13-01 => Text '2023-13-01' could not be parsed: Invalid value for MonthOfYear (valid values 1 - 12): 13

Testing dates with manual validation:
Valid date: 2023-04-30 => 2023-04-30
Invalid date detected: 2023-04-31
Valid date: 2023-02-28 => 2023-02-28
Invalid date detected: 2023-02-29
Valid date: 2024-02-29 => 2024-02-29
Invalid date detected: 2023-13-01

강력한 날짜 파서 구현

이제 형식 불일치와 잘못된 날짜를 모두 적절하게 처리할 수 있는 더 강력한 날짜 파서를 만들어 보겠습니다. 이전 단계의 유연한 형식 처리와 잘못된 날짜 유효성 검사를 결합합니다.

  1. WebIDE 에서 RobustDateParser.java라는 새 파일을 만듭니다.
  2. 다음 코드를 복사하여 붙여넣습니다.
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class RobustDateParser {
    // List of common date formats
    private static final List<DateTimeFormatter> FORMATTERS = Arrays.asList(
        DateTimeFormatter.ofPattern("yyyy-MM-dd"),
        DateTimeFormatter.ofPattern("yyyy/MM/dd"),
        DateTimeFormatter.ofPattern("MM/dd/yyyy"),
        DateTimeFormatter.ofPattern("dd-MM-yyyy"),
        DateTimeFormatter.ofPattern("d MMM yyyy"),
        DateTimeFormatter.ofPattern("MMMM d, yyyy")
    );

    public static void main(String[] args) {
        // Test with various dates, including invalid ones
        List<String> testDates = Arrays.asList(
            "2023-05-15",    // valid, standard format
            "2023/05/15",    // valid, with slashes
            "05/15/2023",    // valid, US format
            "15-05-2023",    // valid, European format
            "15 May 2023",   // valid, with month name
            "May 15, 2023",  // valid, with month name first
            "2023-02-29",    // invalid: 2023 is not a leap year
            "2023-04-31",    // invalid: April has 30 days
            "2023-13-01",    // invalid: month 13 doesn't exist
            "random text",   // invalid format
            ""               // empty string
        );

        for (String dateStr : testDates) {
            Optional<LocalDate> result = parseDate(dateStr);
            if (result.isPresent()) {
                System.out.println("Successfully parsed '" + dateStr + "' to: " + result.get());
            } else {
                System.out.println("Could not parse '" + dateStr + "' - Invalid or unsupported format");
            }
        }
    }

    /**
     * Attempts to parse a date string using multiple formatters.
     * Returns an Optional containing the parsed date if successful,
     * or an empty Optional if parsing fails with all formatters.
     *
     * @param dateStr the date string to parse
     * @return an Optional containing the parsed LocalDate or empty if parsing fails
     */
    public static Optional<LocalDate> parseDate(String dateStr) {
        // Guard against null or empty input
        if (dateStr == null || dateStr.trim().isEmpty()) {
            return Optional.empty();
        }

        List<String> errors = new ArrayList<>();

        // Try each formatter
        for (DateTimeFormatter formatter : FORMATTERS) {
            try {
                LocalDate date = LocalDate.parse(dateStr, formatter);
                return Optional.of(date);
            } catch (DateTimeParseException e) {
                errors.add("Failed with pattern " + formatter.toString() + ": " + e.getMessage());
            }
        }

        // If we get here, all formatters failed
        System.out.println("Debug - All formatters failed for '" + dateStr + "'");
        for (String error : errors) {
            System.out.println("  " + error);
        }

        return Optional.empty();
    }
}

이 프로그램을 컴파일하고 실행합니다.

cd ~/project
javac RobustDateParser.java
java RobustDateParser

출력은 어떤 날짜 문자열을 성공적으로 파싱할 수 있고 어떤 날짜 문자열이 오류를 일으키는지 보여줍니다.

Successfully parsed '2023-05-15' to: 2023-05-15
Successfully parsed '2023/05/15' to: 2023-05-15
Successfully parsed '05/15/2023' to: 2023-05-15
Successfully parsed '15-05-2023' to: 2023-05-15
Successfully parsed '15 May 2023' to: 2023-05-15
Successfully parsed 'May 15, 2023' to: 2023-05-15
Debug - All formatters failed for '2023-02-29'
  Failed with pattern ... Text '2023-02-29' could not be parsed: Invalid date 'February 29' as '2023' is not a leap year
  ...
Could not parse '2023-02-29' - Invalid or unsupported format
Debug - All formatters failed for '2023-04-31'
  ...
Could not parse '2023-04-31' - Invalid or unsupported format
Debug - All formatters failed for '2023-13-01'
  ...
Could not parse '2023-13-01' - Invalid or unsupported format
Debug - All formatters failed for 'random text'
  ...
Could not parse 'random text' - Invalid or unsupported format
Could not parse '' - Invalid or unsupported format

잘못된 날짜 처리를 위한 주요 기술

이 솔루션은 강력한 날짜 파싱을 위한 몇 가지 중요한 기술을 사용합니다.

  1. 다중 형식 지원: 다양한 입력 형식을 처리하기 위해 다른 포맷터로 파싱을 시도합니다.
  2. Optional 반환 유형: 파싱이 실패할 때 명확하게 나타내기 위해 Java 의 Optional을 사용합니다.
  3. 자세한 오류 로깅: 디버깅을 위해 오류 정보를 수집합니다.
  4. 입력 유효성 검사: 파싱을 시도하기 전에 null 또는 빈 입력을 확인합니다.
  5. 정상적인 실패: 예외를 발생시키는 대신 빈 Optional 을 반환합니다.

이러한 기술을 구현하면 잘못된 날짜가 포함될 수 있는 사용자 입력 또는 외부 시스템의 데이터와 관련된 경우에도 애플리케이션에서 날짜 파싱을 보다 강력하게 처리할 수 있습니다.

날짜 및 시간 파싱 모범 사례 구현

이 마지막 단계에서는 날짜와 시간 값을 모두 처리하기 위한 업계 모범 사례를 구현하는 포괄적인 날짜 - 시간 파서를 만들 것입니다. 또한 DateTimeParseException을 피하고 처리하기 위해 전문 Java 개발자가 사용하는 추가 기술도 살펴보겠습니다.

프로덕션 준비가 된 날짜 - 시간 파서 만들기

배운 모든 모범 사례를 통합하는 유틸리티 클래스를 만들어 보겠습니다.

  1. WebIDE 에서 /home/labex/project 폴더를 마우스 오른쪽 버튼으로 클릭합니다.
  2. "New File"을 선택하고 이름을 DateTimeParserUtil.java로 지정합니다.
  3. 다음 코드를 복사하여 붙여넣습니다.
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.time.temporal.ChronoField;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;

/**
 * Utility class for parsing date and time strings in a robust manner.
 */
public class DateTimeParserUtil {
    // Common date formatters with STRICT resolver for validation
    private static final List<DateTimeFormatter> DATE_FORMATTERS = Arrays.asList(
        createFormatter("yyyy-MM-dd"),
        createFormatter("yyyy/MM/dd"),
        createFormatter("MM/dd/yyyy"),
        createFormatter("dd-MM-yyyy"),
        createFormatter("d MMM yyyy"),
        createFormatter("MMMM d, yyyy")
    );

    // Common time formatters
    private static final List<DateTimeFormatter> TIME_FORMATTERS = Arrays.asList(
        createFormatter("HH:mm:ss"),
        createFormatter("HH:mm"),
        createFormatter("h:mm a"),
        createFormatter("h:mm:ss a")
    );

    // Common date-time formatters
    private static final List<DateTimeFormatter> DATETIME_FORMATTERS = Arrays.asList(
        createFormatter("yyyy-MM-dd HH:mm:ss"),
        createFormatter("yyyy-MM-dd'T'HH:mm:ss"),
        createFormatter("yyyy-MM-dd HH:mm"),
        createFormatter("MM/dd/yyyy HH:mm:ss"),
        createFormatter("dd-MM-yyyy HH:mm:ss")
    );

    /**
     * Creates a DateTimeFormatter with strict resolver style.
     */
    private static DateTimeFormatter createFormatter(String pattern) {
        return new DateTimeFormatterBuilder()
                .appendPattern(pattern)
                .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
                .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
                .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
                .toFormatter()
                .withResolverStyle(ResolverStyle.STRICT)
                .withLocale(Locale.US);
    }

    /**
     * Parses a string into a LocalDate.
     *
     * @param dateStr the date string to parse
     * @return an Optional containing the parsed LocalDate, or empty if parsing fails
     */
    public static Optional<LocalDate> parseDate(String dateStr) {
        if (dateStr == null || dateStr.trim().isEmpty()) {
            return Optional.empty();
        }

        for (DateTimeFormatter formatter : DATE_FORMATTERS) {
            try {
                LocalDate date = LocalDate.parse(dateStr, formatter);
                return Optional.of(date);
            } catch (DateTimeParseException e) {
                // Try next formatter
            }
        }

        return Optional.empty();
    }

    /**
     * Parses a string into a LocalTime.
     *
     * @param timeStr the time string to parse
     * @return an Optional containing the parsed LocalTime, or empty if parsing fails
     */
    public static Optional<LocalTime> parseTime(String timeStr) {
        if (timeStr == null || timeStr.trim().isEmpty()) {
            return Optional.empty();
        }

        for (DateTimeFormatter formatter : TIME_FORMATTERS) {
            try {
                LocalTime time = LocalTime.parse(timeStr, formatter);
                return Optional.of(time);
            } catch (DateTimeParseException e) {
                // Try next formatter
            }
        }

        return Optional.empty();
    }

    /**
     * Parses a string into a LocalDateTime.
     *
     * @param dateTimeStr the date-time string to parse
     * @return an Optional containing the parsed LocalDateTime, or empty if parsing fails
     */
    public static Optional<LocalDateTime> parseDateTime(String dateTimeStr) {
        if (dateTimeStr == null || dateTimeStr.trim().isEmpty()) {
            return Optional.empty();
        }

        // First try with combined date-time formatters
        for (DateTimeFormatter formatter : DATETIME_FORMATTERS) {
            try {
                LocalDateTime dateTime = LocalDateTime.parse(dateTimeStr, formatter);
                return Optional.of(dateTime);
            } catch (DateTimeParseException e) {
                // Try next formatter
            }
        }

        // Then try to split into date and time parts
        String[] parts = dateTimeStr.split(" ", 2);
        if (parts.length == 2) {
            Optional<LocalDate> date = parseDate(parts[0]);
            Optional<LocalTime> time = parseTime(parts[1]);

            if (date.isPresent() && time.isPresent()) {
                return Optional.of(LocalDateTime.of(date.get(), time.get()));
            }
        }

        return Optional.empty();
    }

    /**
     * Attempts to parse a string as a date, time, or date-time value.
     *
     * @param input the string to parse
     * @return a string describing the parsed result or an error message
     */
    public static String parseAny(String input) {
        Optional<LocalDate> date = parseDate(input);
        if (date.isPresent()) {
            return "Parsed as date: " + date.get();
        }

        Optional<LocalTime> time = parseTime(input);
        if (time.isPresent()) {
            return "Parsed as time: " + time.get();
        }

        Optional<LocalDateTime> dateTime = parseDateTime(input);
        if (dateTime.isPresent()) {
            return "Parsed as date-time: " + dateTime.get();
        }

        return "Could not parse: " + input;
    }
}

이제 유틸리티를 보여주기 위해 테스트 클래스를 만들어 보겠습니다.

  1. WebIDE 에서 DateTimeParsingDemo.java라는 새 파일을 만듭니다.
  2. 다음 코드를 복사하여 붙여넣습니다.
public class DateTimeParsingDemo {
    public static void main(String[] args) {
        // Test with various date, time, and date-time strings
        String[] inputs = {
            // Valid dates
            "2023-05-15",
            "05/15/2023",
            "15 May 2023",

            // Valid times
            "14:30:00",
            "2:30 PM",
            "14:30",

            // Valid date-times
            "2023-05-15 14:30:00",
            "2023-05-15T14:30:00",
            "05/15/2023 14:30:00",
            "15-05-2023 14:30:00",

            // Invalid examples
            "2023-02-30",
            "25:30:00",
            "2023-13-01",
            "Not a date or time",
            ""
        };

        for (String input : inputs) {
            String result = DateTimeParserUtil.parseAny(input);
            System.out.println("Input: \"" + input + "\" → " + result);
        }
    }
}

이 프로그램을 컴파일하고 실행합니다.

cd ~/project
javac DateTimeParserUtil.java DateTimeParsingDemo.java
java DateTimeParsingDemo

다음과 유사한 출력을 볼 수 있습니다.

Input: "2023-05-15" → Parsed as date: 2023-05-15
Input: "05/15/2023" → Parsed as date: 2023-05-15
Input: "15 May 2023" → Parsed as date: 2023-05-15
Input: "14:30:00" → Parsed as time: 14:30
Input: "2:30 PM" → Parsed as time: 14:30
Input: "14:30" → Parsed as time: 14:30
Input: "2023-05-15 14:30:00" → Parsed as date-time: 2023-05-15T14:30
Input: "2023-05-15T14:30:00" → Parsed as date-time: 2023-05-15T14:30
Input: "05/15/2023 14:30:00" → Parsed as date-time: 2023-05-15T14:30
Input: "15-05-2023 14:30:00" → Parsed as date-time: 2023-05-15T14:30
Input: "2023-02-30" → Could not parse: 2023-02-30
Input: "25:30:00" → Could not parse: 25:30:00
Input: "2023-13-01" → Could not parse: 2023-13-01
Input: "Not a date or time" → Could not parse: Not a date or time
Input: "" → Could not parse:

Java 에서 날짜 - 시간 파싱을 위한 모범 사례

유틸리티 클래스에 구현된 주요 모범 사례를 살펴보겠습니다.

1. ResolverStyle.STRICT 설정 사용

.withResolverStyle(ResolverStyle.STRICT)

STRICT 리졸버 스타일은 유효한 날짜와 시간만 허용하도록 합니다. 이렇게 하면 "2 월 31 일"을 3 월 3 일로 파싱하는 것과 같은 문제를 방지할 수 있습니다.

2. 기본 로케일 지정

.withLocale(Locale.US)

월 이름 및 오전/오후 표시기에 대해 일관된 파싱 동작을 보장하려면 항상 로케일을 지정하십시오.

3. Optional 반환 유형 사용

public static Optional<LocalDate> parseDate(String dateStr) {
    // ...
    return Optional.empty();
}

Optional을 사용하면 파싱이 실패할 수 있음을 명확하게 전달하고 호출 코드가 이 경우를 명시적으로 처리하도록 합니다.

4. 여러 형식 지원

private static final List<DateTimeFormatter> DATE_FORMATTERS = Arrays.asList(
    createFormatter("yyyy-MM-dd"),
    createFormatter("yyyy/MM/dd"),
    // ...
);

다중 형식을 지원하면 다양한 입력 소스를 처리할 때 파싱 로직의 견고성이 향상됩니다.

5. 누락된 시간 필드 기본값 설정

.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)

누락된 시간 필드를 기본값으로 설정하면 특히 다른 날짜 - 시간 유형 간에 변환할 때 보다 유연한 파싱이 가능합니다.

6. 입력 유효성 검사

if (dateStr == null || dateStr.trim().isEmpty()) {
    return Optional.empty();
}

불필요한 예외를 방지하기 위해 파싱을 시도하기 전에 항상 입력을 확인하십시오.

7. 정상적인 오류 처리

try {
    LocalDate date = LocalDate.parse(dateStr, formatter);
    return Optional.of(date);
} catch (DateTimeParseException e) {
    // Try next formatter
}

예외를 정상적으로 처리하고 파싱이 실패할 때 명확한 피드백을 제공합니다.

8. 스마트 복합 파싱

// Then try to split into date and time parts
String[] parts = dateTimeStr.split(" ", 2);
if (parts.length == 2) {
    Optional<LocalDate> date = parseDate(parts[0]);
    Optional<LocalTime> time = parseTime(parts[1]);

    if (date.isPresent() && time.isPresent()) {
        return Optional.of(LocalDateTime.of(date.get(), time.get()));
    }
}

복잡한 파싱 작업을 더 간단한 구성 요소로 나누면 더 나은 결과를 얻을 수 있습니다.

이러한 모범 사례를 구현하면 광범위한 입력을 정상적으로 처리하고 파싱이 실패할 때 명확한 피드백을 제공하는 강력한 날짜 및 시간 파싱 로직을 만들 수 있습니다.

요약

이 Lab 에서는 Java 애플리케이션에서 java.time.format.DateTimeParseException을 효과적으로 처리하는 방법을 배웠습니다. 다음 사항에 대한 실질적인 경험을 얻었습니다.

  1. DateTimeParseException 이해: 형식 불일치 및 잘못된 날짜 값을 포함하여 이 예외의 일반적인 원인에 대해 배웠습니다.

  2. 형식 불일치 문제 해결: 여러 날짜 형식을 처리하고 형식 불일치를 적절하게 처리할 수 있는 유연한 날짜 파서를 구현했습니다.

  3. 잘못된 날짜 값 처리: 2 월 30 일 또는 4 월 31 일과 같은 잘못된 날짜 값을 감지하고 처리하기 위한 강력한 솔루션을 만들었습니다.

  4. 모범 사례 구현: 업계 모범 사례를 통합하는 포괄적인 날짜 - 시간 파서 유틸리티를 구축했습니다. 여기에는 다음이 포함됩니다.

    • STRICT 리졸버 스타일 사용
    • 기본 로케일 지정
    • Optional 값 반환
    • 여러 형식 지원
    • 입력 유효성 검사
    • 정상적인 오류 처리 구현
    • 복잡한 파싱 작업 분해

이러한 방식을 따르면 다양한 소스에서 날짜 및 시간 데이터를 처리할 때 Java 애플리케이션을 더욱 강력하게 만들 수 있으며, 예기치 않은 예외 발생 가능성을 줄이고 사용자에게 더 나은 경험을 제공할 수 있습니다.

효과적인 날짜 및 시간 파싱은 특히 사용자 입력, 데이터 가져오기 또는 외부 시스템과의 통합을 처리하는 많은 애플리케이션에 매우 중요하다는 점을 기억하십시오. 이 Lab 에서 배운 기술은 Java 프로젝트에서 이러한 시나리오를 보다 효과적으로 처리하는 데 도움이 될 것입니다.