Java SimpleDateFormat 위험성

죠랭이·2021년 2월 26일
0

Java

목록 보기
1/2
post-thumbnail

'Clean Code' 책을 보면서 Thread-Safe하지 않은 케이스로 SimpleDateFormat 클래스를 접하였다. 개인적으로 호기심이 생겨 좀 더 찾아보게 되었다.

먼저, 공식 Reference 문서에서 다음의 내용을 발견하였다.

요점만 말하자면, SimpleDateFormat은 Multi-Thread 환경에서 Safe하지 않다는 말이다. Single 스레드 환경이 아닐 경우 의도했던 결과대로 나오지 않을 수 있다는 점. 그렇다면 왜 이런 것일까?

원인은 SimpleDateFormat 인스턴스가 중간 결과값을 필드값으로 저장해놓기 때문. 만약, 두 개의 Thread가 하나의 SimpleDateFormat 인스턴스에 접근하여 사용할 시 인스턴스 필드값이 일관적이지 못하기에 Thread마다 사용하는 인스턴스 필드값이 달라짐. 이로 인해 의도했던 결과와 다르게 출력함.

public class TestExecution {
	
	static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");

	public static void main(String[] args) {
		
		// init
		ExecutorService exec = Executors.newFixedThreadPool(5);
		List<Future<Date>> results = new ArrayList<Future<Date>>();

		try {
			// SimpleDateFormat을 이용한 parse 작업 (멀티 쓰레드)
			Callable<Date> task = new Callable<Date>() {
				public Date call() throws Exception {
					String threadName = Thread.currentThread().getName();
					System.out.println("threadName ==> " + threadName);
					return TestExecution.sdf.parse("20210225");
				}
			};

			// 10건 수행
			for (int i = 0; i < 10; i++) {
				results.add(exec.submit(task));
			}

			// 결과 출력
			for (Future<Date> result : results) {
				System.out.println(result.get());
			}

		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}
}

다음과 같은 예제 코드를 실행한 결과, 다음의 결과값이 출력되었다.

threadName ==> pool-1-thread-1
threadName ==> pool-1-thread-2
threadName ==> pool-1-thread-4
threadName ==> pool-1-thread-3
threadName ==> pool-1-thread-5
threadName ==> pool-1-thread-4
threadName ==> pool-1-thread-5
threadName ==> pool-1-thread-3
threadName ==> pool-1-thread-1
threadName ==> pool-1-thread-4
Fri Dec 25 00:00:00 KST 2020
Thu Feb 25 00:00:00 KST 2021

java.util.concurrent.ExecutionException: java.lang.NumberFormatException: empty String

그나마 에러를 던진 경우엔 다행이지만 다음과 같이 의도치 않은 결과값이 저장되면 예상치 못한 버그를 생성하게 되어 디버깅하기 힘든 프로그램 에러를 발생시킬 수 있다. 그렇다면 어떻게 해결할 수 있을까? 여러 가지 해결 방법이 존재하나 크게 3가지 방법을 소개해보겠다.

  1. 매번 new 생성자로 새로운 SimpleDateFormat 객체 생성하기

    • 이는 가장 쉽게 해결할 수 있는 방법으로 parse()하는 하나의 static SimpleDateFormat 객체 대신 매번 return할 때마다 new SimepleDateFormat() 객체를 생성하여 parse() 작업을 해준다.
      return new SimpleDateFormat("yyyyMMdd").parse("20210225");
  2. Java 8 버젼부터 적용된 DateTimeFormatter 사용하기

    • Java 8 버젼부터 Thread-Safe한 DateTimeFormatter라는 날짜 형식 클래스가 등장한다. 해당 클래스를 사용하여 Date Pattern 파싱할 경우 멀티쓰레드 환경에서 잘못 출력될 걱정할 필요가 없다.
  3. ThreadLocal 활용

    • 아래 코드로 ThreadLocal 리스트를 먼저 선언하여 파싱해야 한다.
    private static ThreadLocal<DateFormat> df = 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 static Date parseDateStr(String dateStr) throws ParseException
    {
    	 
        return df.get().parse(dateStr);
     
    }
    
    public static void main(String...args) {
    	...
        ...
        ...
    	return parseDateStr("20210225");
        ...
        ...
        ...
    }

    다음을 선언하여 위의 main함수를 실행시킨 결과, 다음의 결과가 출력되었다.

threadName ==> pool-1-thread-2
threadName ==> pool-1-thread-1
threadName ==> pool-1-thread-3
threadName ==> pool-1-thread-4
threadName ==> pool-1-thread-5
threadName ==> pool-1-thread-5
threadName ==> pool-1-thread-4
threadName ==> pool-1-thread-3
threadName ==> pool-1-thread-1
threadName ==> pool-1-thread-5
Thu Feb 25 00:00:00 KST 2021
Thu Feb 25 00:00:00 KST 2021
Thu Feb 25 00:00:00 KST 2021
Thu Feb 25 00:00:00 KST 2021
Thu Feb 25 00:00:00 KST 2021
Thu Feb 25 00:00:00 KST 2021
Thu Feb 25 00:00:00 KST 2021
Thu Feb 25 00:00:00 KST 2021
Thu Feb 25 00:00:00 KST 2021
Thu Feb 25 00:00:00 KST 2021

Synchronized라는 키워드를 활용(Block구간에는 하나의 쓰레드만 접근하여 작업하는 기능)하여 동기화 작업으로도 해당 이슈를 해결할 수 있으나 이럴 경우 성능 저하를 불러일으키기에 웬만하면 다른 방법을 사용하기를 권하고 싶다.

지금까지 Java SimpleDateFormat 클래스의 위험성에 대해 공부해보았다. 해당 이슈를 직접 눈으로 보기 위해 여러 블로그 코드들을 참고해보았는데 다음의 코드가 쉽게 이해할 수 있었다. 실제 프로그래밍을 할 적에는 다음과 같은 이슈가 없도록 주의깊게 사용해야겠다!

출처:
https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html
https://lng1982.tistory.com/229
https://stackoverflow.com/questions/6840803/why-is-javas-simpledateformat-not-thread-safe
profile
슈퍼 개발자를 목표로 하는 주니어

0개의 댓글