SimpleDateFormat은 thread-safe하지 않습니다.

설명

SonarQube를 이용해 코드의 취약점이나 잠재적 오류 발생 포인트를 확인하고 있는도중에 아래와 같은 오류 메시지를 발견하였습니다.

sonarqube 이미지

이전에 작업하셨던 분이 String값을 Date 객체로 변환하기 위해 SimpleDateFormat을 쓰고,
모든 객체에서 참조하면 관리가 편해지고, 객체 생성을 안해도 되니까 static final을 이용해 만드신거 같습니다.

  • 다행이 사용되는 부분이 없었습니다.

SonarQube에서는 저렇게 코드를 작성하는게 문제가 있다고 하는데 어떤문제가 있는지 한번 알아보도록 하겠습니다.
해당 에러의 구체적인 설명은 아래와 같이 나와있는데요

sonqrqube 이미지2

가장 윗줄에 Non-thread-safe fields should not be static 라고 적혀 있네요.
스레드에서 안전하지 않은 필드로 static이 되면 안된다. 라는 말 같은데요 아래 설명을 더 읽어보면

1
2
3
자바 라이브러리의 모든 클래스는 thread-safe 하게 작성되지 않았습니다. 멀티 스레드 방식으로 이것들을 사용하면 런타임에서 exception 또는 데이터 문제를 야기할 가능성이 높습니다. 

Calendar, DateFormat, javax.xml.xpath.XPath, javax.xml.validation.SchemaFactory에 static을 사용할때 이런 이슈가 발생합니다.

이런 설명과 함께 예제 코드까지 보여주고 있습니다.
결국 SimpleDateFormat을 멀티 스레드 환경에서 사용시에는 static 키워드를 사용하면 안된다고 하는건데 왜 쓰면 문제가 날지 궁금해졌습니다.

일단 SimpleDateFormat 클래에도 아래와 같은 설명이 있었습니다.

1
2
3
4
5
6
7
8
9
10
/*
* Date formats are not synchronized.
* Date 포맷은 동기화 되지 않습니다.

* It is recommended to create separate format instances for each thread.
* 각각의 스레드에서 분리된 포맷 인스턴스를 사용하길 추천합니다.

* If multiple threads access a format concurrently, it must be synchronized externally.
* 만약 멀티 스레드에서 동시에 접근하는경우 외부 동기 처리가 필요합니다.
*/

테스트

그럼 SimpleDateFormat을 멀티 스레드에서 사용했을때 어떤 문제가 발생하는지 한번 테스트를 해 보도록 하겠습니다.

SimpleDateFormat을 static으로 만들어주고

1
public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

스레드 안에서 String으로 입력된 날짜를 파싱하는 그런 형태로 테스트를 진행을 해보았는데요

1
2
String date = "20210819";
System.out.println(seq + " : " + DATE_FORMAT.parse(date));

아래와같은 형태의 NumberFormatException 이 발생을 하였고

1
2
3
4
5
1. java.lang.NumberFormatException: multiple points
2. java.lang.NumberFormatException: For input string: "E.2919E2"
3. java.lang.NumberFormatException: For input string: "E.2919"
4. java.lang.NumberFormatException: For input string: "E"
5. java.lang.NumberFormatException: For input string: ""

같은 날짜를 파싱함에도 불구하고 비정상적으로 파싱이 되는 경우도 있었습니다.

1
2
3
4
5
6
0 : Sat Aug 19 00:00:00 KST 2220
0 : Thu Aug 19 00:00:00 KST 2021
1 : Thu Nov 19 00:00:00 KST 2359
2 : Thu Nov 19 00:00:00 KST 2359
3 : Thu Nov 19 00:00:00 KST 2359
3 : Sun Nov 19 00:00:00 KST 2001

정확하게 어느포인트에서 문제가 발생하는지 알고싶었지만 stackTrace를 따라서 디버깅도 해보고 했는데 정확하게 어느포인트가 문제인지는 확인하기 어려웠습니다 ㅠㅠ

1
2
3
4
5
6
7
8
9
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)

해결책

  1. 가장 간단한건 매번 SimpleDateFormat을 만들어서 사용하는것입니다.

    1
    2
    3
    4
    public Date parseDate(String date) throws ParseException {
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    return simpleDateFormat.parse(date);
    }
  2. synchronized 사용

    1
    2
    3
    synchronized(DATE_FORMAT) {
    System.out.println(seq + " : " + DATE_FORMAT.parse(date));
    }
  3. DateTimeFormatter 사용
    DateTimeFormatter를 사용하면 LocalDate 형태로 파싱가능하다.

    1
    2
    public static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    System.out.println(seq + " : " + LocalDate.parse(date, Utils.dateTimeFormatter));

위와같은 형태로 위에서 발생한 문제들을 해결할 수 있을것으로 생각된다.

참고

공유하기