devseop08 님의 블로그
[API] java.text 패키지 형식화 클래스: DecimalFormat, DateFormat, SimpleDateFormat 본문
Language/Java
[API] java.text 패키지 형식화 클래스: DecimalFormat, DateFormat, SimpleDateFormat
devseop08 2025. 9. 17. 17:51DecimalFormat
- 숫자를 문자열로 형식화할 때 사용(숫자 -> 형식 문자열):
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()는 콤마가 포함된 문자열을 숫자로 변환하지 못한다.- 정밀도가 필요하다면
DecimalFormat의setParseBigDecimal(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)필수.
- 관대 모드(true):
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)은 스레드 안전하지 않다.- 멀티스레드에서 공유 금지.
- 방법:
- 매 호출 시 새 인스턴스 생성(간단하지만 상대적으로 비용↑)
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());- 커스텀 패턴이 필요할 땐:
SimpleDateFormatDateFormat자체는 패턴 문자열을 받지 않는다.- 패턴 제어(예:
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 |
연도(캘린더 연) | yyyy → 2025 |
M |
월(숫자/이름) | MM→09, MMM→Sep, MMMM→September |
d |
일(월 내) | dd→07 |
E |
요일 | EEE→Wed, EEEE→Wednesday |
H |
시(0–23) | HH→16 |
h |
시(1–12) | hh→04 + a(오전/오후)와 사용 |
m |
분 | mm→05 |
s |
초 | ss→09 |
S |
밀리초(0–999) | SSS→123 |
a |
오전/오후 | a→오후 |
z |
타임존 이름 | z→KST, 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) | D→251 |
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사용 시 주의 사항
YYYYvsyyyyYYYY는 week-year(ISO 주차 기반 연도), 연말/연초에 실제 연도와 달라질 수 있다.- 일반적인 연도는
yyyy를 사용
new SimpleDateFormat("YYYY-MM-dd").format(new Date());
// 특정 날엔 "2024-12-31"이 아닌 "2025-12-31" 같은 오류 가능mm(분) vsMM(월): 월 포맷에 실수로mm쓰면 “09월”이 아니라 “분”이 들어간다.관대 파싱 기본값(true)
"2025-02-30"같은 입력도 보정되어 통과할 수 있음 →setLenient(false)로 엄격 파싱.
로케일/언어
- 영문 월/요일이 필요하면
Locale.ENGLISH등 로케일 명시.
- 영문 월/요일이 필요하면
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH);타임존 고정
- 서버 기본 TimeZone 의존 금지 ->
setTimeZone(ZoneId/ID)로 명시.
- 서버 기본 TimeZone 의존 금지 ->
스레드 안전 아님
- 공유 금지. 매 호출 생성 or
ThreadLocal<SimpleDateFormat>사용.
- 공유 금지. 매 호출 생성 or
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 |