Formatter(1)

de_sj_awa·2021년 5월 6일
1

1. Formatting이란?

기본적으로 자바에서는 숫자를 표시할 때 다음과 같이 제공한다.

123456789

그런데, 돈 계산을 많이 하는 사람이나 미국식 사고 방식을 갖고 있는 사람들은 다음과 같은 숫자 표시를 더 선호한다.

123,456,789

이렇게 콤마를 긴 숫자 사이에 적어두면 가독성이 높아져 숫자의 단위를 보다 빨리 이해할 수 있다. 영어로 100만을 뜻하는 million은 1,000,000이고, 10억을 뜻하는 billion은 1,000,000,000이듯이 숫자를 빨리 읽을 수 있도록 콤마를 찍어주는 것은 매우 중요하다.

웹 화면에 어떤 데이터를 보여줄 때 이렇게 숫자나 문자를 formatting 하는 작업은 꼭 자바에서 할 필요는 없다. 왜냐하면 웹에서 UI를 처리하기 위한 자바 스크립트에서 처리해도 되기 때문이다. 하지만, UI에서 이러한 데이터를 처리하면 사용자에게 전달되어야 하는 데이터의 양도 많아지고, 사용자 PC의 성능에 따라 영향이 있으므로 되도록이면 서버단에서 하는 것이 좋다.

그렇다면 자바에서 제공하는 Formatting 기능에는 어떤 것들이 있을까? 목록으로 나열해보면 다음과 같다.

  • 숫자 및 통화
  • 날짜와 시간
  • 문자열

숫자는 위와 같은 예에 사용하고, 통화 양식도 지정할 수 있다. 또한 어떤 문장을 조합하는 문자열을 만들 때에는 +를 사용하여 데이터를 제공하는 것보다, Formatting 된 문자열로 제공하는 것이 소스 코드의 가독성 측면에서도 좋다.

2. 숫자와 통화를 처리하기 위한 NumberFormat 클래스

숫자 및 통화를 쉽게 표현하는 NumberFormat 클래스에 대해서 알아보자. 이 클래스는 abstract 클래스로 선언되어 있다. 그러므로, 이 클래스를 확장하여 사용할 수도 있다. 그리고, 이 클래스의 생성자는 protected로 확장한 클래스와 클래스 내부에 있는 메소드에서만 사용할 수록 되어 있기 때문에, 이 클래스의 객체를 생성하려면 다음과 같이 static으로 선언되어 있는 getInstance() 메소드를 사용해야 한다.

NumberFormat formatter = NumberFormat.getInstance();

이처럼, NumberFormat의 객체를 생성할 때 사용하는 static 메소드들은 다음과 같다.

메소드 이름 용도
getInstance() 현재 JVM의 기본 지역(Locale)으로 일반적인 목적의 숫자 format 제공
getInstance(Locale inLocale) 매개 변수로 제공된 지역으로 숫자 format 제공
getCurrencyInstance() 현재 JVM의 기본 지역(Locale)으로 통화 format 제공
getCurrencyInstance(Locale inLocale) 매개 변수로 제공된 지역으로 통화 format 제공
getIntegerInstance() 현재 JVM의 기본 지역(Locale)으로 정수 format 제공
getIntegerInstance(Locale inLocale) 매개 변수로 제공된 지역으로 정수 format 제공

이렇게 여러 종류의 getInstance() 메소드를 사용하여 객체를 생성한 후에는 숫자를 매개 변수로 한 format() 메소드만 호출하면 된다. 간단하게 두 줄만 나타내면 다음과 같다.

NumberFormat formatter = NumberFormat.getInstance();
String str = formatter.format(3.1415927);

이렇게 하면 str은 현재 사용하는 JVM의 기본 지역 정보를 참조하여 결과를 출력한다. 추가로, 기본 소수점 출력 길이는 3이기 때문에 str 값을 출력해보면 소수점 3자리까지만 출력한다. 그리고, 소수점 4번째 자리에서 반올림하여 값을 처리하며, 그 값은 3.141이 아닌 3.142가 된다.

다음과 같은 예제를 보자.

package d.format;

import java.text.NumberFormat;
import java.util.Locale;

public class NumberFormatSample {
    public static void main(String[] args){
        NumberFormatSample sample = new NumberFormatSample();
        sample.checkNumberFormat();
    }
}

main() 메소드에서 사용하는 checkNumberFormat()은 뒤에서 살펴보고, 먼저 이 클래스에 다음과 같은 상수들과 공통적으로 사용하는 메소드를 선언하자.

package d.format;

import java.text.NumberFormat;
import java.util.Locale;

public class NumberFormatSample {

    public static void main(String[] args){
        NumberFormatSample sample = new NumberFormatSample();
        sample.checkNumberFormat();
    }
    private final int DECIMAL = 1;
    private final int ONLY_INTEGER = 2;
    private final int CURRENCY = 3;
    private final int PERCENTAGE = 4;

    public void printFormattedNumber(double number, Locale locale, int type){
        NumberFormat formatter = null;
        switch (type){
            case DECIMAL:
                formatter=NumberFormat.getInstance(locale);
                //formatter.setMinimumFractionDigits(10);
                //formatter.setMinimumIntegerDigits(10);
                break;
            case ONLY_INTEGER:
                formatter=NumberFormat.getIntegerInstance(locale);
                break;
            case CURRENCY:
                formatter=NumberFormat.getCurrencyInstance(locale);
                break;
            case PERCENTAGE:
                formatter=NumberFormat.getPercentInstance(locale);
                break;
            default:
                return;
        }
        System.out.println(locale.getCountry()+":"+formatter.format(number));
    }
}

DECIMAL, ONLY_INTEGER, CURRENCY, PERCENTAGE 이렇게 4개의 상수를 클래스에 선언했다. 이렇게 4가지로 상수를 선언해 놓은 이유는 printFormattedNumber() 메소드를 보면 이해할 수 있을 것이다. 이 메소드에는 매개 변수로 변환하고자 하는 숫자, Locale 객체, 변환 타입을 받아 그에 해당하는 NumberFormat 객체를 생성한 후 format()를 통하여 값을 변환하여 출력한다.

NumberFormat 클래스의 네 가지 객체 생성 방식을 다르게 하여 그 결과를 비교하기 위해서는 이렇게 코드를 작성하는 것이 간결하다. 그렇지 않으면, 별도의 메소드를 만들어 상황에 따른 결과를 확인해 봐야만 한다. 그러면 코드의 양도 늘어나게 된다. 따라서 NumberFormat 객체를 생성하고 format() 메소드 호출 결과를 출력해주는 메소드를 별도로 만듦으로써 보다 간단하게 결과를 확인할 수 있다.

이제 main() 메소드에서 호출하는 checkNumberFormat() 메소드를 보자.

 public void checkNumberFormat(){
        double number = 3.1415927;
        //double number = 86400.1234;
        int type = DECIMAL;
        //int type = PERCENTAGE;
        //int type = ONLY_INTEGER;
        //int type = CURRENCY;
        printFormattedNumber(number, Locale.KOREA, type);
        printFormattedNumber(number, Locale.US, type);
        printFormattedNumber(number, Locale.FRANCE, type);
        printFormattedNumber(number, Locale.GERMANY, type);
    }

checkNumberFormat()을 살펴보면 파이(3.1315927) 값을 나타내는 double이 선언되어 있다. DECIMAL 값을 출력하도록 하는 type 변수의 값을 지정한 후, 한국, 미국, 프랑스, 독일의 지역 정보를 넘겨주도록 했다. Locale 클래스에는 이 외에도 이미 선언해 놓은 국가들의 코드들이 존재한다. 이처럼 Locale.KOREA라고 지정해주면, 해당 상수의 타입도 Locale이기 때문에 별도의 Locale 객체를 생성할 필요가 없이 한국의 Locale 객체를 얻을 수 있다.

결과는 다음과 같다.

KR:3.142
US:3.142
FR:3,142
DE:3,142

우리나라(한국)와 미국은 소수점을 점(.)으로 구분하는 데 비해 프랑스와 독일은 콤마(,)로 구분하는 것을 확인할 수 있다.

이번에는 PERCENTAGE와 ONLY_INTEGER로 type을 변경해보자. 이 때의 결과는 다음과 같다.

KR:314%
US:314%
FR:314 %
DE:314 %

KR:3
US:3
FR:3
DE:3

PERCENTAGE는 현재 값에 100을 곱한 것과 같기 때문에 소수로 되어 있는 값을 반환할 때 용이하다. 그리고, ONLY_INTEGER는 말 그대로 정수만을 출력한다.
1000 단위가 넘어갈 때는 어떤지 다음과 같이 checkNumber Format() 메소드의 값을 다음과 같이 변경하고, type 값을 다시 DECIMAL로 바꾼 후 결과를 확인해보자.

//double number = 3.1415927;
double number = 86400.1234;

결과는 다음과 같다.

KR:86,400.123
US:86,400.123
FR:86400,123
DE:86.400,123

우리나라와 미국은 동일하지만, 프랑스와 독일의 경우에는 숫자 표현이 아주 다른 것을 볼 수 있다.

이번에는 포맷 소수 타입에서 통화 타입으로 변경하여 확인해보자.

//int type = DECIMAL;
int type = CURRENCY;

결과는 다음과 같이 출력된다.

KR:86,400
US:$86,400.12
FR:86400,12 €
DE:86.400,12

각 나라마다 사용하는 통화의 형태로 출력되는 것을 볼 수 있다. 특히 우리나라는 소수점 이하의 돈은 돈으로 별로 치지 않기 때문에 다른 나라와 다르게 소수점 금액이 반환되지 않는다. 그런데, 여기서 "DECIMAL로 출력할 때 소수점을 왜 3자리까지만 출력하지? 더 자세히 출력할 수는 없자?"라고 의문을 가질 수 있다. 물론 더 자세히 출력하는 방법이 있다.

NumberFormat 클래스를 살펴보면 다음의 메소드들이 존재한다.

리턴타입 메소드 설명
void setMaximumFractionDigits(int newValue) 소수점 이하의 최대 표시 개수 지정
void setMaximumIntegerDigits(int newValue) 정수형의 최대 표시 개수 지정
void setMinimumFractionDigits 소수점 이하의 최소 표시 개수 지정
void setMinimumIntegerDigits(int newValue) 정수형의 최소 표시 개수 지정

메소드 설명을 보면 알 수 있듯이 이 메소드들을 사용하면 정수 부분과 소수 부분의 최소 및 최대 표시 개수를 확인할 수 있다.

public void printFormattedNumber(double number, Locale locale, int type){
        NumberFormat formatter = null;
        switch (type){
            case DECIMAL:
                formatter=NumberFormat.getInstance(locale);
                formatter.setMinimumFractionDigits(10);
                formatter.setMinimumIntegerDigits(10);
                break;

getFormattedNumber() 메소드의 case DECIMAL 부분에 굵은 글씨로 표시한 두 줄을 추가하자. 그 다음에는 checkNumberFormat() 메소드에서 type 값을 DECIMAL로 다시 바꾸어 실행해보자. number 값이 86400.1234인 경우에 다음과 같은 결과가 출력된다.

KR:0,000,086,400.1234000000
US:0,000,086,400.1234000000
FR:0000086400,1234000000
DE:0.000.086.400,1234000000

보는 것과 같이 최소 정수 10자리, 소수 10자리를 표시하라고 지정해 놓았기 때문에 10자리의 정수와 소수가 출력된 것을 볼 수 있다.

3. DecimalFormat 클래스

NumberFormat 클래스는 어떻게 보면 매우 간단하게 숫자를 표시하는 데 사용된다. 보다 세밀한 숫자 표현이 필요하다면, DecimalFormat이라는 클래스를 사용하면 된다. 이 DecimalFormat 클래스는 NumberFormat 클래스를 확장한 것이기 때문에 NumberFormat에서 제공하는 모든 메소드를 사용할 수 있다.

DecimalFormat을 사용하여 숫자를 표시할 때에는 다음과 같이 미리 정해져 있는 "심볼 symbol"을 사용하며, 이 값들 중 기본적인 몇 가지는 외워 두면 좋다.

심볼 위치 지역화여부 의미
0 숫자 O 숫자
# 숫자 O 숫자, 숫자가 있을 경우에만 출력
. 숫자 O 소수점 구분자, 혹은 monetary 소수점 구분자
- 숫자 O 음수 표시
, 숫자 O 그룹 구분자
E 숫자 O mantissa와 exponent 표시
; 하위 패턴 경계 O 양수와 음수를 표시하기 윈한 하위 패턴 경계
% 앞이나 뒤 O 100배를 하고 퍼센트로 표시
\u2030 앞이나 뒤 O 1,000배를 하고 밀리 단위 표시
¤ (\u00A4) 앞이나 뒤 X 통화 표시이며 현재 통화로 표시
' 앞이나 뒤 X #와 같이 특수 표시를 나타내기 위한 기호

예제를 통해 이 심볼들을 어떻게 사용하는지 알아보자. 다음과 같이 DecimalFormatSample 클래스를 만들자.

package d.format;

import java.text.DecimalFormat;

public class DecimalFormatSample {

    public static void main(String[] args){
        DecimalFormatSample sample = new DecimalFormatSample();
        sample.checkDecimalFormat();
    }
    public void checkDecimalFormat(){
        double number = 123.456;
        String pattern = "0,000.00";
        printFormattedNumber(number, pattern);

        number = 1234567.890123;
        printFormattedNumber(number, pattern);

        number = 1;
        printFormattedNumber(number, pattern);

        number = 123.456;
        pattern="#,###.##";
        printFormattedNumber(number, pattern);

        number = 1234567.890123;
        printFormattedNumber(number, pattern);
    }
    public void printFormattedNumber(double number, String pattern){
        DecimalFormat format = new DecimalFormat(pattern);
        String result = format.format(number);
        System.out.println("pattern:"+pattern+" number:"+number+" result:"+result);
    }
}

먼저 printFormattedNumber() 메소드를 살펴보자. 이 메소드로 숫자와 패턴이 매개 변수로 넘어온다. 그러면 DecimalFormat 객체가 패턴을 사용하여 생성되고, 숫자가 패턴에 따라서 변환 된다. 그리고 마지막 줄에는 그 패턴을 출력한다.

checkDecimalFormat() 메소드에서는 "0,000.00"와 "#,###.##"의 단 두개의 패턴이 존재하고, 값을 계속 바꾸어 가면서 결과를 확인하고 있다. 이 클래스를 컴파일하고 실행해보자.

결과는 다음과 같다.

pattern:0,000.00 number:123.456 result:0,123.46
pattern:0,000.00 number:1234567.890123 result:1,234,567.89
pattern:0,000.00 number:1.0 result:0,001.00
pattern:#,###.## number:123.456 result:123.46
pattern:#,###.## number:1234567.890123 result:1,234,567.89

결과를 보면 알겠지만, 0으로 지정된 부분은 그 위치에 숫자가 없더라도 0으로 채우고 있다. 반면, #으로 지정된 부분은 그 위치에 숫자가 없으면 0으로 채우지 않는다.

그래서, 보통은 일반적으로 0과 #을 섞어 놓은 다음과 같은 패턴을 사용한다.

"#,##0.0#"

여기서 한 가지 짚고 넘어가야 할 것이 있다. 숫자 format에서 콤마(,)의 명칭은 그룹 구분자다. 다시 말해서 여기서는 3번째 자리에 콤마를 적어두었기 때문에 3자리 단위로 콤마가 찍힌다. 즉, 6번째, 9번째 자리에도 콤마가 자동으로 표시된다는 말이다. 숫자와 패턴을 바꾸면서 확인해보면 각 패턴의 특성을 쉽게 이해할 수 있을 것이다.

DecimalPattern은 특이하게도 하위 패턴을 지정할 수 있다. 하위 패턴은 세미콜론(;)으로 구분되며, 앞에는 양수일 때, 뒤에는 음수일 때의 표시 패턴을 지정한다. 보통은 음수일 때는 - 기호를 사용하여 표시하지만, 만약 괄호로 표시하고 싶다면 다음과 같이 지정하면 된다.

"#,##0.00;(#,##0.00)"

이렇게 지정하면 양수일 경우 앞의 패턴을, 음수일 경우에는 뒤에 있는 패턴을 따르게된다.

예제를 통해서 살펴보자.

    public void checkSubPattern(){
        double number = 1234.5678;
        String pattern = "#,##0.00;(#,##0.00)";
        printFormattedNumber(number, pattern);

        number = -number;
        printFormattedNumber(number, pattern);
    }

결과는 다음과 같다.

pattern:#,##0.00;(#,##0.00) number:1234.5678 result:1,234.57
pattern:#,##0.00;(#,##0.00) number:-1234.5678 result:(1,234.57)

예상한 대로 음수일 때에는 괄호로 묶여 있는 것을 볼 수 있다. 이 외에 DecimalFormat을 보다 복잡하고 세밀하게 사용할 수 있는 DecimalFormatSymbols 클래스가 있다.

4. 날짜와 시간을 간단히 표현하려면 DateFormat

날짜와 시간을 표현하는 방법을 알아보자. 숫자의 NumberFormat처럼 날짜와 시간을 표현할 때에는 DateFormat을 사용하면 간단하게 처리할 수 있다. DateFormat 클래스도 abstract로 선언되어 있는 클래스이며, 객체를 생성하기 위해서는 생성자가 아닌 getInstance() 메소드로 객체를 생성해야 한다. 제공되는 메소드는 다음과 같다.

메소드 이름 용도
getInstance() 날짜와 시간을 현재 JVM의 기본 지역(Locale) 및 짧은 형식(SHORT)으로 생성하는 format 제공
getDateInstance() 날짜를 기본 지역(Locale) 및 기본 형식(DEFAULT)로 생성하는 format 제공
getDateInstance(int style) 날짜를 기본 지역(Locale) 및 style에 지정된 형식으로 생성하는 format 제공
getDateInstance(int style, Locale aLocale) 날짜를 aLocale에 지정된 지역 및 style에 지정된 형식으로 format 제공
getTimeInstance() 시간을 기본 지역(Locale) 및 기본 형식(DEFAULT)으로 생성하는 format 제공
getTimeInstance(int style) 시간을 기본 지역(Locale) 및 style에 지정된 형식으로 format 제공
getTimeInstance(int style, Locale aLocale) 시간을 aLocale에 지정된 지역 및 style에 지정된 형식으로 format 제공
getDateTimeInstance() 날짜와 시간을 기본 지역(Locale) 및 기본 형식(DEFAULT)으로 생성하는 format 제공
getDateTimeInstance(int dateStyle, int timeStyle) 기본 지역(Locale)에 날짜는 dateStyle에, 시간을 timeStyle에 지정된 형식으로 생성하는 format 제공
getDateTimeInstance(int dateStyle, int timeStyle, Locale aLocale) aLocale에 지정된 지역, 날짜는 dateStyle에, 시간은 timeStyle에 지정된 형식으로 생성하는 format 제공

표에서 보는 것과 같이 클래스 이름은 DateFormat이지만, 날짜와 시간을 처리하는 객체를 생성하도록 되어 잇는 것을 볼 수 있다. 간단하게 이 객체를 생성하려면, 다음과 같이 getInstance() 메소드를 사용하면 된다.

Date date = new Date();
DateFormat formatter = DateFormat.getInstance();
String str = formatter.format(date);

여기서 Date 클래스는 java.util 패키지에 선언된 클래스를 말한다.

추가로 이 표에서 SHORT와 DEFAULT로 명시한 스타일이 있다. DateFormat 클래스에서 제공하는 기본 타입은 다음과 같다.

  • DEFAULT
  • FULL
  • LONG
  • MEDIUM
  • SHORT

그러면 각 객체 생성 메소드 및 스타일을 적용하여 어떻게 차이가 발생하는지 예제를 통해서 알아보자.

package d.format;

import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;

public class DateFormatSample {
    public static void main(String[] args){
        DateFormatSample sample = new DateFormatSample();
        sample.checkDateFormat();
    }
    private final static int DATE = 1;
    private final static int TIME = 2;
    private final static int DATE_TIME = 3;
    public void checkDateFormat(){
        Date currentDate = new Date(1328054400000L);
        int style = DateFormat.DEFAULT;
        //int style = DateFormat.FULL;
        //int style = DateFormat.LONG;
        //int style = DateFormat.MEDIUM:
        //int style = DateFormat.SHORT;
        int formatType = DATE;
        //int formatType = TIME;
        //int formatType = DATE_TIME;
        printFormattedDateTime(currentDate, Locale.KOREA, style, formatType);
        printFormattedDateTime(currentDate, Locale.US, style, formatType);
        printFormattedDateTime(currentDate, Locale.FRANCE, style, formatType);
        printFormattedDateTime(currentDate, Locale.GERMANY, style, formatType);

    }
    public void printFormattedDateTime(Date date, Locale locale, int style, int formatType){
        DateFormat formatter;
        switch(formatType){
            case DATE:
                formatter = DateFormat.getDateInstance(style, locale);
                break;
            case TIME:
                formatter = DateFormat.getTimeInstance(style, locale);
                break;
            case DATE_TIME:
                formatter = DateFormat.getDateTimeInstance(style, style, locale);
                break;
            default:
                return;
        }
        String result = formatter.format(date);
        System.out.println(locale.getCountry()+":"+result);
    }
}

예제 클래스를 보면 위의 NumberFormatSample 클래스와 큰 차이가 없다. 유심해서 봐야 할 것은 printFormattedDateTime() 메소드의 switch문에서 formatType에 따라서 DateFormat 객체 생성 메소드가 다르다는 정도이다. 이처럼, DateFormat 클래스는 getDate로 시작하면 날짜를, getTime으로 시작하면 시간을, getDateTime으로 시작하면 날짜와 시간을 변환하는 객체가 각각 생성된다.

추가로 checkDateFormat() 메소드의 첫 줄에 선언한 Date 생성자에 13280544 000001이라는 값이 있는 것을 볼 수 있다. 이 값은 2012년 2월 1일 09시에 대한 long 타입 값이다. 만약 현재 시간을 나타내려면 이 long 값을 지워주면 된다.

먼저 지금 상태(style은 "DateFormat.DEFAULT"로, formatType은 "DATE"로 지정한)의 결과를 살펴보자.

출력 결과는 다음과 같다.

KR:2012. 2. 1.
US:Feb 1, 2012
FR:1 févr. 2012
DE:01.02.2012

이번에는 시간을 살펴보자. formatType을 "TIME"으로 변경하고, style은 "DEFAULT"로 지정한 후 결과를 살펴보자.

출력 결과는 다음과 같다.

KR:오전 9:00:00
US:9:00:00 AM
FR:09:00:00
DE:09:00:00

마지막으로 날짜와 시간을 같이 표현하도록 formatType을 "DATE_TIME"으로, "DEFAULT" 스타일로 변경한 후 결과를 살펴보자.

출력 결과는 다음과 같다.

KR:2012. 2. 1. 오전 9:00:00
US:Feb 1, 2012, 9:00:00 AM
FR:1 févr. 2012, 09:00:00
DE:01.02.2012, 09:00:00

이처럼 printFormattedDateTime() 메소드에서 getDateTimeInstance() 메소드로 DateFormat 객체를 생성할 때에는 날짜의 스타일과 시간의 스타일을 각각 별도로 지정할 수도 있다. 그러므로, 필요에 따라서 선택하여 사용하면 된다.
추가로, DateFormat 클래스에서는 문자열로 되어 있는 시간 값을 Date 타입으로 변환해 주는 parse() 메소드도 제공해주고, 그 외에도 날짜와 시간을 처리하기 위한 많은 메소드들을 제공하고 있다.

참고

  • 자바의 신
profile
이것저것 관심많은 개발자.

0개의 댓글