Java에서 문자열을 일정한 간격으로 나열하여 출력하려면, printf()의 첫 인자에 들어가는 문자열의 타입을 나타내는 %문구 앞뒤로 스페이스를 넣어주는 방법이 있다.
하지만 값의 길이가 정형화되지 않은 경우(위의 예시에서는 이름), 수고스럽게 공백을 추가한 보람도 없이 출력되는 값들의 열이 살짝 깨져서 나오게 된다. 문자열 외에도, 평균 점수를 출력하는 경우도 소숫점 자리수와 정수 자릿수가 정렬되지 않는 경우 출력값을 읽는데 약간 성가신 상황이 일어난다.
Java는 java.text 패키지에 문자로 출력되는 값들의 포맷을 지정해주는 몇 가지 클래스들을 가지고 있다. 문자열과 숫자는 물론, 날짜도 java.text 내 존재하는 패키지를 사용하여 문자열로 깔끔하게 변환 출력할 수 있다.
1. NumberFormat, DecimalFormat 클래스
Numberformat과 DecimalFormat은 숫자를 문자열로 정렬 출력하는데 사용하는 클래스다. 이 중 Number 클래스는 필자의 이 포스팅에 언급한 parseInt(), parseDouble() 등의 매서드와 동일한 기능을 하는 format()이라는 매서드를 정의하고 있다.
format()은 인자로 double이나 long 타입의 값을 받아, 문자열로 출력해주는 기능을 한다. 크게 어려운 녀석은 아니다.
NumberFormat은 단순히 값을 String으로 변환하는 매서드밖에 가지지 않기 때문에 필자가 원하는 정렬 기능은 이 클래스로 만들 수 없다. 대신 정렬 출력 기능은 DecimalFormat 클래스에서 제공한다.
DecimalFormat은 NumberFormat과 달리 new 키워드를 통한 인스턴스 생성을 지원한다. 생성자의 매개인자로 문자열을 입력받는데 이 문자열이 출력할 숫자의 포맷을 결정하게 된다.
# 또는 0: 각 숫자를 의미한다. 단, 0의 경우 빈 값이 입력되면 0으로 채우나 #은 빈 값 입력 시 공백을 반환한다.
E : 지수기호. 소숫점 자릿수가 긴 경우, # 또는 0과 함께 사용한다. 단 E 뒤에는 0만 사용가능하다 ex) ##.#E0
.(온점) : 소숫점을 나타낼 때 사용한다.
- : 음수 부호. 음수를 나타낼 때 가장 앞 또는 뒤에 입력한다.
% : 퍼센트. 숫자 뒤 퍼센트 기호 추가 시 사용한다.
예를 들어, 세 학생의 국어, 영어, 수학 점수 및 합산 / 평균 점수를 정렬하여 나타내고자 한다고 해보자. 각 학생마다 취득한 총점이 다르기 때문에 소숫점 자릿수는 double일 경우 15자리, float일 경우 7자리까지 출력된다. 이러면 성적을 확인하는 교사 입장에서도 짜증이 나니, 평균값은 소숫점 2자리까지만 출력되도록 만들어보려한다.
위의 예시를 보면 알 수 있듯이 DecimalFormat 클래스의 기능은, 자신이 출력해야하는 숫자의 포맷대로(인스턴스화 시 매개변수로 사용한 문자열) 기억하는 것이며, DecimalFormat의 매서드인 format()은 매개인자로 들어온 숫자를 정의된 포맷대로 문자열로 변환하는 기능을 한다.
#와 0은 둘 다 한 자리 숫자를 나타내는 기호이지만, E와 함께 사용되는 경우를 제외하면 동시 사용이 불가능하다.
참고로 DecimalFormat은 숫자와 관련된 끝판왕 클래스인 Number로 변환하는 매서드를 제공한다. DecimalFormat, Number, double 사이 변환은 아래와 같이 진행된다.
2. ChoiceFormat 클래스
앞서 보았던 Calendar, Date 클래스 관련 포스팅에서, 이들 클래스는 요일 값이 숫자로 반환됨을 언급했었다. 따라서 이 값들을 실제 요일로 변경하기 위해서는 if-else if문으로 조건을 달아 코드를 작성해야하는데, Java if문의 특성 상, 코드가 장황해지면서 가독성이 떨어지기 쉽다. 이 때문에 Java는 특정 범위에 속하는 값을 지정된 문자열로 변경해주는 ChoiceFormat 클래스를 제공한다.
ChoiceFormat의 경우 인자로 double 배열과 String 배열을 가지는데, double 배열의 경우 오름차순으로 값이 정렬되어 있는 배열이어야 한다고 명시되어 있다. Date 클래스로부터 얻은 요일값을 문자열로 변경하는 작업을 진행해보자.
Format으로 끝나는 클래스는 모두 format()이라는 매서드를 정의하고 있는데, 정렬된 값을 문자열로 돌려주는 기능을 공통적으로 가진다. ChoiceFormat의 경우도, 매개 인자의 첫 배열에 정의된 범위에 위치한 값을 두 번째 배열 문자열로 변환한다. 크게 어려운 내용은 아니다.
그런데 의문점이 하나 생긴다. 왜 생성자의 첫 매개인자는 int가 아니라 double 배열을 사용할까? 필자가 범위라고 언급한 데는 이유가 있다. 그럴 일은 없지만 오늘 요일이 정수가 아닌 실수로 반환된다고 해보자. 1.0 이상 2.0 미만의 값이 반환될 경우 "Mon" 값이 출력된다.
시험 성적을 등급으로 변환하는 코드 또한 복잡하게 if-else if 문을 사용할 필요 없이 ChoiceFormat으로 가독성있게 코딩하면 된다.
ChoiceFormat은 생성자 매개 인자로 배열이 아닌 문자열 패턴을 받을 수 있도록 오버로딩 되어 있다.
여기서 사용되는 문자열 패턴은 마치 앞서 보았던 두 매개인자 배열이 합쳐진 듯한 형상을 하고 있다.
double [] limits = {0, 60, 70, 80, 90};
String [] grade = {"F", "D", "C", "B", "A"};
=> String pattern = new String ("0#F|60#D|70#C|80#B|90#A");
패턴에서 사용된 #은 =(이퀄)과 같은 의미다. 위의 패턴을 보면 90점을 획득하면 "A" 등급이 부여된다. 만약 90점을 맞았을 때 "B" 등급이 부여하도록 변경하고 싶다면 #을 < 로 변경하면 된다. ChoiceFormat의 패턴은 #과 < 만 기호로 제공한다.
3. MessageFormat 클래스
MessageFormat은 System.out.printf()와 유사한 기능을 제공하는 클래스다. 이 클래스로 서로 다른 타입의 변수를 문자열로 한 번에 출력할 수 있다. 필자는 이 MessageFormat을 사용하여 Linux 프로그램에서 tcpdump 명령어로 캡쳐된 일부 통신 내역을 분석해보려한다.
위 사진은 필자가 사용하는 Linux가 진행하는 네트워크 통신 내역을 tcpdump 명령어로 추출한 결과의 일부다. 대부분의 행은 가장 앞에 캡쳐한 통신 순번과 시간(Time stamp), 출발지 IP 주소 -> 목적지 IP 주소, 통신 프로토콜(TCP, UDP 등), 출발지 포트 번호 -> 목적지 포트 번호 등으로 표시된다. 즉, 각 행을 split(" ")으로 나누면 아래와 같이 분류가 가능해진다.
{0}: 순번
{1}: 타임스탬프(Timestamp)
{2}: 출발지 목적지 IP 주소
{3}: ->
{4}: 목적지 IP 주소
{5}: 통신 프로토콜
{7}: 출발지 Port 번호
{8}: ->
{9}: 목적지 Port 번호
MessageFormat은 인스턴스 생성 시, 생성자에 출력할 문자 패턴을 입력한다. 그리고 저장된 패턴을 바탕으로 출력하고자 하는 Object 배열의 각 값을 format() 매서드로 출력한다.
패턴 문자열에 중괄호{}로 표기한 부분은 출력할 문자열 배열의 index를 나타낸다. 위에서 명시한대로 각 행에 대해 split()을 적용하면 순번은 0, 출발지 IP는 2... 로 저장되며, format() 매개 인자로 사용된 배열의 각 index 값이 패턴에 명시된 자신의 번호에 대입된다. 만약에 MessageFormat 없이 동일한 결과를 출력하는 코드를 작성하려면 변수로 배열의 각 값을 저장하거나 일일이 배열 index를 명시해주어야 하는데, 전자의 경우 메모리도 불필요하게 사용하게되며 후자는 가독성이 떨어져 추후 코드 관리가 어려워진다는 문제가 발생한다.
4. DateFormat, SimpleDateFormat 클래스
SimpleDateFormat과 DateFormat은 날짜를 형식화하는 클래스다. Date나 Calendar, LocalDateTime은 프로그래머가 지정한 날짜 형식대로 출력하려면 일일이 필드값을 뽑아낸 뒤 MessageFormat이나 printf()로 일일이 포맷을 작성해야 한다는 불편함이 있다. SimpleDateFormat 클래스는 날짜의 각 요소를 알파벳을 사용하여 패턴 지정을 할 수 있으며, 다른 Format 클래스와 마찬가지로 format() 매서드를 이용해 패턴에 맞게 날짜를 문자열로 출력할 수 있다.
필자가 기존의 방식으로 Date 날짜를 2022-08-26 형식과 2022/08/26 형식으로 출력한다고 가정해보자.
printf() 매서드로 형식을 작성하면, 형식 패턴 상에서 어떤 것이 연도를 나타내고 어떤 것이 월을 나타내는지 헷갈릴 수 있다. 따라서 SimpleDateFormat은 이러한 문제가 발생하는 것을 방지하기 위해, 패턴을 일반 알파벳으로 표시한다. SimpleDateFormat 인스턴스를 생성하고 패턴을 정의한 뒤, format() 매서드에 Date 객체 변수명인 todayDate를 대입하면 아래와 같이 동일한 결과가 나타난다.
날짜와 시간 요소를 대표하면 문자는 아래와 같다.
참고로 SimpleDateFormat의 format() 매서드는 매개인자로 Date 밖에 입력받지 못한다. 따라서 Calendar 등의 날짜/시간 객체를 SimpleDateFormat 클래스로 형식화하려면 반드시 Date 객체 타입으로 변경을 먼저 진행해야 한다.
SimpleDateFormat이 상속하는 클래스 중 DateFormat이라는 클래스도 있는데, 이는 추상클래스다. SimpleDateFormat은 DateFormat에 정의된 매서드를 오버라이딩하여 사용한다.
5. java.time.format.DateTimeFormatter 클래스
그럼, 여기서 의문점이 하나 생긴다. 자주 사용하는 날짜 형식은 이미 표준으로 정해진 것이 많은데, 이를 정의해놓은 클래스도 존재하지 않을까? 당연히 있다. java.time.format 패키지 안에 있는 DateTimeFormatter 클래스가 그 예인데, 이 클래스는 상수 형태로 날짜와 시간 패턴을 정의하고 있다.
필자가 바로 직전에 SimpleDateFormat을 통해 사용했던 yyyy-mm-dd는 ISO-LOCAL_TIME이라는 이름으로 지정되어 있다. 이를 DateTimeFormatter의 인스턴스에 지정해보려한다.
DateTimeFormatter를 사용하는 방식은 두 가지다. DateTimeFormatter의 상수인 패턴을 LocalDate, LocalTime, LocalDatetime 객체 인스턴스로부터 호출된 format() 매서드의 매개인자로 사용하던가, 혹은 해당 패턴으로부터 format() a매서드를 호출한 뒤, 매서드 매개인자로 LocalDate, LocalTime, LocalDateTime 객체를 대입하는 방식이 있다.
DateTimeFormat은 각 시간대에서 표시하는 시간의 형식도 제공한다. 이는 ofLocalizedDate(), ofLocalizedTime(), ofLocalizedDateTime() 매서드를 사용하면 된다. 각 매서드는 매개인자로 FormatStyle 클래스 내의 상수를 사용하는데, 상수명은 FULL, LONG, MEDIUM, SHORT 4 가지가 있다. 한국 시간대에서 각 상수들이 날짜와 시간을 표현하는 방식은 아래와 같다.
상수명 | 날짜 | 시간 |
FULL | yyyy년 MM월 dd일 E요일 | - |
LONG | yyyy년 MM월 dd일 | 오후 h시 m분 s초 |
MEDIUM | yyyy.MM.dd | 오후 h:m:s |
SHORT | yy.MM.dd | 오후 h:m |
LocalDateTime과 LocalTime을 ofLocalizedDateTime() 또는 ofLocalizedTime() 매서드로 FormatStyle.FULL, FormatStyle.LONG으로 포맷팅하면 에러가 발생하는데, DateTimeFormatter에서 파싱이 불가능한 TimeZone 때문에 그렇다. 따라서 LocalTime과 LocalDateTime의 시간 영역에 한해서는 MEDIUM과 SHORT로만 포맷팅이 가능하다.
그럼 직접 DateTimeFormatter에 패턴을 만드는 것은 가능할까? DateTimeFormatter는 ofPattern() 이라는 이름의 매서드를 통해 프로그래머가 정의한 형태의 날짜/시간 패턴을 만드는 것이 가능하다. 물론 이 값들은 상수로 지정이 되지 않기 때문에 DateTimeFormatter 인스턴스 변수명을 format() 매서드의 매개인자로 사용해야 한다.
지난 6개의 포스팅을 통해 Java에서 자주 사용하거나 혹은 유용한 클래스에 대해 간략하게나마 알아보았다. 다음 포스팅부터는 Java의 컬렉션 프레임워크(Collection Framework), 즉 데이터 집합체의 저장과 관련된 내용에 대해 알아보려 한다.
Fin
'Java > Java Basic' 카테고리의 다른 글
[Java Basic] 32 - 컬렉션 프레임워크2 (ArrayList, LinkedList) (0) | 2022.09.01 |
---|---|
[Java Basic] 31 - 컬렉션 프레임워크 개요 및 Vector 클래스 (0) | 2022.08.29 |
[Java Basic] 29. 날짜 및 시간 관련 클래스 (0) | 2022.08.24 |
[Java Basic] 28. Wrapper 클래스와 기타 클래스 (0) | 2022.08.22 |
[Java Basic] 27. java.lang.StringBuffer, StringBuilder 클래스, 문자열 인코딩 (0) | 2022.08.16 |
댓글