날짜와 시간 & 형식화

해당 포스팅의 내용은 Java의 정석 2권 - Chapter 10 날짜와 시간 & 형식화에 있는 내용을 요약한 것입니다. 해당 책으로 복습하며 정리한 내용이고 문제가 된다면 바로 해당 포스팅을 삭제하도록 하겠습니다.

날짜와 시간 & 형식화

타임존 포함 ISO 8601 문자열의 표현

날짜/시간 및 타임존을 다루는 국제적인 규약은 다양하다. RFC 822, 1036, 1123, 2822, 3339, ISO 8601 등이 있다. 여기서는 ISO 8601RFC 3339와 관련된 표기법을 알아본다.

1
2
3
4
5
6
7
8
// 로컬 시간을 의미하는 ISO 8601 문자열
2017-11-06T15:00:00.000

// UTC(GMT) 시간을 의미하는 ISO 8601 문자열
2017-11-06T06:00:00.000Z

// 로컬 시간을 의미하면서 UTC(GMT) 대비 +09:00 임을 의미하는 ISO 8601 문자열
2017-11-06T15:00:00.000+09:00
  • 2017-11-06T15:00:00.000ISO 8601의 기본 형식이다. 해당 시간이 로컬 시간 임을 의미한다.
  • 2017-11-06T06:00:00.000Z와 같이 뒤에 Z 식별자를 추가하면 해당 시간이 UTC(GMT) 시간 임을 의미한다.
  • 2017-11-06T15:00:00.000+09:00와 같이 뒤에 Z 대신 +HH:mm 식별자를 추가하면 해당 시간이 로컬 시간이면서 **UTC(GMT)**와 09:00 만큼 차이가 남을 의미한다. 이 형식의 장점은 인간이 손쉽게 추가적인 계산 없이 로컬 시간을 인지하면서 추가적으로 타임존 정보까지 제공하기 때문에 가장 인간친화적이라고 할 수 있다.

날짜와 시간

Date는 날짜와 시간을 다룰 목적으로 JDK 1.0부터 제공되어온 클래스이다. Date 클래스는 기능이 부족했기 때문에, Calendar라는 새로운 클래스를 그 다음 버젼인 JDK 1.1부터 제공하기 시작했다. Calendar는 Date보다는 훨씬 나았지만 몇 가지 단점들이 있었고, JDK 1.8부터 java.time 패키지로 기존의 단점들을 개선한 새로운 클래스들이 추가되었다.

Date 클래스는 java.util 패키지에 속해있다.

Date와 Calendar간의 변환

Calendar가 새로 추가되면서 Date는 대부분의 메서드가 ‘deprecated’되었으므로 잘 사용되지 않는다. 그럼에도 불구하고 여전히 Date를 필요로 하는 메서드들이 존재하기 때문에 Calendar를 Date로 또는 그 반대로 변환할 일이 생긴다.

1
2
3
4
5
6
7
8
9
1. Calendar를 Date로 변환
Calendar cal = Calendar.getInstance();
Date d1 = new Date(cal.getTimeInMillis()); // Date(long date)
Date d2 = cal.getTime();

2. Date를 Calendar로 변환
Date d = new Date();
Calendar cal = Calendar.getInstance();
cal.setTime(d);

Calendar.getInstance()를 통해서 얻은 인스턴스는 기본적으로 현재 시스템의 날짜와 시간에 대한 정보를 담고 있다. (GregorianCalendar, BuddhistCalendar)

형식화 클래스

자바의 형식화 클래스는 java.text 패키지에 포함되어 있으며 숫자, 날짜, 텍스트 데이터를 일정한 형식에 맞게 표현할 수 있는 방법을 객체지향적으로 설계하여 표준화하였다. 형식화 클래스는 형식화에 사용될 패턴을 정의하는데, 데이터를 정의된 패턴에 맞춰 형식화할 수 있을 뿐만 아니라 역으로 형식화된 데이터에서 원래의 데이터를 얻어낼 수도 있다. 즉, 형식화된 데이터의 패턴만 정의해주면 복잡한 문자열에서도 substring()을 사용하지 않고도 쉽게 원하는 값을 얻어낼 수 있다는 것이다.

DecimalFormat

형식화 클래스 중에서 숫자를 형식화 하는데 사용되는 것이 DecimalFormat이다. DecimalFormat을 이용하면 숫자 데이터를 정수, 부동소수점, 금액 등의 다양한 형식으로 표현할 수 있으며, 반대로 일정한 형식의 테스트 데이터를 숫자로 쉽게 변환하는 것도 가능하다.

1
2
3
double number = 1234567.89;
DecimalFormat df = new DecimalFormat("#.#E0");
String result = df.format(number);

Number 클래스는 Integer, Double과 같은 숫자를 저장하는 래퍼 클래스의 조상이며, doubleValue()는 Number에 저장된 값을 double형의 값으로 변환하여 반환한다. 이 외에도 intValue(), floatValue()등의 메서드가 Number클래스에 정의되어 있다.

SimpleDateFormat

Date와 Calendar만으로는 날짜 데이터를 원하는 형태로 다양하게 출력하는 것은 불편하고 복잡하다. 그러나 SimpleDateFormat을 사용하면 이러한 문제들이 간단하게 해결된다.

DateFormat은 추상클래스로 SimpleDateFormat의 조상이다. DateFormat는 추상클래스이므로 인스턴스를 생성하기 위해서는 getDateInstance()와 같은 static 메서드를 이용해야 한다. getDateInstance()에 의해서 반환되는 것은 DateFormat을 상속받아 완전하게 구현한 SimpleDateFormat 인스턴스이다.

1
2
3
4
5
Date today = new Date();
SimpleDateFormat dt = new SimpleDateFormat("yyyy-MM-dd");

// 오늘 날짜를 yyyy-MM-dd 형태로 변환하여 반환한다.
String result = df.format(today);

Date 인스턴스만 format 메서드에 사용될 수 있다.

1
2
DateFormat df = new SimpleDateFormat("yyyy년 MM월 dd일");
Date d = df.parse("2018년 6월 6일");

parse(String source)를 사용하여 날짜 데이터의 출력형식을 변환하는 방법을 보여주는 예제이다. Integer의 parseInt()가 문자열을 정수로 변환하는 것처럼 SimpleDateFormat의 parse(String source)는 문자열(source)을 날짜(Date인스턴스)로 변환해주기 때문에 매우 유용하게 쓰일 수 있다.

ChoiceFormat

ChoiceFormat은 특정 범위에 속하는 값을 문자열로 변환해준다. 연속적 또는 불연속적인 범위의 값들을 처리하는 데 있어서 if문이나 switch문은 적절하지 못한 경우가 많다. 이럴때 ChoiceFormat을 잘 사용하면 복잡하게 처리될 수밖에 없었던 코드를 간단하고 직관적으로 만들 수 있다.

MessageFormat

MessageFormat은 데이터를 정해진 양식에 맞게 출력할 수 있도록 도와준다. 데이터가 들어갈 자리를 마련해 놓은 양식을 미리 작성하고 프로그램을 이용해서 다수의 데이터를 같은 양식으로 출력할 때 사용하면 좋다. 하나의 데이터를 다양한 양식으로 출력할 때 사용한다.

그리고 SimpleDateFormat의 parse처럼 MessageFormat의 parse를 이용하면 지정된 양식에서 필요한 데이터만을 손쉽게 추출해 낼 수도 있다.

1
2
3
4
String msg = "Name: {0} \nTel: {1} \nnAge:{2} \nBirthday:{3}";
Object[] arguments = {"이름", "01-234-5678", "27", "04-27"};

String result = MessageFormat.format(msg, arguments);

MessageFormat에 사용될 양식인 문자열 msg를 작성할 때 ‘{숫자}’로 표시된 부분이 데이터가 출력될 자리이다.

데이터를 양식에 넣어서 출려하는것 뿐만 아니라, parse(String source)를 이용해서 출력된 데이터로부터 필요한 데이터만을 뽑아낼 수 있다.

Java.time 패키지

java의 탄생부터 지금까지 날짜와 시간을 다루는데 사용해왔던, Date와 Calendar가 가지고 있던 단점들을 해소하기 위해 JDK1.8부터 ‘java.time 패키지’가 추가되었다. 이 패키지는 다음과 같이 4개의 하위 패키지를 가지고 있다.

  • java.time : 날짜와 시간을 다루는데 필요한 핵심 클래스들을 제공
    • java.time.chrono : 표준(ISO)이 아닌 달력 시스템을 위한 클래스들을 제공
    • java.time.format : 날짜와 시간을 파싱하고, 형식화하기 위한 클래스들을 제공
    • java.time.temporal : 날짜와 시간의 필드(field)와 단위(unit)를 위한 클래스들을 제공
    • java.time.zone : 시간대(time-zone)와 관련된 클래스들을 제공

위의 패키지들에 속한 클래스들의 가장 큰 특징은 String 클래스처럼 **불변(immutable)**이라는 것이다. 그래서 날짜나 시간을 변경하는 메서드들은 기존의 객체를 변경하는 대신 항상 변경된 새로운 객체를 반환한다. 기존 Calendar 클래스는 변경 가능하므로, 멀티 쓰레드 환경에서 안전하지 못하다.

멀티 쓰레드 환경에서는 동시에 여러 쓰레드가 같은 개겣에 접근할 수 있기 때문에, 변경 가능한 객체는 데이터가 잘못될 가능성이 있으며, 이를 쓰레드에 안전(thread-safe)하지 않다고 한다.

java.time 패키지의 핵심 클래스

날짜와 시간을 하나로 표현하는 Calendar 클래스와 달리, java.time 패키지에서는 날짜와 시간을 별도의 클래스로 분리해 놓았다. 시간을 표현할 때는 LocalTime 클래스를 사용하고, 날짜를 표현할 때는 LocalDate 클래스를 사용한다. 그리고 날짜와 시간이 모두 필요할 때는 LocalDateTime 클래스를 사용하면 된다.

LocalDate + LocalTime -> LocalDateTime
날짜 시간 날짜 & 시간

여기에 시간대(time-zone)까지 다뤄야 한다면, ZonedDateTime 클래스를 사용한다.

LocalDateTime + 시간대 -> ZonedDateTime

Calendar는 ZonedDateTime처럼, 날짜와 시간 그리고 시간대까지 모두 가지고 있다. Date와 유사한 클래스로는 Instant가 있는데, 이 클래스는 날짜와 시간을 초 단위(정확히는 나노초)로 표현한다. 날짜와 시간을 초단위로 표현한 값을 타임스탬프(time-stamp) 라고 부르는데, 이 값은 날짜와 시간을 하나의 정수로 표현할 수 있으므로 날짜와 시간의 차이를 계산하거나 순서를 비교하는데 유리해서 데이터베이스에 많이 사용한다.

객체 생성하기 - now(), of()

java.time 패키지에 속한 클래스의 객체를 생성하는 가장 기본적인 방법은 now()와 of()를 사용하는 것이다.

1
2
3
4
LocalDate date = LocalDate.now(); // 2018-06-06
LocalTime time = LocalTime.now(); // 02:34:50.223
LocalDateTime dateTime = LocalDateTime.now(); // 2018-06-06T02:34:50.223
ZonedDateTime dateTimeInKr = ZonedDateTime.now(); // 2018-06-06T02:34:50.223+09:00[Asia/Seoul]

LocalDate와 LocalTime

LocalDateLocalTime은 java.time 패키지의 가장 기본이 되는 클래스이며, 나머지 클래스들은 이들의 확장이므로 이 두 클래스만 잘 이해하고 나면 나머지는 아주 쉬워진다.

객체를 생성하는 방법은 현재의 날짜와 시간을 LocalDate와 LocalTime으로 각각 반환하는 now()와 지정된 날짜와 시간으로 LocalDate와 LocalTime 객체를 생성하는 of()가 있다.

  • 특정 필드의 값 가져오기 - get(), getXXX()
  • 필드의 값 변경하기 - with(), plus(), minus()
  • 날짜와 시간의 비교 - isAfter(), isBefore(), isEqual()

Instant

Instant는 에포크 타임(EPOCH TIME, 1970-01-01 00:00:00 UTC)부터 경과된 시간을 나노초 단위로 표현한다. 사람이 보기에는 불편하지만, 단일 진법으로 다루기 때문에 계산에는 편리하다. 사람이 사용하는 날짜와 시간에는 여러 진법이 섞여있어서 계산하기 어렵다.

1
2
3
Instant now = Instant.now();
Instant now2 = Instant.ofEpochSecond(now.getEpochSecond());
Instant now3 = Instant.ofEpochSecond(now.getEpochSecond(), now.getNano());

Instant를 생성할 때는 위와 같이 now()와 ofEpochSecond()를 사용한다. 그리고 필드에 저장된 값을 가져올 때는 다음과 같이 한다.

1
2
long epochSec = now.getEpochSecond();
int nano = now.getNano();

위의 코드처럼, Instant는 시간을 초 단위와 나노초 단위로 나누어 저장한다. 오라클 데이터베이스의 타임스탬프(timestamp)처럼 밀리초 단위의 EPOCH TIME을 필요로 하는 경우를 위해 toEpochMilli()가 정의되어 있다.

1
long toEpochMilli()

Instant는 항상 UTC(+00:00)를 기준으로 하기 때문에, LocalTime과 차이가 있을 수 있다. 예를 들어 한국은 시간대가 ‘+09:00’이므로 Instant와 LocalTime간에는 9시간의 차이가 있다. 시간대를 고려해야하는 경우 OffsetDateTime을 사용하는 것이 더 나은 선택일 수 있다.

UTC는 ‘Coordinated Universal Time’의 약어로 ‘세계 협정시’이라고 하며, 1972년 1월 1일부터 시행된 국제 표준시이다. 이전에 사용되던 GMT(Greenwich Mean Time)와 UTC는 거의 같지만, UTC가 좀 더 정확하다.

LocalDateTime과 ZonedDateTime

LocalDateTime에 시간대(time-zone)를 추가하면, ZonedDateTime이 된다. 기존에는 TimeZone클래스로 시간대를 다뤘지만 새로운 시간 패키지에서는 ZoneId라는 클래스를 사용한다. ZoneId는 일광 절약시간(DST, Daylight Saving Time)을 자동적으로 처리해주므로 더 편리하다.

LocalDate에 시간 정보를 추가하는 atTime()을 쓰면 LocalDateTime을 얻을 수 있는 것처럼, LocalDateTime에 atZone()으로 시간대 정보를 추가하면, ZonedDateTime을 얻을 수 있다.

1
2
3
ZoneId zid = ZoneId.of("Asia/Seoul");
zonedDateTime zdt = dateTime.atZone(zid);
Syste.out.println(zdt); // 2018-06-06T14:23:50.235+09:00[Asia/Seoul]

만일 현재 특정 시간대의 시간, 예를 들어 뉴욕을 알고 싶다면 다음과 같이 하면 된다.

1
2
ZoneId nyId = ZoneId.of("America/New_York");
ZonedDateTime nyTime = ZonedDateTime.now().withZoneSameInstant(nyId);

ZoneOffSet

UTC로부터 얼마만큼 떨어져 있는지를 ZoneOffSet으로 표현한다. 위의 결과에서 알 수 있듯이 서울은 ‘+9’이다. 즉, UTC보다 9시간(32400초=60*60*9)이 빠르다.

1
2
ZoneOffset krOffset = ZonedDateTime.now().getOffset();
int krOffsetInSec = KrOffset.get(ChronoField.OFFSET_SECONDS); // 32400초

OffsetDateTime

ZonedDateTime은 ZoneId로 구역을 표현하는데, ZoneId가 아닌 ZoneOffset을 사용하는 것이 OffSetDateTime이다. ZoneId는 일광절약시간처럼 시간대와 관련된 규칙들을 포함하고 있는데, ZoneOffset은 단지 시간대를 시간의 차이로만 구분한다. 컴퓨터에게 일광절약시간처럼 계절별로 시간을 더했다 뺐다 하는 것과 같은 행위는 위험하다. 아무런 변화 없이 일관된 시간체계를 유지하는 것이 더 안전하다. 같은 지역 내의 컴퓨터 간에 데이터를 주고 받을 때, 전송시간을 표현하기에 LocalDateTime이면, 충분하겠지만, 서로 다른 시간대에 존재하는 컴퓨터간의 통신에는 OffsetDateTime이 필요하다.

일광 절약 시간제(Daylight saving time, DST) 또는 서머 타임(summer time)은 하절기에 표준시를 원래 시간보다 한 시간 앞당긴 시간을 쓰는 것을 말한다. 즉, 0시에 일광 절약 시간제를 실시하면 1시로 시간을 조정해야 하는 것이다. 실제 낮 시간과 사람들이 활동하는 낮 시간 사이의 격차를 줄이기 위해 사용한다.

1
2
3
4
5
ZonedDateTime zdt = ZondedDateTime.of(date, time, zid);
OffsetDateTime odt = offsetDateTime.of(date, time, krOffset);

// ZonedDatetime -> OffsetDateTime
OffsetDateTime odt = zdt.toOffsetDateTime();

OffsetDateTime을 ZonedDateTime처럼, LocalDate와 LocalTime에 ZonedOffset을 더하거나, ZonedDateTime에 toOffsetDateTime()을 호출해서 얻을 수도 있다.

지금까지의 내용을 예제로 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
LocalDate ld = LocalDate.now();
LocalTime lt = LocalTime.now();
System.out.println("LocalDate : " + ld);
System.out.println("LocalTime : " +lt);

LocalDateTime dt = LocalDateTime.of(ld, lt);
System.out.println("LocalDateTime : " + dt);

ZoneId zid = ZoneId.of("Asia/Seoul");
ZonedDateTime zdt = dt.atZone(zid);
System.out.println("ZonedDateTime1 : " + zdt);

ZonedDateTime seoulTime = ZonedDateTime.now();
System.out.println("ZonedDateTime2 : " + seoulTime);

// 특정 구역 시간의 다른 구역 시간 구하기
ZoneId nyId = ZoneId.of("America/New_York");
ZonedDateTime nyTime = ZonedDateTime.now().withZoneSameInstant(nyId);
System.out.println("ZonedDateTime3 : " + nyTime);

// 출력 결과
LocalDate : 2018-06-06
LocalTime : 10:28:37.743
LocalDateTime : 2018-06-06T10:28:37.743
ZonedDateTime1 : 2018-06-06T10:28:37.743+09:00[Asia/Seoul]
ZonedDateTime2 : 2018-06-06T10:28:37.744+09:00[Asia/Seoul]
ZonedDateTime3 : 2018-06-05T21:28:37.747-04:00[America/New_York]

TemporalAdjusters

plus(), minus()와 같은 메서드로 날짜와 시간을 계산하기에는 불편한 경우가 있다. (Ex. 이번 달의 3번째 금요일) 그래서 자주 쓰일만한 날짜 계산들을 대신 해주는 메서드를 정의해놓은 것이 TemporalAdjusters 클래스이다.

1
2
LocalDate today = LocalDate.now();
LocalDate nextMonday = today.with(TemporalAdjusters.next(DayofWeek.MONDAY))l

with()는 LocalTime, LocalDateTime, ZonedDateTime, Instant 등 대부분의 날짜와 시간에 관련된 클래스에 포함되어 있다.

Period와 Duration

Period는 날짜의 차이를, Duration은 시간의 차이를 계산하기 위한 것이다.

between()

두 날짜 date1과 date2의 차이를 나타내는 Period는 between()으로 얻을 수 있다.

1
2
3
LocalDate date1 = LocalDate.of(2014, 1, 1);
LocalDate date2 = LocalDate.of(2018, 6, 6);
Period pe = Period.between(date1, date2)

date1이 date2보다 날짜 상으로 이전이면 양수로, 이후면 음수로 Period에 저장된다. 그리고 시간차이를 구할 때는 Duration을 사용한다는 것을 제외하고는 Period와 똑같다.

Period, Duration에서 특정 필드의 값을 얻을 때는 get()을 사용한다.

1
2
3
4
5
6
long year = pe.get(ChronoUnit.YEARS); // int getYears()
long month = pe.get(ChronoUnit.MONTHS); // int getMonths()
long day = pe.get(ChronoUnit.DAYS); // int getDays()

long sec = du.get(ChronoUnit.SECONDS); // long getSeconds()
long nano = du.get(ChronoUnit.NANOS); // int getNano()

between()과 until()

until()은 between()과 거의 같은 일을 한다. between()은 static 메서드이고, until()은 인스턴스 메서드라는 차이가 있다.

Period는 년월일을 분리해서 저장하기 때문에, D-day를 구하려는 경우에는 두 개의 매개변수를 받는 until()을 사용하는 것이 낫다.

파싱과 포맷

날짜와 시간을 원하는 형식으로 출력하고 해석(파싱)을 위한 형식화(formatting)와 관련된 클래스들은 java.time.format 패키지에 들어 있다. 그 중에서 DateTimeFormatter가 핵심이다. 이 클래스에는 자주 쓰이는 다양한 형식들을 기본적으로 정의하고 있으며, 그 외의 형식이 필요하다면 직접 정의해서 사용할 수도 있다.

1
2
3
LocalDate date = LocalDate.of(2016, 6, 6);
String yyyymmdd = DateTimeFormatter.ISO_LOCAL_DATE.format(date); // "2016-06-06"
String yyyymmdd = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // "2018-06-06

날짜와 시간의 형식화에는 format()이 사용되는데, 이 메서드는 DateTimeFormatter뿐만 아니라 LocalDate나 LocalTime같은 클래스에도 있다. 같은 기능을 하기 때문에 상황에 따라 편한 쪽을 선택해서 사용하면 된다.

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

문자열을 날짜 또는 시간으로 변환하려면 static 메서드 parse()를 사용하면 된다. 자주 사용되는 기본적인 형식의 문자열은 ISO_LOCAL_DATE와 같은 형식화 상수를 사용하지 않고도 파싱이 가능하다.

1
2
3
4
LocalDate date = LocalDate.parse("2018-06-06", DateTimeFormatter.ISO_LOCAL_DATE);
LocalDate newDate = LocalDate.parse("2018-06-06");
LocalTime newTime = LocalTime.parse("23:59:59");
LocalDateTime newDateTime = LocalDateTime.parse("2018-06-06T23:59:59");

참고

Author

KimJongMin

Posted on

2018-06-06

Updated on

2021-03-22

Licensed under

댓글