✏️[모던 자바 인 액션, 전문가를 위한 자바 8, 9, 10 기법 가이드] 스터디 관련 책 내용을 정리한 글입니다.
📌 이 장의 내용
- 자바 8에서 새로운 날짜와 시간 라이브러리를 제공하는 이유
- 사람이나 기계가 이해할 수 있는 날짜와 시간 표현 방법
- 시간의 양 정의하기
- 날짜 조작, 포매팅, 파싱
- 시간대와 캘린더 다루기
기존 자바 API의 날짜와 시간 관련 기능은 불편하고 모호했으며, Date 클래스는 실제로 밀리초 단위의 특정 시점을 표현하는 클래스였습니다. 하지만 자바 8에서는 새로운 날짜와 시간 API가 도입되어, 이전에 있었던 문제들을 개선하고 자바 개발자들이 보다 쉽게 날짜와 시간을 다룰 수 있게 되었습니다.
다음은 자바 9의 릴리스 날짜인 2017년 9월 21일을 가리키는 Date 인스턴스를 만드는 코드입니다.
Date date = new Date(117, 8, 21);
// 다음은 날짜 출력 결과입니다.
// Thu Sep 21 00:00:00 CET 2017
Date 클래스의 결과가 직관적이지 않고, 반환되는 문자열을 추가로 활용하기가 어렵습니다. Date 클래스는 중앙 유럽 시간대를 사용하지만, 자체적으로 시간대 정보를 알고 있는 것은 아닙니다.
과거 버전과 호환성을 깨뜨리지 않으면서 자바 1.0의 Date 클래스 문제를 해결하기 위해, 자바 1.1에서는 java.util.Calendar 클래스를 대안으로 제공했습니다. 그러나 Calendar 클래스도 설계 문제가 있어 개발자들에게 혼란을 초래했습니다. DateFormat은 스레드 안전하지 않아서 예기치 못한 결과가 발생할 수 있었고, Date와 Calendar 클래스는 가변 클래스로 유지보수가 어려워 Joda-Time 같은 서드파티 라이브러리를 사용하는 경우가 많았습니다. 이에 자바 8에서는 java.time 패키지를 도입하여 더 나은 날짜와 시간 API를 제공하였으며, Joda-Time의 많은 기능을 추가하였습니다.
이 장에서는 새로운 날짜와 시간 API가 제공하는 새로운 기능을 살펴봅니다.
12.1 LocalDate, LocalTime, Instant, Duration, Period 클래스
java.time 패키지는 LocalDate, LocalTime, LocalDateTime, Instant, Duration, Period 등 새로운 클래스를 제공하여 날짜와 시간 간격을 다룰 수 있도록 도와줍니다.
12.1.1 LocalDate와 LocalTime 사용
java.time 패키지에서 제공하는 LocalDate는 불변 객체로, 시간을 제외한 날짜를 표현합니다. 어떤 시간대 정보도 포함하지 않습니다. of 정적 팩토리 메서드를 이용하여 LocalDate 인스턴스를 생성할 수 있으며, 다음 코드에서 보여주는 것처럼 연도, 달, 요일 등을 반환하는 메서드를 제공합니다.
LocalDate date = LocalDate.of(2017, 9, 21); // 2017-09-21
int year = date.getYear(); // 2017
Month month = date.getMonth(); // SEPTEMBER
int day = date.getDayOfMonth(); // 21
DayOfWeek dow = date.getDayOfWeek(); // THURSDAY
int len = date.lengthOfMonth(); // 31(3월의 일 수)
boolean leap = date.isLeapYear(); // false(윤년이 아님)
팩토리 메서드 now는 시스템 시계의 정보를 이용해서 현재 날짜와 정보를 얻습니다.
LocalDate today = LocalDate.now();
java.time 패키지에서 제공하는 다른 날짜와 시간 관련 클래스들도 get 메서드와 TemporalField 인터페이스를 이용하여 정보를 얻을 수 있습니다. ChronoField는 TemporalField 인터페이스를 제공하므로, 다음 코드처럼 열거자 요소를 이용하여 원하는 정보를 쉽게 얻을 수 있습니다.
int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int day = date.get(ChronoField.DAY_OF_MONTH);
java.time 패키지에서 제공하는 클래스들은 getYear(), getMonthValue(), getDayOfMonth() 등과 같은 내장 메서드를 제공합니다. 이를 활용하면 가독성을 높일 수 있습니다.
int year = date.getYear();
int month = date.getMonth();
int day = date.getDayOfMonth();
LocalTime 클래스를 사용하면 시간을 표현할 수 있습니다. of 메서드를 사용하여 LocalTime 인스턴스를 만들 수 있으며, 시간과 분 또는 시간과 분, 초를 인수로 받는 오버로드 버전의 of 메서드가 있습니다. 다음처럼 LocalTime 클래스는 LocalDate 클래스와 마찬가지로 다양한 get 메서드를 제공하여 시간을 구성하는 시, 분, 초, 밀리초 등의 정보를 얻을 수 있습니다.
LocalTime time = LocalTime.of(13, 45, 20); // 13:45:20
int hour = time.getHour(); // 13
int minute = time.getMinute(); // 45
int second = time.getSecond(); // 20
날짜와 시간 문자열로 LocalDate와 LocalTime의 인스턴스를 만드는 방법도 있습니다. 다음처럼 parse 정적 메서드를 사용할 수 있습니다.
LocalDate date = LocalDate.parse("2017-09-21");
LocalDate time = LocalDate.parse("13:45:20");
parse 메서드를 사용할 때는 DateTimeFormatter 인스턴스를 전달하여 날짜와 시간 객체의 형식을 지정할 수 있습니다. DateTimeFormatter 클래스는 java.util.DateFormat 클래스를 대체하는 클래스입니다. 문자열을 LocalDate나 LocalTime으로 파싱 하는 도중 문제가 발생하면 parse 메서드는 RuntimeException을 상속받는 DateTimeParseException 예외를 발생시킵니다.
12.1.2 날짜와 시간 조합
LocalDateTime은 LocalDate와 LocalTime을 쌍으로 갖는 복합 클래스로, 날짜와 시간을 모두 표현할 수 있습니다.
다음 코드에서처럼 직접 LocalDateTime을 만드는 방법도 있고 날짜와 시간을 조합하는 방법도 있습니다.
// 2017-09-21T13:45:20
LocalDateTime dt1 = LocalDateTime.of(2017, Month.SEPTEMBER, 21, 13, 45, 20);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);
LocalDate의 atTime 메서드와 LocalTime의 atDate 메서드로 LocalDateTime을 만들 수 있으며, LocalDateTime의 toLocalDate나 toLocalTime 메서드로 LocalDate나 LocalTime을 추출할 수 있습니다.
LocalDate date1 = dt1.toLocalDate(); // 2017-09-21
LocalDate time1 = dt1.toLocalTime(); // 13:45:20
12.1.3 Instant 클래스 : 기계의 날짜와 시간
Instant 클래스는 기계적인 관점에서 시간을 표현하며, ofEpochSecond 팩토리 메서드를 사용하여 초를 인수로 전달하여 Instant 인스턴스를 만들 수 있습니다. Instant 클래스는 나노초 단위의 정밀도를 제공하며, ofEpochSecond 메서드의 두 번째 인수를 사용하여 나노초 단위로 시간을 보정할 수 있습니다. 두 번째 인수는 0에서 999,999,999 사이의 값을 가질 수 있습니다.
따라서 다음 네 가지 ofEpochSecond 호출 코드는 같은 Instant를 반환합니다.
Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3, 0);
Instant.ofEpochSecond(2, 1_000_000_000); // 2초 이후의 1억 나노초 (1초)
Instant.ofEpochSecond(4, -1_000_000_000); // 4초 이전의 1억 나노초 (1초)
Instant 클래스는 기계적인 시간 정보를 포함하기 때문에, now 메서드로 생성된 인스턴스는 초와 나노초 정보를 포함하지만, 사람이 읽을 수 있는 시간 정보를 제공하지 않습니다. 따라서 Instant 클래스를 사용할 때는 유의해야 합니다.
예를 들어 다음 코드를 보면 다음과 같습니다.
int day = Instant.now().get(ChronoField.DAY_OF_MONTH);
// 위 코드는 다음과 같은 예외를 일으킵니다.
java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth
Instant에서는 Duration과 Period 클래스를 함께 활용할 수 있습니다.
12.1.4 Duration과 Period 정의
Temporal 인터페이스는 특정 시간을 모델링하는 객체의 값을 어떻게 읽고 조작할지 정의하며, Duration 클래스의 between 메서드를 이용하여 두 시간 객체 사이의 지속시간을 만들 수 있습니다.
다음 코드에서 보여주는 것처럼 두 개의 LocalTime, 두 개의 LocalDateTime, 또는 두 개의 Instant로 Duration을 만들 수 있습니다.
Duration d1 = Duration.between(time1, time2);
Duration d1 = Duration.between(dateTime1, dateTime2);
Duration d2 = Duration.between(instant1, instant2);
LocalDateTime과 Instant는 서로 혼합할 수 없으며, Duration 클래스는 초와 나노초로 시간 단위를 표현하므로 LocalDate를 사용할 수 없습니다. 년, 월, 일로 시간을 표현할 때는 Period 클래스의 팩토리 메서드 between 메서드를 사용하여 두 LocalDate의 차이를 확인할 수 있습니다.
Period tenDays = Period.between(LocalDate.of(2017, 9, 11),
LocalDate.of(2017, 9, 21));
마지막으로 Duration과 Period 클래스는 다양한 팩토리 메서드를 제공하여 인스턴스를 만들 수 있습니다. 이를 사용하면 다음 예제처럼 두 시간 객체를 사용하지 않고도 Duration과 Period 클래스를 만들 수 있습니다.
Duration threeMinutes = Duration.ofMinutes(3);
Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES);
Period tenDays = Period.ofDays(10);
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);
아래 그림은 Duration과 Period 클래스가 공통으로 제공하는 메서드입니다.
새로운 날짜와 시간 API에서는 변경된 객체 버전을 만들 수 있는 메서드를 제공합니다.
다음 절에서는 LocalDate 인스턴스에 변경된 버전을 만들 수 있는 방법과, 날짜와 시간 포맷을 만드는 방법과 이를 파싱하고 출력하는 방법을 다룹니다.
12.2 날짜 조정, 파싱, 포매팅
withAttribute 메서드로 기존의 LocalDate를 바꾼 버전을 직접 간단하게 만들 수 있습니다. 다음 코드에서는 바뀐 속성을 포함하는 새로운 객체를 반환하는 메서드를 보여줍니다. 모든 메서드는 기존 객체를 바꾸지 않습니다.
LocalDate date1 = LocalDate.of(2017, 9, 21); // 2017-09-21
LocalDate date2 = date1.withYear(2011); // 2011-09-21
LocalDate date3 = date2.withDayOfMonth(25); // 2011-09-25
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 2); // 2011-02-25
위 코드에서는 TemporalField를 인자로 받는 with 메서드를 사용하여 불변 객체의 필드값을 변경하는 방법을 보여줍니다. 이는 범용적으로 사용할 수 있는 방법입니다. 이와 함께, Temporal 인터페이스는 get과 with 메서드를 제공하여, 해당 객체의 필드 값을 읽거나 수정할 수 있습니다. 그러나, 지원하지 않는 필드를 사용하면 UnsupportedTemporalTypeException이 발생합니다. LocalDate와 같은 객체를 사용하는 선언형 방식도 가능합니다.
예를 들어 다음 예제처럼 지정된 시간을 추가하거나 뺄 수 있습니다.
LocalDate date1 = LocalDate.of(2017, 9, 21); // 2017-09-21
LocalDate date2 = date1.plusWeek(1); // 2017-09-28
LocalDate date3 = date2.minusYears(6); // 2011-09-28
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS); // 2012-03-28
위 코드에서는 Temporal 인터페이스에 정의된 plus, minus 메서드를 이용해서 Temporal 객체를 특정 시간만큼 앞뒤로 이동시킬 수 있습니다. 이들 메서드는 ChronoUnit 열거형을 활용해서 사용할 수 있으며, LocalDate, LocalTime, LocalDateTime, Instant 등 날짜와 시간을 표현하는 모든 클래스에서 사용할 수 있습니다.
다음 그림은 이들 공통 메서드를 설명합니다.
12.2.1 TemporalAdjusters 사용하기
날짜 조정을 더 복잡하게 처리해야 할 때는 TemporalAdjuster를 이용할 수 있습니다. TemporalAdjuster는 다양한 조정 기능을 제공하는데, 이를 이용해 오버로드된 버전의 with 메서드를 호출할 수 있습니다. 예제 코드에서는 TemporalAdjuster를 이용해 다양한 조정 기능을 수행하는 방법을 보여줍니다.
import static java.time.temporal.TemporalAdjusters.*;
LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY)); // 2014-03-23
LocalDate date3 = date2.with(lastDayOfMonth()); // 2014-03-31
다음 그림은 다양한 TemporalAdjusters의 팩토리 메서드로 만들 수 있는 TemporalAdjuster 리스트를 보여줍니다.
위 예제에서 확인할 수 있는 것 처럼 TemporalAdjuster 인터페이스는 날짜와 시간을 조정하는 adjustInto() 메서드 하나만을 정의합니다. 이러한 구조 때문에 TemporalAdjuster는 함수형 인터페이스이며, 람다식을 이용하여 쉽게 구현할 수 있습니다.
@FunctionalInterface
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}
TemporalAdjuster 인터페이스 구현은 Temporal 객체를 다른 Temporal 객체로 변환하는 방법을 정의하며, 이를 UnaryOperator<Temporal>과 유사한 형태로 간주할 수 있습니다.
다음 절에서는 새로운 날짜와 시간 API에서 문자열로 표현된 날짜와 시간을 다루는 방법과 반대로 날짜와 시간 객체를 문자열로 변환하는 방법을 살펴봅니다.
12.2.2 날짜와 시간 객체 출력과 파싱
java.time.format 패키지의 DateTimeFormatter 클래스는 포매팅과 파싱을 위한 클래스이며, 정적 팩토리 메서드와 상수를 활용하여 포매터를 쉽게 만들 수 있습니다. BASIC_ISO_DATE와 ISO_LOCAL_DATE 등의 상수를 제공하며, 이를 이용하여 날짜와 시간을 특정 형식의 문자열로 변환할 수 있습니다. 다음은 두 개의 서로 다른 포매터로 문자열을 만드는 예제입니다.
LocalDate date = LocalDate.of(2014, 3, 18);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); // 20140318
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2014-03-18
날짜와 시간을 표현하는 문자열을 파싱해서 날짜 객체를 만드는 방법으로, 날짜와 시간 API에서 특정 시점이나 간격을 표현하는 클래스들의 parse() 팩토리 메서드를 이용할 수 있다. 이를 통해 문자열을 날짜 객체로 만들 수 있다.
LocalDate date1 = LocalDate.parse("20140318", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2014-03-18", DateTimeFormatter.ISO_LOCAL_DATE);
DateTimeFormatter 클래스는 스레드에서 안전하게 사용할 수 있는 클래스이며, 다음 예제에서 보여주는 것처럼 특정 패턴으로 포매터를 만들 수 있는 정적 팩토리 메서드를 제공합니다.
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date1.format(formatter);
LocalDate date2 = LocalDate.parse(formattedDate, formatter);
LocalDate의 format 메서드는 지정한 포맷의 문자열을 생성하고, parse 메서드는 해당 문자열을 파싱하여 날짜를 생성합니다. 다음 예제에서 보여주는 것처럼 ofPattern 메서드는 Locale을 지정하여 포매터를 만들 수 있는 오버로드된 메서드를 제공합니다.
DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date.format(italianFormatter); // 18. marzo 2014
LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter);
DateTimeFormatterBuilder 클래스를 사용하면 좀 더 세부적인 포매터를 정의할 수 있습니다. 이 클래스를 사용하여 파싱, 패딩, 포맷의 선택사항 등을 제어할 수 있습니다. 이전에 생성한 포매터를 이용해서 프로그램적으로 포매터를 만들 수도 있습니다.
예를 들어 위 코드에서 사용한 italianFormatter를 DateTimeFormatterBuilder에 이용하면 프로그램적으로 포매터를 만들 수 있습니다.
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral(". ")
.appendText(ChronoField.MONTH_OF_YEAR)
.appendLiteral(" ")
.appendText(ChronoField.YEAR)
.parseCaseInsensitive()
.toFormatter(Locale.ITALIAN);
다음 절에서는 새로운 날짜와 시간 API를 사용하여 다양한 시간대와 캘린더를 처리하는 방법을 다룹니다.
12.3 다양한 시간대와 캘린더 활용 방법
새로운 날짜와 시간 API에서는 시간대를 간단하게 처리할 수 있도록 java.time.ZoneId 클래스가 등장했습니다. 이 클래스는 기존의 java.util.TimeZone을 대체할 수 있으며, 서머타임(DST)과 같은 복잡한 사항을 자동으로 처리합니다. ZoneId는 불변 클래스입니다.
12.3.1 시간대 사용하기
ZoneRules 클래스는 표준 시간이 같은 지역을 묶어서 시간대 규칙 집합을 정의하는 클래스입니다. 약 40개 정도의 시간대가 있으며, ZoneId의 getRules()를 사용하여 해당 시간대의 규칙을 획득할 수 있습니다.
다음처럼 지역 ID로 특정 ZoneId를 구분합니다.
ZoneId romeZone = ZoneId.Of("Europe/Rome");
ZoneId는 지역 ID를 나타내는 클래스로, IANA Time Zone Database에서 제공하는 지역 집합 정보를 사용합니다. 다음 코드에서 보여주는 것처럼 toZoneId 메서드를 사용하면 기존의 TimeZone 객체를 ZoneId 객체로 변환할 수 있습니다.
ZoneId zoneId = TimeZone.getDefault().toZoneId();
아래 코드에서는 ZoneId 객체를 이용해 LocalDate, LocalDateTime, Instant를 ZonedDateTime으로 변환하는 방법을 보여줍니다. 이를 통해 특정 시간대에 상대적인 시점을 표현할 수 있습니다.
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
ZonedDateTIme zdt1 = date.atStartOfDay(romeZone);
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
ZonedDateTIme zdt2 = dateTime.atZone(romeZone);
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);
ZoneId를 이용해서 LocalDateTime을 Instant로 변환할 수 있습니다.
instant instant = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);
기존의 Date 클래스를 처리하는 코드와 새로운 날짜와 시간 API 간의 동작에 도움이 되는 toInstant(), fromInstant() 두 개의 메서드가 존재합니다.
12.3.2 UTC/Greenwich 기준의 고정 오프셋
ZoneOffset 클래스를 이용해서 UTC/GMT 기준으로 시간대를 표현할 수 있습니다. 이를 이용하면 특정 지역의 표준 시간과 차이를 표현할 수 있습니다.
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
ZoneOffset 클래스는 그리니치 0도 자오선과 시간값의 차이를 표현하며, 서머타임을 처리하지 않으므로 권장되지 않습니다. ZoneId를 사용해서 오프셋을 처리하는 것이 좋습니다. 또한 ISO-8601 캘린더 시스템에서 정의하는 UTC/GMT와 오프셋으로 날짜와 시간을 표현하는 OffsetDateTime을 사용하는 방법도 있습니다.
LocalDateTime dateTIme = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(date, newYorkOffset);
새로운 날짜와 시간 API는 ISO 캘린더 시스템에 기반하지 않은 정보도 처리할 수 있는 기능을 제공합니다.
12.3.3 대안 캘린더 시스템 사용하기
새로운 날짜와 시간 API는 추가로 4개의 캘린더 시스템을 제공합니다. ThaiBuddhistDate, MinguoDate, JapaneseDate, HijrahDate 4개의 클래스가 각각의 캘린더 시스템을 대표하며, LocalDate를 이용해서 이들 4개의 클래스 중 하나의 인스턴스를 만들 수 있습니다. 이들 클래스는 모두 ChronoLocalDate 인터페이스를 구현하며, 임의의 연대기에서 특정 날짜를 표현할 수 있는 기능을 제공합니다. 일반적으로 다음 코드에서 보여주는 것처럼 정적 메서드로 Temporal 인스턴스를 만들 수 있습니다.
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
JapaneseDate japaneseDate = JapaneseDate.from(date);
새로운 날짜와 시간 API에서는 Chronology가 캘린더 시스템을 나타내며, Locale을 기반으로 Chronology 인스턴스를 만들 수 있습니다. ofLocale 정적 팩토리 메서드를 사용하여 Chronology 인스턴스를 가져올 수 있습니다.
Chronology japaneseChronology = Chronology.ofLocale(Locale.JAPAN);
ChronoLocalDate now = japaneseChronology.dateNow();
ChronoLocalDate보다는 LocalDate를 사용하는 것이 권장되며, 모든 데이터 저장, 조작, 비즈니스 규칙 해석 등의 작업에서 LocalDate를 사용해야 한다는 것이 날짜와 시간 API 설계자의 권장입니다. 이는 멀티캘린더 시스템에서는 특히 적용되지 않기 때문입니다.
이슬람력
HijrahDate 클래스는 이슬람력을 따르는 캘린더 시스템으로, 새로운 달을 결정하는 방식에 따라 변형이 있습니다. withVariant 메서드를 사용하여 원하는 변형 방식을 선택할 수 있으며, 자바 8에서는 UmmAl-Qura를 표준 변형 방식으로 제공합니다.
다음 코드는 현재 이슬람 연도와 시작과 끝을 ISO 날짜로 출력하는 예제입니다.
HijrahDate = ramadanDate = HijrahDate.now().with(ChronoField.DAY_OF_MONTH, 1)
.with(ChronoField.MONTH_OF_YEAR, 9);
System.out.println("Ramadan starts on "
+ IsoChronology.INSTANCE.date(ramadanDate)
+ and ends on "
+ IsoChronology.INSTANCE.date(ramadanDate.with(
TemporalAdjusters.lastDayOfMonth())));
'📚 서적 및 학습자료 > 모던 자바 인 액션[2회독]' 카테고리의 다른 글
[모던 자바 인 액션] 14장 자바 모듈 시스템 (0) | 2023.03.27 |
---|---|
[모던 자바 인 액션] 13장 디폴트 메서드 (0) | 2023.03.26 |
[모던 자바 인 액션] 11장 null 대신 Optional 클래스 (0) | 2023.03.25 |
[모던 자바 인 액션] 10장 람다를 이용한 도메인 전용 언어 (0) | 2023.03.23 |
[모던 자바 인 액션] 9장 리팩터링, 테스팅, 디버깅 (0) | 2023.03.21 |