[Java] 당신이 SimpleDateFormat 을 쓰지 말아야 할 이유
Why need to not use SimpleDateFormat?
들어가기에 앞서
제목이 좀 자극적이었나요? 전혀 자극적이지 않습니다.
엄밀하게는, production 에 SimpleDateFormat 을 쓰면 안되는 이유입니다.
더 엄밀하게는, multi-thread 환경에서 사용하면 안되는 이유입니다.
SimpleDateFormat 이란?
Java7 에서 제공하는 (locale sensitive 한) SimpleDateFormat 클래스는 다음과 같은 역할을 합니다.
- String 을 파싱하여 Date 객체를 생성합니다.
- Date 객체를 formatting 하여. String 화 합니다.
- python datetime 패키지의 strftime, strptime 과 같은 역할을 합니다.
그런데 왜 사용하지말아야 할까요?
해답은 oracle javadocs 에 있습니다.
https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html
Synchronization
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.
- See Also:
- Java Tutorial,
Calendar,TimeZone,DateFormat,DateFormatSymbols, Serialized Form
synchronized 하지 않기 때문입니다.
근데 이게 왜?
일반적으로 SimpleDateFormat 을 사용하시는 분들이 대부분,
public static final 로 선언하고 static 객체를 참조하기 때문이죠.
우리팀의 Legacy 코드에도 비슷한 문제를 가지고 있었습니다. (이 아티클을 작성하는 주된 이유이기도 합니다)
HTTP 요청을 받는 레이어는 thread pool 로 connection 을 관리합니다.
다시 말해, WEB, WAS 는 기본적으로 multi-thread 환경이란 뜻입니다.
절대로 절대로 절대로 SimpleDateFormat 을 public static 객체로 선언해놓고 여러곳에서 참조해서는 안됩니다.
정말로 문제가 될까?
네 문제가 됩니다.
아래 테스트 코드를 예시로 들겠습니다.
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.stream.IntStream;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class ThreadNotSafeSimpledateformatApplicationTests {
private static final Logger logger = LoggerFactory.getLogger(
ThreadNotSafeSimpledateformatApplicationTests.class);
public static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss");
private static final long MIN_TIMESTAMP_RANGE = 1400000000L;
private static final long MAX_TIMESTAMP_RANGE = 1570000000L;
private static final int ITERATE_START_VALUE = 0;
private static final int ITERATE_END_VALUE = 10000;
@Test
public void it_is_looks_like_thread_safe_simpledateformat() {
for (int i = ITERATE_START_VALUE; i < ITERATE_END_VALUE; i++) {
Date randomDate = new Date(generateRandomTimestamp());
assertEquals(dateToFormattedString1 (randomDate),dateToFormattedString2(randomDate));
}
}
@Test
public void why_thread_unsafe_simpledateformat() {
IntStream.range(ITERATE_START_VALUE, ITERATE_END_VALUE).parallel().forEach(e -> {
final Date randomDate = new Date(generateRandomTimestamp());
assertEquals(dateToFormattedString1 (randomDate),dateToFormattedString2(randomDate));
});
}
public static long generateRandomTimestamp() {
// generate time range in 1400000000~1570000000
return (long) (Math.random() * ((MAX_TIMESTAMP_RANGE - MIN_TIMESTAMP_RANGE) + 1) + MIN_TIMESTAMP_RANGE);
}
public static String dateToFormattedString1(Date date) {
return SIMPLE_DATE_FORMAT.format(date);
}
public static String dateToFormattedString2(Date date) {
LocalDateTime localDateTime = date.toInstant().atZone(ZoneId.of("Asia/Seoul")).toLocalDateTime();
return localDateTime.format(DATE_TIME_FORMATTER);
}
}
Utilization Method
util 성 method 먼저 설명을 드리겠습니다.
- generateRandomTimestamp
- 사전에 지정한 범위 내에서 random 한 unix timestamp 를 생성하여 long 형태로 반환한다.
- dateToFormattedString1
- 주어진 Date 객체를 SimpleDateFormatter 를 이용해 formatting 한다.
- dateToFormattedString2
- 주어진 Date 객체를 DateTimeFormatter 를 이용해 formatting 한다.
Test Flow
generateRandomTimestamp 로 생성한 long 값으로 Date 를 객체를 만듭니다.
만든 Date 객체를 dateToFormattedString1, dateToFormattedString2 메서드를 통해 stringify 하고,
두 문자열 값을 Assertion 합니다.
Test Method
- it_is_look_like_thread_safe_simpledateformat
- sequential 하게 iterative 한 loop 문을 돌면서 Test Flow 를 진행합니다.
- why_thread_unsafe_simpledateformat
- Parallel stream 을 이용해 병렬로 Test Flow 를 진행합니다.
Test Result
이런 이런, 정말로 why_thread_unsafe_simpledateformat 테스트 메서드에서 실패를 했습니다.
좀 더 자세하게 실패 사유를 보면, 정말로 stringify 된 두 값이 달라 에러를 발생하고 있습니다.

해결 방법
두 가지 안이 있습니다.
- SimpleDateFormat 을 사용해야하는 로직안에서 new
- apache common lang 패키지의 FastDateFormat 을 사용
- Java8 에서 지원하는 DateTimeFormatter 로 전환
SimpleDateFormat 을 public static 으로 만들고, 여러 곳에서 참조한다면 당연히 thread un-safe 합니다.
따라서 사용되는 로직에서 new SimpleDateFormat(“format”) 하면 ‘심플’ 하게 해결이 됩니다.
하지만 비즈니스 로직안에 new SimpleDateFormat() 을 하고 싶은 개발자분은 없으리라 믿습니다.
가급적 2안 혹은, (진행하는 프로젝트가 Java8+ 라면,) 3안을 선택하여 리팩토링을 진행하시기 바랍니다.
2개의 댓글
홍길동 · 2020-03-31 22:39:50
비즈니스 로직안에 new SimpleDateFormat() 을 하고 싶은 개발자가 바로 접니다!
Engineer myoa · 2020-04-02 23:18:15
JSR310 스펙으로 함께 넘어갑시다 : )