[Java] 당신이 SimpleDateFormat 을 쓰지 말아야 할 이유

글쓴이 Engineer Myoa 날짜

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

답글 남기기

Avatar placeholder

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다