공공부하자개발 · 영어 학습 노트
자바
중급객체지향과 코어 라이브러리0/9 완료
  • 01상속, 다형성, 오버라이딩
  • 02추상 클래스 vs 인터페이스
  • 03예외 처리
  • 04java.lang 심화
  • 05컬렉션 프레임워크 딥다이브
  • 06메서드 활용 패턴 (중급)
  • 07Object 메서드와 비교
  • 08java.time 실무 날짜 계산
  • 09HTTP 와 JSON 기초
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 중급 › 08 / 9

java.time 실무 날짜 계산

정산일, 영업일, 월말, 시간대, DB 매핑
섹션 7진행 0 / 9
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

2. 핵심 원리

2.1 타입 고르기: 시간대가 있는가

타입 담는 것 쓰는 곳 예
LocalDate 날짜만 생일, 정산일, 마감일, 계약 기간 2026-09-09
LocalTime 시각만 영업 시작·종료 시각, 배치 실행 시각 09:30
LocalDateTime 날짜+시각, 시간대 없음 국내 단일 시간대 서비스의 등록·수정 시각 2026-09-09T09:30
ZonedDateTime 날짜+시각+시간대 해외 사용자 표시, 항공·회의 일정 2026-09-09T09:30+09:00[Asia/Seoul]
Instant UTC 절대 시각 로그, 이벤트 발생 시각, 토큰 만료, 서버 간 전달 2026-09-09T00:30:00Z
YearMonth 연·월 정산 월, 카드 유효기간 2026-09
Period / Duration 두 시점의 차이 나이·잔여일 / 소요 시간·타임아웃 P35Y8M15D / PT30M

LocalDateTime 은 "어느 나라의 9시 30분" 인지 모릅니다. 국내 서비스에서는 편하지만, 이것을 Instant 로 바꾸는 순간 시간대를 가정해야 하고 그 가정이 서버 설정에 따라 달라지는 것이 시간대 버그의 근원입니다(2.6). 절대 시각이 필요한 곳(로그, 만료)은 처음부터 Instant 를 씁니다.

2.2 불변 객체와 월말 규칙

java.time 의 모든 타입은 불변입니다. d.plusDays(1) 은 d 를 바꾸지 않고 새 객체를 돌려주므로 반환값을 받지 않으면 아무 일도 일어나지 않습니다. 예제 2 첫 줄이 이것입니다. 대신 스레드 안전이 공짜이고 static final 상수로 공유할 수 있습니다.

월 연산에는 규칙이 있습니다. 1월 31일 + 1개월은 2월 28일입니다. 없는 날짜로 가면 예외 대신 그 달 말일로 당깁니다. 문제는 그다음입니다. 2월 28일 + 1개월은 3월 28일이라 "월말" 이라는 성질이 사라집니다. "매월 말일 정산" 을 plusMonths 로 이어 가면 3월부터 28일에 정산하게 됩니다.

월말을 유지하려면 날짜 대신 YearMonth 로 월을 옮긴 뒤 atEndOfMonth() 로 말일을 다시 구합니다.

TemporalAdjusters 는 "이달 말일", "다음 달 첫 월요일", "다음 금요일" 같은 상대 날짜를 제공합니다. d.with(TemporalAdjusters.lastDayOfMonth()) 처럼 with 에 넣습니다.

2.3 영업일 계산

달력 날짜와 영업일은 다릅니다. "3영업일 이내 처리", "지급일이 휴일이면 다음 영업일" 은 주말과 공휴일을 건너뛰며 세는 계산입니다. 표준 API 에 없으므로 직접 만듭니다.

java
public boolean isBusinessDay(LocalDate d) {
    DayOfWeek w = d.getDayOfWeek();
    return w != DayOfWeek.SATURDAY && w != DayOfWeek.SUNDAY && !holidays.contains(d);
}
public LocalDate addBusinessDays(LocalDate d, int n) {          // 하루씩 옮기며 영업일만 센다
    int step = n >= 0 ? 1 : -1;
    for (int left = Math.abs(n); left > 0; ) { d = d.plusDays(step); if (isBusinessDay(d)) left--; }
    return d;
}

공휴일은 해마다 바뀌고 대체공휴일이 있으므로 코드에 박지 않고 DB 테이블(holiday) 에 두어 매년 등록합니다. 이 레슨은 Set<LocalDate> 로 대신합니다. 정산 기간은 YearMonth 하나로 정해집니다. ym.atDay(1) 부터 ym.atEndOfMonth() 까지이고, 지급일은 ym.plusMonths(1).atDay(10) 을 영업일로 보정합니다(예제 3).

2.4 Period, Duration, ChronoUnit

Period Duration ChronoUnit.X.between
단위 년·월·일 시·분·초·나노 지정한 단위 하나로 환산
대상 LocalDate LocalDateTime, Instant, LocalTime 둘 다
예 만 나이 P35Y8M15D 배치 소요 PT30M 총 일수 13042, D-day

만 나이는 Period.between(birth, today).getYears() 이고, 총 일수는 ChronoUnit.DAYS.between 입니다. Period 의 getDays() 는 "나머지 일" 이지 총 일수가 아닙니다.

Duration.between(LocalTime 23:50, LocalTime 00:20) 은 자정을 모르니 음수(-23시간 30분) 가 됩니다. 자정을 넘길 수 있는 소요 시간은 반드시 날짜를 포함한 LocalDateTime 이나 Instant 로 계산합니다(예제 4).

2.5 DateTimeFormatter: yyyy 와 YYYY, hh 와 HH, SMART 와 STRICT

패턴 뜻 함정
yyyy 연도 항상 이것
YYYY 주 기준 연도 12월 마지막 주가 다음 해로 찍힌다. 12월 31일이 2027-12-31 이 되는 버그
MM / mm 월 / 분 대소문자 바꾸면 조용히 틀린다
HH / hh 24시간 / 12시간 hh 는 오후 2시가 02
dd / DD 일 / 연중 일수

파싱에는 해석 방식(ResolverStyle) 이 있습니다. 기본 SMART 는 20260231 을 예외 없이 2월 28일로 조정합니다. 입력 검증 용도라면 withResolverStyle(ResolverStyle.STRICT) 로 바꿔야 하고, STRICT 에서는 yyyy 대신 uuuu(기원 표시 없는 연도) 를 써야 합니다.

예제 5 가 두 결과를 나란히 보여줍니다. DateTimeFormatter 는 불변이라 static final 로 공유하고, SimpleDateFormat 처럼 요청마다 만들거나 동기화할 필요가 없습니다.

2.6 시간대: Instant 는 하나, 표시는 여러 개

Instant 는 UTC 기준 절대 시각 하나입니다. 같은 Instant 가 서울에서는 09:30, 뉴욕에서는 전날 20:30 으로 표시됩니다. 저장·전달은 Instant(또는 epoch), 표시는 atZone(zone) 이 원칙입니다.

가장 흔한 사고는 LocalDateTime.now() 입니다. 시간대 없는 값을 "지금" 이라며 만들면 JVM 기본 시간대(ZoneId.systemDefault())를 씁니다. 개발 PC 는 Asia/Seoul 인데 클라우드·컨테이너는 UTC 인 경우가 많아 운영에서 9시간이 어긋납니다.

LocalDateTime.now(ZoneId.of("Asia/Seoul")) 처럼 시간대를 명시하거나, Instant.now() 를 쓰고 표시할 때 변환합니다. 같은 LocalDateTime 을 서울로 해석한 Instant 와 UTC 로 해석한 Instant 는 9시간 차이가 납니다(예제 6).

서머타임(DST)이 있는 지역은 하루가 23시간이거나 25시간이고 존재하지 않는 시각이 있습니다. 뉴욕 3월 8일 01:30 에 1시간을 더하면 03:30 입니다. 한국은 DST 가 없지만 해외 파트너 API 나 글로벌 서비스에서는 ZonedDateTime 이 이것을 자동으로 처리합니다. ZonedDateTime.equals 는 시간대까지 비교하고, 같은 순간인지는 isEqual 로 봅니다.

2.7 DB 와 레거시 API 매핑

DB 컬럼 (Oracle) 자바 타입 JDBC 비고
DATE LocalDateTime (초까지 담김) 또는 LocalDate setObject / getObject(col, LocalDateTime.class) Oracle DATE 는 이름과 달리 시각을 포함한다
TIMESTAMP LocalDateTime 같음 밀리초 이하
TIMESTAMP WITH TIME ZONE OffsetDateTime / Instant 같음 글로벌 서비스
NUMBER(epoch) Instant.ofEpochSecond JWT exp, 외부 API

MyBatis 3.4.5 이상은 LocalDate/LocalDateTime 을 자동 매핑하므로 java.sql.Timestamp 를 직접 다룰 일이 줄었습니다. 레거시 코드의 java.util.Date 는 toInstant() 로, java.sql.Timestamp 는 toLocalDateTime() 으로 건너옵니다.

반대 방향은 Timestamp.valueOf(ldt), java.sql.Date.valueOf(ld) 입니다. 비교는 isBefore/isAfter/isEqual 이 읽기 쉽고, 정렬은 Comparable 이라 그대로 sort 됩니다(07 레슨).

핵심 원리
  • 2.1 타입 고르기: 시간대가 있는가
  • 2.2 불변 객체와 월말 규칙
  • 2.3 영업일 계산
  • 2.4 Period, Duration, ChronoUnit
  • 2.5 DateTimeFormatter: yyyy 와 YYYY, hh 와 HH, SMART 와 STRICT
  • 2.6 시간대: Instant 는 하나, 표시는 여러 개
  • 2.7 DB 와 레거시 API 매핑
이전 섹션1 왜 배우는가2 / 7다음 섹션3 코드 예제