Java를 이용해서 프로그래밍을 하다보면 날짜 데이터를 다뤄야 할 때가 많다. 날짜를 쉽게 파싱하고 다루기 위해 SimpleDateFormat 클래스를 많이 사용한다. 이번 포스트에서는 SimpleDateFormat 클래스의 사용 예제와 멀티쓰레드에서 고려해야 할 사항에 대해서 다뤄보겠다.
SimpleDateFormat은 java.text.DataFormat 이라는 abstract 클래스를 상속받은 concrete 클래스다. 이름에서 알 수 있듯이 간단하게(Simple) 날짜 포맷을 다룰 수 있는 메소드를 제공한다.
SimpleDateFormat 간단한 예제 (문자열 to Date 객체)
간단한 예제를 살펴보자.
이 코드를 실행하면 다음 결과를 얻을 수 있다.
1572534000000
코드를 살펴보면 "20191101"이라는 문자열이 있다. 이 자체로는 아무 의미를 가지고 있지 않은 그냥 문자열일 뿐이다. 프로그램이 이 문자열을 2019년 11월 1일이라는 의미로 파싱을 하기 위해서는 몇 단계를 거쳐야한다.
우선 이 문자열을 해석하기 위해서 SimpleDateFormat 객체를 생성한다. 이 때 파싱할 문자열의 포맷을 나타내는 문자열을 생성자의 인자로 주게된다. 잠시후 알아보겠지만 "yyyyMMdd"는 4글자의 년도, 2글자의 월, 2글자의 날짜를 표현하는 포맷(혹은 패턴)을 의미한다. 이제 만들어질 SimpleDateFormat 객체는 "yyyyMMdd"라는 형태의 문자열을 Date 객체로 만들어 줄 수 있다.
format.parse(dateStr) 코드를 실행하면, fotmat 객체는 입력받은 문자열을 가지고 "yyyyMMdd" 패턴으로 해석한다. "20191101"이라는 문자열은 각각 2019년, 11월, 1일 이라는 의미로 해체된다. 이 정보를 이용해서 parse() 메소드는 Date 객체를 만들어서 반환해준다. 만약 yyyyMMdd 패턴과 다른 문자가 입력되면 ParseException을 발생시킨다. 예를 들어, November 1 2019 역시 2019년 11월 1일을 의미하지만 yyyyMMdd 패턴으로는 파싱되지 않고 ParseException을 발생시킨다.
SimpleDateFormat 간단한 예제 (Date 객체 to 문자열)
문자열을 파싱해서 Date 객체를 얻어봤으니 그 반대도 해보자. Date 객체를 특정 포맷에 맞는 문자열로 만들어보자.
이 코드를 실행하면 실행 당시의 시간이 문자열로 출력된다.
20190114
new Date()로 만들어진 객체에 date.setTime(System.currentTimeMillis());를 이용해 현재 시간을 설정한다.
그리고 "yyyyMMdd" 패턴의 SimpleDateFormat 객체를 만들고 format() 메소드에 Date 객체를 넘겨주면, Date 객체가 가지고 있는 시간정보가 "yyyyMMdd" 포맷에 맞게 문자열로 만들어진다. 이전 예제보다 더 사용하기 간단하다.
Format 심볼
yyyyMMdd 라는 패턴을 이용해서 년도와 월, 일 정보를 문자열로 만들 수 있었다. y는 year, M은 Month, d는 day 에서 가져왔다는 것을 쉽게 유추해볼 수 있다. 그 밖에 다른 패턴도 있다.
문자 |
설명 |
예 |
G |
BC 혹은 AD |
AD |
y |
년도 |
1996; 96 |
M |
년 중 몇 번째 달인지 |
July; Jul; 07 |
w |
년 중 몇 번째 주인지(Week in year) |
27 |
W |
월 중 몇 번째 주인지 (Week in month) |
2 |
D |
년 중 몇 번째 날인지 (Day in year) |
189 |
d |
이번 달 몇 번째 날인지 (Day in month) |
10 |
F |
이번 달, 이번 주에서 몇 번째 날인지 (Day of Week in month) |
2 |
E |
이번 주에서 몇 번째 날인지 (Day in week) |
Tuesday; Tue |
a |
오전/오후 (AM/PM marker) |
PM |
H |
하루 중 시각 (Hour in day) (0-23) |
0 |
k |
하루 중 시각 (Hour in day) (1-24) |
24 |
K |
오전/오후 시각 (Hour in am/pm) (0-11) |
0 |
h |
오전/오후 시각 (Hour in am/pm) (1-12) |
12 |
m |
분 (Minute in hour) |
30 |
s |
초 (second in minute) |
55 |
S | 밀리초 (Millisecond) | 978 |
z |
타임존 (General time zone) |
Pacific Standard Time; PST; GMT-08:00 |
Z |
타임존 (RFC 822 time zone) |
-0800 |
한글자 단어가 의미하는 바는 위 표에서 찾아볼 수 있는 것과 같다. 대소문자에 따라서 의미가 달라질 수 있으니 주의하길 바란다. 예를 들어 대문자 'M'은 Month를 의미하지만 소문자 'm'은 Minute를 의미한다.
위 예제에서 사용했던 'yyyyMMdd' 패턴을 위 표에서 해석해보면,
- yyyy : 년도를 4글자로 표현
- MM : 월 단위를 2 글자로 표현
- dd : 일 단위를 2 글자로 표현
현재 시간을 위 표를 참조하여 다양한 형태로 출력해보면,
위 코드를 실행해보면 다음과 같다.
20190114
Mon Jan 14, 19
9:39 PM Mon Jan 14, 19
02019.Jan.14 CE 09:39 PM
2019-01-14T21:39:27.145+0900
이처럼 SimpleDateFormat을 이용하면 Date 객체를 필요한 포맷에 맞게 잘 말아서 사용할 수 있다.
멀티 쓰레드 환경에서의 SimpleDateFormat
DateFormat을 구현한 Concrete 객체를 사용할 때 Thread-safety에 대해서 주의깊게 살펴봐야 한다. 즉 여러 쓰레드가 같은 DateFormat 객체를 공유해서 사용할 때 동기화 문제가 발생할 수 있다. Java Doc 문서에도 이와 같은 내용이 명시되어 있다.
Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally
별도의 동기화 없이 여러 쓰레드에서 동시에 DateFormat을 공유해서 사용하면,
- java.lang.NumberFormatException
- java.lang.ArrayIndexOutOfBoundsException
등을 만나게 된다. 필자도 SimpleDateFormat 객체를 static으로 선언하여 여러 쓰레드에서 접근하도록 코드를 작성했다가 NuberFormatException을 맞은 적이 있다.
이런 문제는 동시성 이슈이기 때문에 코드 로직을 아무리 들여다봐도 단독으로 수행되는 로직 자체에는 문제가 없다. 따라서 Thread-Safety 문제를 알지 못하고 멀티쓰레드에서 사용하면 귀신같은 Exception 들을 만나게 된다. (멀티 쓰레드 환경이 그렇지뭐.. ㅜㅜ )
Thread-safety 문제로부터 자유롭기 위해서 3가지 정도의 해법을 생각해볼 수 있다.
1. Thread 마다 객체를 생성한다
2. Synchronized로 SimpleDateFormat 객체를 보호한다
3. ThreadLocal을 사용한다.
각각에 대해서 알아보자.
1. Thread 마다 객체를 생성
가장 쉽게 생각해 볼 수 있는게 "여러 쓰레드가 동시에 사용하지 못하게 하려면 각각 자신만의 SimpeDateFormat을 만들면 된다"는 접근 방법이다. 그 중에서 호출 될 때마다 객체를 만들면 생각 복잡하게 생각하지 않아도 된다.
1
2
3
4
|
public Date parseDateStr(String dateStr) throws ParseException {
return new SimpleDateFormat("yyyyMMdd").parse(dateStr);
}
|
cs |
이런식으로 parseDateStr() 메소드를 호출할 때마다 새로운 객체를 생성하면 동시성 이슈는 없다. 하지만 매번 객체를 생성하기 때문에 비효율적이고 속도도 느리다.
2. Synchronized로 SimpleDateFormat 객체를 보호한다.
자바에서는 언어 자체에서 동기화 도구를 제공한다. synchronized 키워드를 이용하면 쉽게 동기화를 구현할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
private DateFormat format = new SimpleDateFormat("yyyyMMdd");
public Date parseDateStr(String dateStr) throws ParseException {
Date date;
synchronized(format) {
date = format.parse(dateStr);
}
return date;
}
|
cs |
사용될 SimpleDateFormat 은 전역으로 선언해 놓고 synchronized 블록을 이용해 critical-section 을 만들어서 동시성 이슈를 해결한 코드다. 동시에 SimpleDateFormat 객체에 접근하지 않기 때문에 이상한 유령같은 Exception은 발생하지 않는다.
하지만 synchronized 블록에서 Block 되기 때문에 성능 저하가 발생할 수 있다.
3. ThreadLocal을 이용한 방법
ThreadLocal을 이용하여 개개의 Thread가 자신만의 SimpleDateFormat 객체를 갖고, 자신의 것만 참조해서 사용하게 할 수 있다.
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
28
29
30
31
32
33
34
|
private ThreadLocal<DateFormat> format = new ThreadLocal<DateFormat> () {
@Override
public DateFormat get() {
return super.get();
}
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyyMMdd");
}
@Override
public void remove() {
super.remove();
}
@Override
public void set(DateFormat value) {
super.set(value);
}
}
public Date parseDateStr(String dateStr) throws ParseException {
return format.get().parse(dateStr);
}
|
cs |
format.get() 을 호출하면 쓰레드는 자신의 SimpleDateFormat 객체를 가져오게 된다. 쓰레드들이 각각 자신의 객체를 사용하기 때문에 동시성 이슈는 발생하지 않는다.
지금까지 SimpleDateFormat 객체를 이용한 날짜 정보 다루기에 대해서 알아봤다.
댓글