본문 바로가기
Old Posts/Java

[Java] SimpleDateFormat을 이용한 날짜 포맷 변환 예제

by A6K 2021. 3. 30.

Java를 이용해서 시간 데이터를 다루는 방법에는 몇 가지가 있다. 그 중에 가장 간단한 방법은 SimpleDateFormat 클래스를 이용해서 문자열을 파싱하거나 문자열로 시간 데이터를 표현하기도 한다. 이번 포스트에서는 SimpleDateFormat 클래스의 사용방법과 예제, 주의 사항을 다뤄보겠다.

SimpleDateFormat은 java.text.DataFormat 이라는 abstract 클래스를 상속받은 클래스다. 이름에서도 알 수 있듯이 간단하게(Simple) 날짜 포맷을 다룰 수 있는 메소드를 제공한다.

SimpleDateFormat 예제 - 문자열을 Date 객체로 변환

문자열로 표현된 날짜 정보를 파싱하여 Date 객체로 변환해보자.

// 변환할 문자열
String dateStr = "20211101";

// SimpleDateFormat 생성
String datePattern = "yyyyMMdd";
SimpleDateFormat format = new SimpleDateFormat(datePattern);

try {
    // 문자열을 Date 객체로 파싱
    Date date = format.parse(dateStr);

    // 결과 출력
    System.out.println(date.getTime());
} catch (ParseException e) {
    System.err.println("dateStr : " + dateStr + ", datePattern:" + datePattern);
    e.printStackTrace();
}

이 코드를 실행하면 다음 결과를 얻을 수 있다.

1635692400000

예제 코드를 살펴보자.

우선 변환할 문자열 "20211101"이라는 문자열이 있다. 이 문자열은 그 자체로 아무 의미가 없다. 그냥 문자열 데이터일 뿐이다. 아무의미 없는 문자열에서 2021년 11월 1일이라는 의미를 파싱해내기 위해서는 몇 단계를 거쳐야한다.

SimpleDateFormat 객체를 생성한다. 객체를 생성할 때, 파싱할 문자열의 포맷을 생성자의 인자로 입력해줘야한다. 위 코드에서는 'yyyyMMdd'가 날짜 포맷에 해당한다. 'yyyyMMdd'는 4글자의 년도(y), 2글자의 월(M), 2글자의 날짜(d)로 표현된 날짜 포맷을 의미한다.

그런 다음 format.parse(dateStr) 코드를 실행한다. 이 메소드는 'yyyyMMdd' 포맷으로 '20211101'이라는 문자열을 파싱해서 Date 객체로 만들어준다. '20211101'이라는 문자열은 이 메소드에서 '2021년 11월 1일'이라는 의미로 해석된다.

만약 'yyyyMMdd' 패턴과 다른 문자가 입력되면 ParseException 을 발생시킨다. 예를 들어, '2021-11-01'이라는 문자열은 20211101과 같은 의미를 갖겠지만 'yyyyMMdd' 포맷으로 파싱할 수 없어 ParseException을 발생시킨다. ('yyyy-MM-dd' 포맷으로 파싱해야한다)

SimpleDateFormat 예제 - Date 객체를 문자열로 변환

이번에는 반대로 Date 객체를 문자열로 변환해보자.

// 현재 시간을 Date 객체로 가져옴
Date date = new Date();

// 혹은
// Date date = new Date();
// date.setTime(System.currentTimeMillis());

// SimpleDateFormat 객체 생성
String datePattern = "yyyyMMdd";
SimpleDateFormat format = new SimpleDateFormat(datePattern);

// 문자열로 변환
String dateStr = format.format(date);

// 출력
System.out.println(dateStr);

이 코드를 실행하면 다음 결과를 얻을 수 있다.

20210329

예제 코드를 살펴보자.

현재시간정보를 얻기 위해서 Date 객체를 생성한다. 그런다음 마찬가지로 SimpleDateFormat객체를 생성한다.

SimpleDateFormat 클래스의 format() 메소드를 이용하면 Date 객체가 가지고 있는 시간정보가 'yyyyMMdd' 포맷에 맞게 문자열로 만들어진다.

SimpleDateFormat 심볼

SimpleDateFormat 객체를 생성할 때 날짜 포맷을 입력했었다. 'yyyyMMdd'처럼 날짜 포맷에서 년도, 월, 일 등을 나타내는 문자들이 미리 정해져있어 잘 가져다 사용하면 된다.

문자 설명
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글자로 표현

현재 시간 정보를 위 표를 참고해서 다양한 형태로 표현해보자면

Date date = new Date();
date.setTime(System.currentTimeMillis());

String datePattern1 = "yyyyMMdd";
SimpleDateFormat format1 = new SimpleDateFormat(datePattern1);
System.out.println(format1.format(date));

String datePattern2 = "EE MMM d, yy";
SimpleDateFormat format2 = new SimpleDateFormat(datePattern2);
System.out.println(format2.format(date));

String datePattern3 = "h:mm a EE MMM d, yy";
SimpleDateFormat format3 = new SimpleDateFormat(datePattern3);
System.out.println(format3.format(date));

String datePattern4 = "yyyyy.MMMMM.dd GGG hh:mm aaa";
SimpleDateFormat format4 = new SimpleDateFormat(datePattern4);
System.out.println(format4.format(date));

String datePattern5 = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
SimpleDateFormat format5 = new SimpleDateFormat(datePattern5);
System.out.println(format5.format(date));

이 코드를 실행해보면 지금 시간을 다양한 형태로 출력해볼 수 있다.

20210329
Mon Mar 29, 21
9:33 AM Mon Mar 29, 21
02021.March.29 AD 09:33 AM
2021-03-29T09:33:30.976+0900

필요한 형태에 따라서 포맷을 다양하게 변화시켜서 사용하면 된다.

멀티 쓰레드 환경에서 SimpleDateFormat

DateFormat을 구현한 객체를 사용할 때, Thread-safety에 대해서 주의깊게 생각해봐야한다. 즉, 여러 쓰레드가 같은 DateFormat 객체를 공유해서 사용할 때 동기화 문제가 발생할 수 있다. JavaDoc 문서를 읽어보면 다음 내용을 확인할 수 있다.

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을 사용하면서 Thread-Safety 문제로부터 자유롭기 위해서는 3가지 정도의 해법을 생각해볼 수 있다.

  1. 호출할 때마다 객체를 생성한다
  2. 동기화를 사용한다
  3. ThreadLocal을 사용한다.

1. 호출할 때마다 객체를 생성

가장 쉽게 생각해볼 수 있는 것은 DateFormat을 사용할 때마다 객체를 생성해서 사용하는 것이다.

public Date parseDateStr(String dateStr) throws ParseException {

    return new SimpleDateFormat("yyyyMMdd").parse(dateStr);
}

이렇게 Format을 사용할 때마다 객체를 생성해주면 동시성 문제가 해결된다. 하지만 매번 객체를 생성하기 때문에 비효율적이다. 자주 호출되는 로직에 이런 코드를 사용하면 성능문제가 발생할 수 있다.

2. 동기화사용

Java에서 제공하는 synchronized 키워드를 이용해서 동기화를 구현하는 방법도 있다.

private DateFormat format = new SimpleDateFormat("yyyyMMdd");

public Date parseDateStr(String dateStr) throws ParseException {

    Date date;

    synchronized(format) {

        date = format.parse(dateStr);

    }

    return date;
}

날짜 데이터를 포매팅할 SimpleDateFormat 객체를 전역으로 선언해놓고, 객체에 접근하는 코드 블럭에 synchronized 블럭으로 만들어 두면, 동시성 문제가 발생하지 않는다.

3. ThreadLocal 사용

synchronized를 이용해서 동기화를 시켜놓으면 동시성 문제는 발생하지 않지만 해당 부분에서 코드 실행이 블럭되기 때문에 약간의 성능저하가 발생할 수 있다. 이 경우 쓰레드마다 SimpleDateFormat 객체를 생성해서 자기것을 사용하도록하면 약간의 메모리를 사용하지만 블럭은 발생하지 않아서 더 성능을 뽑아낼 수 있다.

이 때 ThreadLocal 변수를 사용하면 된다.

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);
}

ThreadLocal을 사용하는게 가장 편하고, 가장 빠른 방법이 될 것 같다.

댓글