[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 스펙으로 함께 넘어갑시다 : )