본문 바로가기

Java

[Java] 날짜와 시간 파싱, 포맷팅

날짜와 시간은 크게 특정 시점의 시간(시각)과 시간의 간격(기간) 2가지로 나눌 수 있습니다.

 

특정 시점의 시간(시각)

  • 이 프로젝트는 2013년 8월 16일 까지 완료해야해
  • 다음 회의는 11시 30분에 진행한다.
  • 내 생일은 8월 16일이야.

시간의 간격(기간, 시간의 양)

  • 앞으로 4년은 더 공부해야 해
  • 이 프로젝트는 3개월 남았어
  • 라면은 3분 동안 끓어야 해

 

특정 시점의 시간: Temporal ( TemporalAccessor 포함) 인터페이스를 구현한다.

  • 구현으로 LocalDateTime , LocalDate , LocalTime , ZonedDateTime , OffsetDateTime , Instant 등이 있다.

시간의 간격(기간): TemporalAmount 인터페이스를 구현한다.

  • 구현으로 Period , Duration 이 있다.

TemporalAccessor 인터페이스

  • 날짜와 시간을 읽기 위한 기본 인터페이스
  • 이 인터페이스는 특정 시점의 날짜와 시간 정보를 읽을 수 있는 최소한의 기능을 제공한다.

Temporal 인터페이스

  • TemporalAccessor 의 하위 인터페이스로, 날짜와 시간을 조작(추가, 빼기 등)하기 위한 기능을 제공한다. 이
    를 통해 날짜와 시간을 변경하거나 조정할 수 있다.
  • 간단히 말하면, TemporalAccessor 는 읽기 전용 접근을, Temporal 은 읽기와 쓰기(조작) 모두를 지원한다.

TemporalAmount 인터페이스

  • 시간의 간격(시간의 양, 기간)을 나타내며, 날짜와 시간 객체에 적용하여 그 객체를 조정할 수 있다.
  • 예를 들어, 특정 날짜에 일정 기간을 더하거나 빼는 데 사용된다.

 

시간의 단위와 시간 필드

다음으로 설명할 날짜와 시간의 핵심 인터페이스는 시간의 단위를 뜻하는 TemporalUnit ( ChronoUnit )과

시간의 각 필드를 뜻하는 TemporalField ( ChronoField )이다.

 

시간의 단위 - TemporalUnit, ChronoUnit

  • TemporalUnit 인터페이스는 날짜와 시간을 측정하는 단위를 나타내며, 주로 사용되는 구현체는
    java.time.temporal.ChronoUnit 열거형으로 구현되어 있다.
  • ChronoUnit 은 다양한 시간 단위를 제공한다.
  • 여기서 Unit 이라는 뜻을 번역하면 단위이다. 따라서 시간의 단위 하나하나를 나타낸다.
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;

public class ChronoUnitMain {

    public static void main(String[] args) {
        ChronoUnit[] values = ChronoUnit.values();
        for (ChronoUnit value : values) {
            System.out.println("value = " + value);
        }
        System.out.println("HOURS = " + ChronoUnit.HOURS);
        System.out.println("HOURS.durantion" + ChronoUnit.HOURS.getDuration().getSeconds());
        System.out.println("DAYS = " + ChronoUnit.DAYS);
        System.out.println("DAYS.duration" + ChronoUnit.DAYS.getDuration().getSeconds());

        // 차이 구하기
        LocalTime lt1 = LocalTime.of(1, 10, 0);
        LocalTime lt2 = LocalTime.of(1, 20, 0);

        long secondsBetween = ChronoUnit.SECONDS.between(lt1, lt2);
        System.out.println("secondsBetween = " + secondsBetween);

        long minutesBetween = ChronoUnit.MINUTES.between(lt1, lt2);
        System.out.println("minutesBetween = " + minutesBetween);
    }
}
value = Nanos
value = Micros
value = Millis
value = Seconds
value = Minutes
value = Hours
value = HalfDays
value = Days
value = Weeks
value = Months
value = Years
value = Decades
value = Centuries
value = Millennia
value = Eras
value = Forever
HOURS = Hours
HOURS.durantion3600
DAYS = Days
DAYS.duration86400
secondsBetween = 600
minutesBetween = 10
  • ChronoUnit 을 사용하면 두 날짜 또는 시간 사이의 차이를 해당 단위로 쉽게 계산할 수 있다.
  • 예제 코드에서는 두 LocalTime 객체 사이의 차이를 초, 분 단위로 구한다.

 

시간 필드 - ChronoField


ChronoField 는 날짜 및 시간을 나타내는 데 사용되는 열거형 입니다. 

이 열거형은 다양한 필드를 통해 날짜와 시간의 특정 부분을 나타냅니다. (여기에는 연도, 월, 일, 시간, 분 등이 포함된다)

  • TemporalField 인터페이스는 날짜와 시간을 나타내는데 사용된다. 주로 사용되는 구현체는java.time.temporal.ChronoField 열거형으로 구현되어 있다.
  • ChronoField 는 다양한 필드를 통해 날짜와 시간의 특정 부분을 나타낸다. 여기에는 연도, 월, 일, 시간, 분 등
    이 포함된다.
  • 여기서 필드(Field)라는 뜻이 날짜와 시간 중에 있는 특정 필드들을 뜻한다. 각각의 필드 항목은 다음을 참고하자.
  • 예를 들어 2024년 8월 16일이라고 하면 각각의 필드는 다음과 같다.
    YEAR : 2024
    MONTH_OF_YEAR : 8
    DAY_OF_MONTH : 16
  • 단순히 시간의 단위 하나하나를 뜻하는 ChronoUnit 과는 다른 것을 알 수 있다. 
  • ChronoField 를 사용해야 날짜와 시간의 각 필드 중에 원하는 데이터를 조회할 수 있다
import java.time.temporal.ChronoField;

public class ChronoFieldMain {

    public static void main(String[] args) {
        ChronoField[] values = ChronoField.values();
        for (ChronoField value : values) {
            System.out.println(value + ", range = " + value.range());
        }

        System.out.println("MONTH_OF_YEAR.range() = " + ChronoField.MONTH_OF_YEAR.range());
        System.out.println("DAY_OF_MONTH.range() = " + ChronoField.DAY_OF_MONTH.range());
    }
NanoOfSecond, range = 0 - 999999999
NanoOfDay, range = 0 - 86399999999999
MicroOfSecond, range = 0 - 999999
MicroOfDay, range = 0 - 86399999999
MilliOfSecond, range = 0 - 999
MilliOfDay, range = 0 - 86399999
SecondOfMinute, range = 0 - 59
SecondOfDay, range = 0 - 86399
MinuteOfHour, range = 0 - 59
MinuteOfDay, range = 0 - 1439
HourOfAmPm, range = 0 - 11
ClockHourOfAmPm, range = 1 - 12
HourOfDay, range = 0 - 23
ClockHourOfDay, range = 1 - 24
AmPmOfDay, range = 0 - 1
DayOfWeek, range = 1 - 7
AlignedDayOfWeekInMonth, range = 1 - 7
AlignedDayOfWeekInYear, range = 1 - 7
DayOfMonth, range = 1 - 28/31
DayOfYear, range = 1 - 365/366
EpochDay, range = -365243219162 - 365241780471
AlignedWeekOfMonth, range = 1 - 4/5
AlignedWeekOfYear, range = 1 - 53
MonthOfYear, range = 1 - 12
ProlepticMonth, range = -11999999988 - 11999999999
YearOfEra, range = 1 - 999999999/1000000000
Year, range = -999999999 - 999999999
Era, range = 0 - 1
InstantSeconds, range = -9223372036854775808 - 9223372036854775807
OffsetSeconds, range = -64800 - 64800
MONTH_OF_YEAR.range() = 1 - 12
DAY_OF_MONTH.range() = 1 - 28/31

 

TemporalUnit(ChronoUnit) , TemporalField(ChronoField) 는 단독으로 사용하기 보다는 주로 날짜와시간을 조회하거나

조작할 때 사용한다. 

 

 

날짜와 시간 조회하기

 

날짜와 시간을 조회하려면 날짜와 시간 항목중에 어떤 필드를 조회할 지 선택해야 한다.

이때 날짜와 시간의 필드를 뜻하는 ChronoField 가 사용된다.

import java.time.LocalDateTime;
import java.time.temporal.ChronoField;

public class GetTimeMain {

    public static void main(String[] args) {
        LocalDateTime dt = LocalDateTime.of(2030, 1, 1, 13, 30, 59);
        System.out.println("YEAR = " + dt.get(ChronoField.YEAR));
        System.out.println("MONTH_OF_YEAR = " + dt.get(ChronoField.MONTH_OF_YEAR));
        System.out.println("DAY_OF_MONTH = " + dt.get(ChronoField.DAY_OF_MONTH));
        System.out.println("HOUR_OF_DAY = " + dt.get(ChronoField.HOUR_OF_DAY));
        System.out.println("MINUTE_OF_HOUR = " + dt.get(ChronoField.MINUTE_OF_HOUR));
        System.out.println("SECOND_OF_MINUTE = " + dt.get(ChronoField.SECOND_OF_MINUTE));

        System.out.println("편의 메서드 제공");
        System.out.println("YEAR = " + dt.getYear());
        System.out.println("MONTH_OF_YEAR = " + dt.getMonthValue());
        System.out.println("DAY_OF_MONTH = " + dt.getDayOfMonth());
        System.out.println("HOUR_OF_DAY = " + dt.getHour());
        System.out.println("MINUTE_OF_HOUR = " + dt.getMinute());
        System.out.println("SECOND_OF_MINUTE = " + dt.getSecond());

        System.out.println("편의 메서드에 없음");
        System.out.println("MINUTE_OF_DAY = " + dt.get(ChronoField.MINUTE_OF_DAY));
        System.out.println("SECOND_OF_DAY = " + dt.get(ChronoField.SECOND_OF_DAY));
    }
}
YEAR = 2030
MONTH_OF_YEAR = 1
DAY_OF_MONTH = 1
HOUR_OF_DAY = 13
MINUTE_OF_HOUR = 30
SECOND_OF_MINUTE = 59
편의 메서드 제공
YEAR = 2030
MONTH_OF_YEAR = 1
DAY_OF_MONTH = 1
HOUR_OF_DAY = 13
MINUTE_OF_HOUR = 30
SECOND_OF_MINUTE = 59
편의 메서드에 없음
MINUTE_OF_DAY = 810
SECOND_OF_DAY = 48659

 

TemporalAccessor.get(TemporalField field)

  • LocalDateTime 을 포함한 특정 시점의 시간을 제공하는 클래스는 모두 TemporalAccessor 인터페이스를
    구현한다.
  • TemporalAccessor 는 특정 시점의 시간을 조회하는 기능을 제공한다.
  • get(TemporalField field) 을 호출할 때 어떤 날짜와 시간 필드를 조회할 지 TemporalField 의 구현
    인 ChronoField 를 인수로 전달하면 된다.

 

편의 메서드 사용

  • get(TemporalField field) 을 사용하면 코드가 길어지고 번거롭기 때문에 자주 사용하는 조회 필드는 간
    단한 편의 메서드를 제공한다.
  • dt.get(ChronoField.DAY_OF_MONTH)) => dt.getDayOfMonth()

 

편의 메서드에 없음

  • 자주 사용하지 않는 특별한 기능은 편의 메서드를 제공하지 않는다.
  • 편의 메서드를 사용하는 것이 가독성이 좋기 때문에 일반적으로는 편의 메서드를 사용하고, 편의 메서드가 없는
    경우 get(TemporalField field) 을 사용하면 된다.

 

날짜와 시간 조작하기


날짜와 시간을 조작하려면 어떤 시간 단위(Unit)를 변경할 지 선택해야 한다. 

이때 날짜와 시간의 단위를 뜻하는 ChronoUnit 이 사용된다.

import java.time.LocalDateTime;
import java.time.Period;
import java.time.temporal.ChronoUnit;

public class ChangeTimeMain {

    public static void main(String[] args) {
        LocalDateTime dt = LocalDateTime.of(2018, 1, 1, 13, 30, 59);
        System.out.println("dt = " + dt);

        LocalDateTime plusDt1 = dt.plus(10, ChronoUnit.YEARS);
        System.out.println("plusDt1 = " + plusDt1);

        LocalDateTime plusDt2 = dt.plusYears(10);
        System.out.println("plusDt2 = " + plusDt2);

        Period period = Period.ofYears(10);
        LocalDateTime plusDt3 = dt.plus(period);
        System.out.println("plusDt3 = " + plusDt3);
    }
}
dt = 2018-01-01T13:30:59
plusDt1 = 2028-01-01T13:30:59
plusDt2 = 2028-01-01T13:30:59
plusDt3 = 2028-01-01T13:30:59

 

Temporal plus(long amountToAdd, TemporalUnit unit)

  • LocalDateTime 을 포함한 특정 시점의 시간을 제공하는 클래스는 모두 Temporal 인터페이스를 구현한다.
  • Temporal 은 특정 시점의 시간을 조작하는 기능을 제공한다.
  • plus(long amountToAdd, TemporalUnit unit) 를 호출할 때 더하기 할 숫자와 시간의 단위(Unit)를
    전달하면 된다. 이때 TemporalUnit 의 구현인 ChronoUnit 을 인수로 전달하면 된다.
  • 불변이므로 반환 값을 받아야 한다.
  • 참고로 minus() 도 존재한다.

편의 메서드 사용

  • 자주 사용하는 메서드는 편의 메서드가 제공된다.
  • dt.plus(10, ChronoUnit.YEARS)  => dt.plusYears(10)

Period를 사용한 조작

  • Period 나 Duration 은 기간(시간의 간격)을 뜻한다. 특정 시점의 시간에 기간을 더할 수 있다.

 

정리

시간을 조회하고 조작하는 부분을 보면 TemporalAccessor.get() , Temporal.plus() 와 같은 인터페이스를
통해 특정 구현 클래스와 무관하게 아주 일관성 있는 시간 조회, 조작 기능을 제공하는 것을 확인할 수 있다.
덕분에 LocalDateTime , LocalDate , LocalTime , ZonedDateTime , Instant 와 같은 수 많은 구현에 

관계없이 일관성 있는 방법으로 시간을 조회하고 조작할 수 있다.

 

하지만 모든 시간 필드를 다 조회할 수 있는 것은 아닙니다.

import java.time.LocalDate;
import java.time.temporal.ChronoField;

public class IsSupportedMain1 {

    public static void main(String[] args) {
        LocalDate now = LocalDate.now(); // 시, 분, 초 X
        int minute = now.get(ChronoField.SECOND_OF_MINUTE);
        System.out.println("minute = " + minute);

    }
}
Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: SecondOfMinute
	at java.base/java.time.LocalDate.get0(LocalDate.java:698)
	at java.base/java.time.LocalDate.get(LocalDate.java:641)
	at time.IsSupportedMain1.main(IsSupportedMain1.java:10)
  • LocalDate 는 날짜 정보만 가지고 있고, 분에 대한 정보는 없다. 
  • 따라서 분에 대한 정보를 조회하려고 하면 예외가 발생한다.
  • 이런 문제를 예방하기 위해 TemporalAccessor 와 Temporal 인터페이스는 현재 타입에서 특정 시간 단위나 필드
    를 사용할 수 있는지 확인할 수 있는 메서드를 제공한다.

 

//TemporalAccessor
boolean isSupported(TemporalField field);

//Temporal
boolean isSupported(TemporalUnit unit);
import java.time.LocalDate;
import java.time.temporal.ChronoField;

public class IsSupportedMain2 {

    public static void main(String[] args) {
        LocalDate now = LocalDate.now(); // 시, 분, 초 X
        boolean supported = now.isSupported(ChronoField.SECOND_OF_MINUTE);
        System.out.println("supported = " + supported);

        if (supported) {
            int minute = now.get(ChronoField.SECOND_OF_MINUTE);
            System.out.println("minute = " + minute);
        }
    }
}
supported = false
  • LocalDate 는 분의 초 필드를 지원하지 않으므로 ChronoField.SECOND_OF_MINUTE 를 조회하면 false 를 반환한다.

 

날짜와 시간을 조작하는 with() 메서드

import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAdjusters;

public class ChangeTimeWithMain {

    public static void main(String[] args) {
        LocalDateTime dt = LocalDateTime.of(2018, 1, 1, 13, 30, 59);
        System.out.println("dt = " + dt);

        LocalDateTime changeDt1 = dt.with(ChronoField.YEAR, 2020);
        System.out.println("changeDt1 = " + changeDt1);

        LocalDateTime changeDt2 = dt.withYear(2020);
        System.out.println("changeDt2 = " + changeDt2);

        // TemporalAdjuster 사용
        // 다음주 금요일
        LocalDateTime with1 = dt.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
        System.out.println("기준 날짜: " + dt);
        System.out.println("다음 금요일: " + with1);

        // 이번 달의 마지막 일요일
        LocalDateTime with2 = dt.with(TemporalAdjusters.lastInMonth(DayOfWeek.SUNDAY));
        System.out.println("기준 날짜: " + dt);
        System.out.println("같은 달의 마지막 일요일: " + with2);
    }
}
dt = 2018-01-01T13:30:59
changedDt1 = 2020-01-01T13:30:59
changedDt2 = 2020-01-01T13:30:59
기준 날짜: 2018-01-01T13:30:59다음 금요일: 2018-01-05T13:30:59
같은 달의 마지막 일요일 = 2018-01-28T13:30:59

 

Temporal with(TemporalField field, long newValue)

  • Temporal.with() 를 사용하면 날짜와 시간의 특정 필드의 값만 변경할 수 있다.
  • 불변이므로 반환 값을 받아야 한다.


편의 메서드

  • 자주 사용하는 메서드는 편의 메서드가 제공된다.
  • dt.with(ChronoField.YEAR, 2020) => dt.withYear(2020)


TemporalAdjuster 사용

  • with() 는 아주 단순한 날짜만 변경할 수 있다. 다음주 금요일, 이번 달의 마지막 일요일 같은 복잡한 날짜를 계산하고 싶다면 TemporalAdjuster 를 사용하면 된다.

TemporalAdjuster 인터페이스

public interface TemporalAdjuster {
 Temporal adjustInto(Temporal temporal);
}

 

원래대로 하면 이 인터페이스를 직접 구현해야겠지만, 자바는 이미 필요한 구현체들을 TemporalAdjusters 에 
만들어두었다. 우리는 단순히 구현체들을 모아둔 TemporalAdjusters 를 사용하면 된다.

  • TemporalAdjusters.next(DayOfWeek.FRIDAY) : 다음주 금요일을 구한다.
  • TemporalAdjusters.lastInMonth(DayOfWeek.SUNDAY) : 이번 달의 마지막 일요일을 구한다.
dayOfWeekInMonth(): 주어진 요일이 몇 번째인지에 따라 날짜를 조정한다.
firstDayOfMonth(): 해당 월의 첫째 날로 조정한다.
firstDayOfNextMonth(): 다음 달의 첫째 날로 조정한다.firstDayOfNextYear 다음 해의 첫째 날로 조정한다.
firstDayOfYear(): 해당 해의 첫째 날로 조정한다.
firstInMonth(): 주어진 요일 중 해당 월의 첫 번째 요일로 조정한다.
lastDayOfMonth(): 해당 월의 마지막 날로 조정한다.
lastDayOfNextMonth(): 다음 달의 마지막 날로 조정한다.
lastDayOfNextYear(): 다음 해의 마지막 날로 조정한다.
lastDayOfYear(): 해당 해의 마지막 날로 조정한다.
lastInMonth(): 주어진 요일 중 해당 월의 마지막 요일로 조정한다.
next(): 주어진 요일 이후의 가장 가까운 요일로 조정한다.
nextOrSame(): 주어진 요일 이후의 가장 가까운 요일로 조정하되, 현재 날짜가 주어진 요일인 경우 현재
날짜를 반환한다.
previous(): 주어진 요일 이전의 가장 가까운 요일로 조정한다.
previousOrSame(): 주어진 요일 이전의 가장 가까운 요일로 조정하되, 현재 날짜가 주어진 요일인 경우 현재
날짜를 반환한다.

 

 

 

날짜와 시간 문자열 파싱과 포맷팅

  • 포맷팅: 날짜와 시간 데이터를 원하는 포맷의 문자열로 변경하는 것, Date => String
  • 파싱: 문자열을 날짜와 시간 데이터로 변경하는 것, String => Date
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class FormattingMain1 {

    public static void main(String[] args) {
        // 포맷팅: 날짜를 문자로
        LocalDate date = LocalDate.of(2024, 12, 31);
        System.out.println("date = " + date);

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일");
        String formattedDate = date.format(formatter);
        System.out.println("날짜와 시간 포맷팅: " + formattedDate);

        // 파싱: 문자를 날짜로
        String input = "2030년 01월 01일";
        LocalDate parseDate = LocalDate.parse(input, formatter);
        System.out.println("parseDate = " + parseDate);
    }
}
date = 2024-12-31
날짜와 시간 포맷팅: 2024년 12월 31일
parseDate = 2030-01-01

 

  • LocalDate 과 같은 날짜 객체를 원하는 형태의 문자로 변경하려면 DateTimeFormatter 를 사용하면 된다. 
  • 여기에 ofPattern() 으로 원하는 포맷을 지정하면 된다. 여기서는 yyyy년 MM월 dd일 포맷을 지정했다.

 

DateTimeFormatter 패턴 공식 사이트

 

DateTimeFormatter (Java Platform SE 8 )

Parses the text using this formatter, without resolving the result, intended for advanced use cases. Parsing is implemented as a two-phase operation. First, the text is parsed using the layout defined by the formatter, producing a Map of field to value, a

docs.oracle.com

 

 

문자열을 날짜와 시간으로 파싱


문자열을 읽어서 날짜와 시간 객체로 만드는 것을 파싱이라 합니다.
이때 문자열의 어떤 부분이 년이고, 월이고 일인지 각각의 위치를 정해서 읽어야 합니다.

LocalDate.parse(input, formatter)

 

이때도 앞서 설명한 DateTimeFormatter 를 사용합니다.
2030년 01월 01일 의 경우 yyyy년 MM월 dd일 포맷으로 읽어들이면 됩니다.

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class FormattingMain2 {

    public static void main(String[] args) {
        // 포맷팅: 날짜와 시간을 문자로
        LocalDateTime now = LocalDateTime.of(2024, 12, 31, 13, 30, 59);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String formattedDateTime = now.format(formatter);
        System.out.println(now);
        System.out.println("날짜와 시간 포맷팅: " + formattedDateTime);
        
        // 파싱: 문자를 날짜와 시간으로
        String dateTimeString = "2030-01-01 11:30:00";
        LocalDateTime parsedDateTime = LocalDateTime.parse(dateTimeString, formatter);
        System.out.println("문자열 파싱 날짜와 시간: " + parsedDateTime);
    }
}
2024-12-31T13:30:59
날짜와 시간 포맷팅: 2024-12-31 13:30:59
문자열 파싱 날짜와 시간: 2030-01-01T11:30
  • LocalDateTime 과 같은 날짜와 시간 객체를 원하는 형태의 문자로 변경하려면 DateTimeFormatter 를 사용하면 된다. 
  • 여기에 ofPattern() 으로 원하는 포맷을 지정하면 된다. 여기서는 yyyy-MM-dd HH:mm:ss 포맷을 지정했다.

 

문자열을 날짜와 시간으로 파싱

 

문자열을 읽어서 날짜와 시간으로 파싱할 때는 년, 월, 일, 시, 분, 초의 위치를 정해서 읽어야 합니다.

LocalDateTime.parse(dateTimeString, formatter)

 

  • 이때도 앞서 설명한 DateTimeFormatter 를 사용한다.
  • 2030-01-01 11:30:00 의 경우 yyyy-MM-dd HH:mm:ss 포맷으로 읽어들이면 된다