devseop08 님의 블로그

[API] java.text 패키지 형식화 클래스: DecimalFormat, DateFormat, SimpleDateFormat 본문

Language/Java

[API] java.text 패키지 형식화 클래스: DecimalFormat, DateFormat, SimpleDateFormat

devseop08 2025. 9. 17. 17:51

DecimalFormat

  • 숫자를 문자열로 형식화할 때 사용(숫자 -> 형식 문자열): format 메서드
// DecimalFormat.java

public final StringBuffer format(Object number,  
                                 StringBuffer toAppendTo,  
                                 FieldPosition pos) {  
    if (number instanceof Long || number instanceof Integer ||  
               number instanceof Short || number instanceof Byte ||  
               number instanceof AtomicInteger ||  
               number instanceof AtomicLong ||  
               (number instanceof BigInteger &&  
                ((BigInteger)number).bitLength () < 64)) {  
        return format(((Number)number).longValue(), toAppendTo, pos);  
    } else if (number instanceof BigDecimal) {  
        return format((BigDecimal)number, toAppendTo, pos);  
    } else if (number instanceof BigInteger) {  
        return format((BigInteger)number, toAppendTo, pos);  
    } else if (number instanceof Number) {  
        return format(((Number)number).doubleValue(), toAppendTo, pos);  
    } else {  
        throw 
        new IllegalArgumentException("Cannot format given Object as a Number");  
    }  
}
double number = 1234567.89;
DecimalFormat df = new DecimalFormat("#.#E0");
String result = df.format(number); // "1.2E6"
  • 형식 기호와 패턴
기호 의미 패턴 1234567.89 결과
0 10진수(자리에 값이 없으면 0으로) 0
0.0
0000000000.0000
1234568
1234567.9
0001234567.8900
# 10진수(자리에 값이 없으면 생략) #
#.#
##########.####
1234568
1234567.9
1234567.89
E 지수기호 #.#E0
0.0E0
0.000000000E0
00.00000000E0
#.#########E0
##.########E0
1.2E6
1.2E6
1.234567890E6
12.34567890E5
1.234567890E6
1.23456789E6
, 그룹(천 단위 구분) #,##0.00 1,234,567.89
. 소수점 - -
  • 자주 쓰는 패턴 스니펫
목적 패턴 예시 입력 → 출력
정수 + 천단위 #,##0 12345 → 12,345
소수점 2자리 고정 0.00 3.1 → 3.10
가변 소수(최대 3) 0.### 3.14159 → 3.142, 3 → 3
부호 표시 +0;-0 12 → +12, -12 → -12
양/음/0 개별 패턴 +#,##0; -#,##0; 0 0 → 0
퍼센트 #0.##% 0.256 → 25.6% (※ 100배)
퍼밀(‰) #0.##\u2030 0.256 → 256‰
앞자리 0패딩 000000 42 → 000042
과학표기 0.###E0 12345 → 1.235E4
통화(기호만) ¤#,##0.00 1234.5 → ₩1,234.50 (심볼은 Locale/기호 설정에 따름)
  • 지역화(국제화) & 기호 세부 제어: DecimalFormat은 내부적으로 DecimalFormatSymbols를 사용해 소수점, 그룹 구분자, 통화 기호 등을 제어한다.
var symbols = new java.text.DecimalFormatSymbols(java.util.Locale.KOREA);
symbols.setDecimalSeparator('.');
symbols.setGroupingSeparator(',');
symbols.setCurrency(java.util.Currency.getInstance("KRW")); // ₩
var df = new java.text.DecimalFormat("¤#,##0.00", symbols);
System.out.println(df.format(1234.5)); // ₩1,234.50
  • 라운딩(반올림)과 자릿수 제어: 기본 라운딩은 RoundingMode.HALF_EVEN(JDK 버전에 따라 다를 수 있음) 대신 명시 추천.
var df = (java.text.DecimalFormat) new java.text.DecimalFormat("#,##0.00");
df.setRoundingMode(java.math.RoundingMode.HALF_UP); // 일반적 반올림
df.setMinimumFractionDigits(2);
df.setMaximumFractionDigits(2);
  • 이진 부동소수(Double) 를 포맷할 때 0.1+0.2 오차가 보일 수 있으니 금융/정밀 영역은 BigDecimal 권장.
var df = new java.text.DecimalFormat("#,##0.00");
df.setRoundingMode(java.math.RoundingMode.HALF_UP);
System.out.println(df.format(new java.math.BigDecimal("2.005"))); // 2.01
  • 특정 형식의 문자열을 숫자로 변환할 때도 사용(형식 문자열 -> 숫자): parse 메서드
// DecimalFormat.java

public Number parse(String text, ParsePosition pos) {  
    // special case NaN  
    if (text.regionMatches(pos.index, symbols.getNaN(), 0, 
                    symbols.getNaN().length())) {  
        pos.index = pos.index + symbols.getNaN().length();  
        return Double.valueOf(Double.NaN);  
    }  

    boolean[] status = new boolean[STATUS_LENGTH];  
    if (!subparse(text, pos, positivePrefix, 
            negativePrefix, digitList, false, status)) {  
        return null;  
    }  

    // special case INFINITY  
    if (status[STATUS_INFINITE]) {  
        if (status[STATUS_POSITIVE] == (multiplier >= 0)) {  
            return Double.valueOf(Double.POSITIVE_INFINITY);  
        } else {  
            return Double.valueOf(Double.NEGATIVE_INFINITY);  
        }  
    }  

    if (multiplier == 0) {  
        if (digitList.isZero()) {  
            return Double.valueOf(Double.NaN);  
        } else if (status[STATUS_POSITIVE]) {  
            return Double.valueOf(Double.POSITIVE_INFINITY);  
        } else {  
            return Double.valueOf(Double.NEGATIVE_INFINITY);  
        }  
    }  

    if (isParseBigDecimal()) {  
        BigDecimal bigDecimalResult = digitList.getBigDecimal();  

        if (multiplier != 1) {  
            try {  
                bigDecimalResult = 
                bigDecimalResult.divide(getBigDecimalMultiplier());  
            }  
            catch (ArithmeticException e) {  
            // non-terminating decimal expansion  
                bigDecimalResult = 
                bigDecimalResult.divide(getBigDecimalMultiplier(), 
                roundingMode);  
            }  
        }  

        if (!status[STATUS_POSITIVE]) {  
            bigDecimalResult = bigDecimalResult.negate();  
        }  
        return bigDecimalResult;  
    } else {  
        boolean gotDouble = true;  
        boolean gotLongMinimum = false;  
        double  doubleResult = 0.0;  
        long    longResult = 0;  

        // Finally, have DigitList parse the digits into a value.  
        if (digitList.fitsIntoLong(status[STATUS_POSITIVE], 
                isParseIntegerOnly())) {  
            gotDouble = false;  
            longResult = digitList.getLong();  
            if (longResult < 0) {  // got Long.MIN_VALUE  
                gotLongMinimum = true;  
            }  
        } else {  
            doubleResult = digitList.getDouble();  
        }  

        // Divide by multiplier. We have to be careful here not to do  
        // unneeded conversions between double and long.        
        if (multiplier != 1) {  
            if (gotDouble) {  
                doubleResult /= multiplier;  
            } else {  
                // Avoid converting to double if we can  
                if (longResult % multiplier == 0) {  
                    longResult /= multiplier;  
                } else {  
                    doubleResult = ((double)longResult) / multiplier;  
                    gotDouble = true;  
                }  
            }  
        }  

        if (!status[STATUS_POSITIVE] && !gotLongMinimum) {  
            doubleResult = -doubleResult;  
            longResult = -longResult;  
        }  

        // At this point, if we divided the result by the multiplier, the  
        // result may fit into a long.  We check for this case and return        
        // a long if possible.        
        // We must do this AFTER applying the negative (if appropriate)        
        // in order to handle the case of LONG_MIN; otherwise, if we do        
        // this with a positive value -LONG_MIN, the double is > 0, but        
        // the long is < 0. We also must retain a double in the case of        
        // -0.0, which will compare as == to a long 0 cast to a double        
        // (bug 4162852).        
        if (multiplier != 1 && gotDouble) {  
            longResult = (long)doubleResult;  
            gotDouble = ((doubleResult != (double)longResult) ||  
                        (doubleResult == 0.0 && 1/doubleResult < 0.0)) &&  
                        !isParseIntegerOnly();  
        }  

        // cast inside of ?: because of binary numeric promotion, JLS 15.25  
        return gotDouble ? (Number)doubleResult : (Number)longResult;  
    }  
}
DecimalFormat df = new DecimalFormat("#,###.##");
Number num = df.parse("1,234,567.89");
double d = num.doubleValue(); //1234567.89
  • 형식 문자열 파싱 시 주의점
    • Integer.parseInt()는 콤마가 포함된 문자열을 숫자로 변환하지 못한다.
    • 정밀도가 필요하다면 DecimalFormatsetParseBigDecimal(true)
var df = new java.text.DecimalFormat("#,##0.##");
df.setParseBigDecimal(true); // BigDecimal로 파싱
var num = df.parse("1,234.50"); // → BigDecimal("1234.50")
  • 스레드 안전성 & 성능: DecimalFormat은 스레드 안전하지 않다. 멀티 스레드 환경에선
    • 스레드마다 인스턴스 보유 또는
    • ThreadLocal<DecimalFormat> 사용, 또는
    • 매 호출마다 새로 생성 -> 가볍진 않으므로 재사용 권장
private static final ThreadLocal<java.text.DecimalFormat> TL =
    ThreadLocal.withInitial(() -> {
        var df = new java.text.DecimalFormat("#,##0.00");
        df.setRoundingMode(java.math.RoundingMode.HALF_UP);
        return df;
    });

// 사용
String s = TL.get().format(1234.5);

DateFormat

  • DateFormat날짜/시간 ↔ 문자열 간 포맷팅·파싱을 담당하는 추상 클래스(java.text.DateFormat) => 인스턴스 생성은 불가하지만 정적 메서드 호출 가능
  • 로케일(Locale), 스타일(FULL/LONG/MEDIUM/SHORT), 타임존(TimeZone), lenient(관대한) 파싱이 핵심
  • 커스텀 패턴은 하위 클래스인 SimpleDateFormat이 담당
  • 현대적 대안은 java.time.format.DateTimeFormatter
import java.text.DateFormat;
import java.util.*;

Date now = new Date();

// 로케일/스타일 기반
DateFormat df = DateFormat.getDateTimeInstance(
        DateFormat.LONG, DateFormat.MEDIUM, Locale.KOREA);
df.setTimeZone(TimeZone.getTimeZone("Asia/Seoul"));

String s = df.format(now);     // 예: 2025년 9월 17일 오후 4:12:34
Date parsed = df.parse(s);     // 문자열 → Date
  • 정적 메서드 getDateInstance / getTimeInstance / getDateTimeInstance스타일 기반 포맷터를 만든다.

  • 타임존을 명시하지 않으면 JVM 기본 타임존을 사용

  • 스타일 옵션과 용도

메서드 의미 비고
getDateInstance(style, locale) 날짜만 포맷 예: 2025년 9월 17일
getTimeInstance(style, locale) 시간만 포맷 예: 오후 4:12:34
getDateTimeInstance(dateStyle, timeStyle, locale) 날짜+시간 가장 자주 사용
getInstance() 로케일 표준의 “짧은” 날짜시간 SHORT, SHORT에 가까움(구현 의존)
* 스타일 값: FULL, LONG, MEDIUM, SHORT (로케일마다 실제 출력 형식은 다름)
  • 로케일과 타임존
    • 로케일은 요일/월 이름/오전·오후 등 언어 규칙을, 타임존은 시각 자체(UTC 오프셋)를 결정
    • 서버/컨테이너 환경에서는 항상 타임존을 명시하는 습관이 안전
DateFormat df = DateFormat.getDateTimeInstance(
        DateFormat.FULL, DateFormat.FULL, Locale.KOREA);

// 시스템 기본이 아닌 서울 타임존으로 고정
df.setTimeZone(TimeZone.getTimeZone("Asia/Seoul"));
System.out.println(df.format(new Date()));
  • 파싱과 Lenient(관대) 모드: 기본은 lenient = true(관대 파싱) → 유효하지 않은 날짜도 보정
    • 관대 모드(true): "2025.02.30" → 자동 보정(3월 2일 등)될 수 있어 검증 용도에는 부적절.
    • 입력 검증이나 정확한 파싱이 필요하면 setLenient(false) 필수.
DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.KOREA);
df.setLenient(false); // 엄격 모드
df.setTimeZone(TimeZone.getTimeZone("Asia/Seoul"));

try {
    df.parse("2025.02.30"); // 엄격 모드에선 ParseException
} catch (java.text.ParseException e) {
    // 유효성 오류 처리
}
  • 스레드 안전성: DateFormat (그리고 SimpleDateFormat)은 스레드 안전하지 않다.
    • 멀티스레드에서 공유 금지.
    • 방법:
        1. 매 호출 시 새 인스턴스 생성(간단하지만 상대적으로 비용↑)
        1. ThreadLocal<DateFormat>로 스레드별 보유
private static final ThreadLocal<DateFormat> FULL_KR_SEOUL =
    ThreadLocal.withInitial(() -> {
        DateFormat df = DateFormat.getDateTimeInstance(
                DateFormat.FULL, DateFormat.FULL, Locale.KOREA);
        df.setTimeZone(TimeZone.getTimeZone("Asia/Seoul"));
        df.setLenient(false);
        return df;
    });

// 사용
String out = FULL_KR_SEOUL.get().format(new Date());
  • 커스텀 패턴이 필요할 땐: SimpleDateFormat
    • DateFormat 자체는 패턴 문자열을 받지 않는다.
    • 패턴 제어(예: yyyy-MM-dd HH:mm:ss)는 SimpleDateFormat 사용
import java.text.SimpleDateFormat;

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.KOREA);
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Seoul"));
sdf.setLenient(false);

String s = sdf.format(new Date());          // 2025-09-17 16:12:34
Date d = sdf.parse("2025-09-17 16:12:34");
  • 현대적 대안: java.time (DateTimeFormatter)
    • 장점: 불변/스레드 안전, 엄격 파싱(ResolverStyle), 타입 분리(LocalDate/LocalTime/LocalDateTime/ZonedDateTime)
import java.time.*;
import java.time.format.*;

DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
        .withLocale(Locale.KOREA)
        .withZone(ZoneId.of("Asia/Seoul"));

String s = f.format(Instant.now());        // 포맷(Instant → String)
Instant t = Instant.from(f.parse(s));      // 파싱(String → Instant)

스타일 기반

DateTimeFormatter f2 =
    DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.MEDIUM)
                     .withLocale(Locale.KOREA)
                     .withZone(ZoneId.of("Asia/Seoul"));

SimpleDateFormat: 날짜와 시간을 다양한 형식으로 출력할 수 있게 해준다.

  • 날짜와 시간을 다양한 형식 문자열로 변환: format 메서드
Date today = new Date();
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
String result = df.format(today);
  • 다양한 형식 문자열을 날짜와 시간(Date 객체)으로 변환: parse 메서드
DateFormat df = new SimpleDateFormat("yyyy년 MM월 dd일");
DateFormat df2 = new SimpleDateFormat("yyyy/MM/dd");
Date d = df.parse("2015년 11월 23일"); // 문자열을 Date로 변환
String result = df.format(d);
  • 형식 기호
기호 의미
y 연도(캘린더 연) yyyy2025
M 월(숫자/이름) MM09, MMMSep, MMMMSeptember
d 일(월 내) dd07
E 요일 EEEWed, EEEEWednesday
H 시(0–23) HH16
h 시(1–12) hh04 + a(오전/오후)와 사용
m mm05
s ss09
S 밀리초(0–999) SSS123
a 오전/오후 a오후
z 타임존 이름 zKST, GMT+9
Z RFC 822 오프셋 +0900
X ISO-8601 오프셋 X+09, XX+0900, XXX+09:00
' 리터럴 yyyy-MM-dd'T'HH:mm:ss (작은따옴표로 감싸 리터럴 T 출력)
D 연중 일(1–366) D251
w/W 주(연중/월내) 주차 계산 시 로케일 규칙 영향
k/K 시(1–24 / 0–11) 특수 케이스에만
  • SimpleDateFormat은 패턴 문자열로 Date <->String을 다루고 로케일/타임존/관대(lanient) 파싱을 제어한다.
import.util.*;
import.text.*;

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.KOREA);
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Seoul"));
sdf.setLanient(false);

String out = sdf.format(new Date()); // 포맷
Date parsed = sdf.parse("2025-09-17 16:30:00"); // 파싱
  • SimpleDateFormat 사용 시 주의 사항
  1. YYYY vs yyyy
    • YYYYweek-year(ISO 주차 기반 연도), 연말/연초에 실제 연도와 달라질 수 있다.
    • 일반적인 연도는 yyyy를 사용
new SimpleDateFormat("YYYY-MM-dd").format(new Date()); 
// 특정 날엔 "2024-12-31"이 아닌 "2025-12-31" 같은 오류 가능
  1. mm(분) vs MM(월): 월 포맷에 실수로 mm 쓰면 “09월”이 아니라 “분”이 들어간다.

  2. 관대 파싱 기본값(true)

    • "2025-02-30" 같은 입력도 보정되어 통과할 수 있음 → setLenient(false)로 엄격 파싱.
  3. 로케일/언어

    • 영문 월/요일이 필요하면 Locale.ENGLISH로케일 명시.
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
  1. 타임존 고정

    • 서버 기본 TimeZone 의존 금지 -> setTimeZone(ZoneId/ID)로 명시.
  2. 스레드 안전 아님

    • 공유 금지. 매 호출 생성 or ThreadLocal<SimpleDateFormat> 사용.
private static final ThreadLocal<SimpleDateFormat> TL =
  ThreadLocal.withInitial(() -> {
    var f = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.KOREA);
    f.setTimeZone(TimeZone.getTimeZone("Asia/Seoul"));
    f.setLenient(false);
    return f;
  });

'Language > Java' 카테고리의 다른 글

[OOP] 제네릭스  (0) 2025.09.20
[OOP] 열거형  (0) 2025.09.18
[API] System, Math, Number, Wrapper, BigInteger, BigDecimal  (0) 2025.09.16
[API] StringBuffer, StringBuilder, StringJoiner  (0) 2025.09.15
[API] String  (0) 2025.09.06