μžλ°”μ—μ„œ λ‚ μ§œ 및 μ‹œκ°„μ„ λ‹€λ£¨λŠ” 경우 Instant, OffsetDateTime λ˜λŠ” ZonedDateTimeκ³Ό 같이 νƒ€μž„μ‘΄ μ˜€ν”„μ…‹μ΄ ν¬ν•¨λ˜λŠ” 것을 ν™œμš©ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€. 그런데, 가끔은 μ—°,μ›”,일 톡계와 같은 μš”κ΅¬μ‚¬ν•­μœΌλ‘œ 인해 YearMonth λ˜λŠ” LocalDateλ₯Ό μ‚¬μš©ν•΄μ•Όν•˜λŠ” κ²½μš°κ°€ μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, 2025λ…„ 2월에 λŒ€ν•œ 톡계λ₯Ό μœ„ν•΄μ„œ 2025λ…„ 2μ›”μ˜ 첫번째 λ‚ μ§œμ™€ λ§ˆμ§€λ§‰ λ‚ μ§œμ˜ λ²”μœ„λ₯Ό μ•Œμ•„μ•Ό ν•©λ‹ˆλ‹€. 첫번째 λ‚ μ§œλŠ” λͺ…ν™•ν•˜λ―€λ‘œ κ°„λ‹¨ν•˜μ§€λ§Œ λ§ˆμ§€λ§‰ λ‚ μ§œλŠ” μ›”λ§ˆλ‹€ λ‹€λ₯Έλ° νŠΉνžˆλ‚˜, 2μ›”μ˜ λ§ˆμ§€λ§‰ λ‚ μ§œλŠ” 28일이기도 ν•©λ‹ˆλ‹€.

μžλ°”μ—μ„œ μ—° λ˜λŠ” 월에 λŒ€ν•œ λ§ˆμ§€λ§‰ λ‚ μ§œλ₯Ό κ°€μ Έμ˜€λŠ” 방법을 μ—¬λŸ¬κ°€μ§€κ°€ μžˆμŠ΅λ‹ˆλ‹€. Year λ˜λŠ” YearMonth의 lengthXXX ν•¨μˆ˜λ₯Ό 톡해 λ§ˆμ§€λ§‰ λ‚ μ§œλ₯Ό κ°€μ Έμ™€μ„œ 지정할 수 있으며 YearMonth μ—λŠ” 더 직관적인 atEndOfMonth ν•¨μˆ˜λ₯Ό ν¬ν•¨ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ, TemporalAdjustersλ₯Ό 톡해 λ‚ μ§œλ₯Ό λ³€ν™˜ν•˜λŠ” 것도 κ°€λŠ₯ν•˜μ£ .

μ›” λˆ„μ μ„ μœ„ν•œ 첫번째 λ‚ μ§œμ™€ λ§ˆμ§€λ§‰ λ‚ μ§œ κ°€μ Έμ˜€κΈ°

YearMonth yearMonth = YearMonth.of(2025, 2);
LocalDate startDate = yearMonth.atDay(1);
LocalDate endDate = yearMonth.atEndOfMonth();

λ§ˆμ§€λ§‰ λ‚ μ§œλ₯Ό κ΅¬ν•˜κΈ° μœ„ν•΄μ„œ YearMonth.lengthOfMonth λ˜λŠ” TemporalAdjusters.lastDayOfMonth ν•¨μˆ˜λ₯Ό μ΄μš©ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.

μ—° λˆ„μ μ„ μœ„ν•œ 첫번째 λ‚ μ§œμ™€ λ§ˆμ§€λ§‰ λ‚ μ§œ κ°€μ Έμ˜€κΈ°

Year year = Year.of(2025);
LocalDate firstDate = year.atDay(1);
LocalDate lastDate = year.atDay(year.length());

λ§ˆμ§€λ§‰ λ‚ μ§œλ₯Ό κ΅¬ν•˜κΈ° μœ„ν•΄μ„œ TemporalAdjusters.lastDayOfYear ν•¨μˆ˜λ₯Ό μ΄μš©ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.

μŠ€ν”„λ§ 컨트둀러 ν•Έλ“€λŸ¬ ν•¨μˆ˜ νŒŒλΌλ―Έν„°

@RestController
@RequestMapping("/api")
public class SampleApi {
    private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy.MM.dd");

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(Month.class, new MonthPropertyEditor());
    }

    @GetMapping("/stat/{year:[0-9]{4}}")
    public ResponseEntity<?> getStatOfYear(@PathVariable("year") Year year) {
        LocalDate fromDate = year.atDay(1);
        LocalDate toDate = year.atDay(year.length());
        return ResponseEntity.ok(Map.of(
                "from", fromDate.format(DATE_FORMAT),
                "to", toDate.format(DATE_FORMAT)));
    }

    @GetMapping("/stat/{yearMonth:[0-9]{4}\\-[0-9]{2}}")
    public ResponseEntity<?> getStatOfYearMonth(
            @DateTimeFormat(pattern = "yyyy-MM")
            @PathVariable("yearMonth") YearMonth yearMonth) {
        LocalDate fromDate = yearMonth.atDay(1);
        LocalDate toDate = yearMonth.atEndOfMonth();
        return ResponseEntity.ok(Map.of(
                "from", fromDate.format(DATE_FORMAT),
                "to", toDate.format(DATE_FORMAT)));
    }

    @GetMapping("/stat/{year}/{month}")
    public ResponseEntity<?> getStatOfYearMonth(@PathVariable("year") Year year,
                                                @PathVariable("month") Month month) {
        return getStatOfYearMonth(YearMonth.of(year.getValue(), month));
    }

    private static class MonthPropertyEditor extends PropertyEditorSupport {
        @Override
        public void setAsText(String text) throws IllegalArgumentException {
            if (text.matches("\\d+")) {
                setValue(Month.of(Integer.parseInt(text)));
                return;
            }
            setValue(Month.valueOf(text.toUpperCase()));
        }
    }
}

μŠ€ν”„λ§ 컨트둀러의 ν•Έλ“€λŸ¬ ν•¨μˆ˜λ₯Ό μž‘μ„±ν•  λ•Œ 연도와 월에 λŒ€ν•œ νŒŒλΌλ―Έν„°λ₯Ό int κ°€ μ•„λ‹Œ Year와 YearMonthλ₯Ό κ·ΈλŒ€λ‘œ ν™œμš©ν•  수 μžˆλ„λ‘ 바인딩이 κ°€λŠ₯ν•©λ‹ˆλ‹€. 단, Month의 경우 ν΄λž˜μŠ€κ°€ μ•„λ‹Œ Enum 이기 λ•Œλ¬Έμ— λ³„λ„μ˜ PropertyEditorλ₯Ό μž‘μ„±ν•˜μ—¬ WebDataBinder에 λ“±λ‘ν•΄μ•Όν•©λ‹ˆλ‹€. νŒŒλΌλ―Έν„° λ°”μΈλ”©μœΌλ‘œ λ³€ν™˜ν•˜λŠ” 과정을 κ³΅ν†΅μ μœΌλ‘œ μ μš©ν•˜λ―€λ‘œ 더 효율적이고 직관적인 μ½”λ“œλ₯Ό μž‘μ„±ν•  수 μžˆμŒμ„ μ•Œμ•˜μŠ΅λ‹ˆλ‹€.