Java & Spring

[모던자바인액션] Part4 Optional, 날짜시간, 디폴트메서드, 모듈

CH11. null 대신 Optional 클래스

Optional 클래스 소개

우선 이번 챕터에서 사용할 예제는 다음과 같다.

public class Person {
	private Car car;
    public Car getCar() { return car; }
}

public class Car {
	private Insurance insurance;
    public Insurance getInsurance() { return insurance; }
}

public class Insurance {
	private String name;
    public String getName() { return name; }
}

 

우리는 NullPointerException을 피하기 위해 아래처럼 null 확인 코드를 사용해왔다.

// null 확인을 하지 않는 코드
public String getCarInsuranceName(Person person) {
	return person.getCar().getInsurance().getName();
}

// null 확인을 추가한 코드
public String getCarInsuranceName(Person person) {
	if (person != null) {
    	Car car = person.getCar();
        if (car != null) {
        	Insurance insurance = car.getInsurance();
            if (insurance != null) {
            	return insurance.getName();
            }
        }
    }
    return "Unknown";
}

이렇게 짜여진 코드는 매번 if가 추가되면서 코드 들여쓰기 수준이 증가한다. 코드 구조가 엉망이 되고 가독성도 떨어진다.

 

Optional 클래스는 이를 좀더 간결하고 명확하게 표현하기 위해 Java 8에서 추가되었다. Optional은 선택형값을 캡슐화하는 클래스다.  값이 있으면 Optional 클래스로 값을 감싸고(Optional<Car>), 없는 경우 Optional.empty 메서드로 Optional을 반환한다. Optional로 감싸져있는 값을 받으면 개발자는 이 값이 없는 경우에 대해 적절하게 대응할 수 있다. (null이 될 수 없는 값의 경우 데이터의 문제 혹은 코드의 버그이므로 Optional로 감싸기 보다는 문제를 해결해야 한다.)

 

그럼 이제 Optional 클래스로 새롭게 정의된 예제로 Optional에 대해 알아본다.

public class Person {
	private Optional<Car> car;
    public Optional<Car> getCar() {
    	return car; 
    }
}

public class Car {
	private Optional<Insurance> insurance;
    public Optional<Insurance> getInsurance() {
    	return insurance;
    }
}

public class Insurnace {
	private String name; // 보험회사에는 반드시 이름이 있으므로 Optional로 감싸지 않는다.
    public String getName() {
    	return name;
    }
}

 


Optional 적용 패턴

Optional 객체 만들기

Optional 객체를 만드는 방법은 다음과 같다.

/*
	1. 빈 Optional
*/
Optional<Car> optCar = Optional.empty();

/*
	2. null이 아닌 값으로 Optional 만들기
	car가 null이면 NullPointerException이 발생. 
	Optional 사용하지 않았다면 car의 프로퍼티 접근 시점에 에러 발생했을 것.
*/
Optional<Car> optCar = Optional.of(car);

/*
    3. null값으로 Optional 만들기
    값이 null일 수도, 아닐수도 있다.
    car가 null이면 빈 Optional 객체가 반환된다.
*/
Optional<Car> optCar = Optional.ofNullable(car);

 

 

맵으로 Optional의 값을 추출하고 변환하기

Optional 객체를 생성했으니 이제 객체 내부에 있는 값을 추출하는 방법을 알아본다.

첫번째는 map이다.

// Optional을 사용하지 않는 코드
String name = null;
if (insurance != null) {
	name = insurance.getName();
}

// Optional을 사용해서 Map으로 값을 꺼내는 코드
Optional<Insurnace> optInsurnace = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);

optInsurance라는 Optional 객체가 값을 포함하고 있다면 map 메서드는 string을 리턴하는 getName을 통해 Optional<String> 을 리턴한다. 값을 포함하고 있지 않다면 아무 일도 일어나지 않는다.

 

 

flatMap으로 Optional 객체 연결

위에서는 map을 이용해서 Optional 내부에 있는 값을 Optional로 감싸서 뽑아냈다.

만약 이렇게 값을 연달아서 뽑아내고 싶다면 어떻게 해야할까? (ex. Person의 Car를 뽑고, Car의 Insurance를 뽑고, 최종적으로 Insurance의 name을 뽑고 싶으면?)

// 컴파일 오류
Optional<Person> optPerson = Optional.of(person);
Optional<String> name = 
	optPerson.map(Person::getCar)
    		 .map(Car::getInsurance)
             	 .map(Insurance::getName);
             
// flatMap으로 짠 코드
Optional<Person> optPerson = Optional.of(person);
Optional<String> name =
	optPerson.flatMap(Person::getCar)
    		 .flatMap(Car::getInsuarnce)
             	 .map(Insurance::getName);

첫번째 코드는 컴파일되지 않는다. getCar 메서드의 리턴 형식이 Optional<Car>인데(위의 Insurance의 getName은 리턴값이 string이다) map으로 한번더 감싸져서 Optional<Optional<Car>> 형식이 되기 때문이다. 

 

Optional에서는 이런 문제를 해결하기 위해 flatMap을 제공한다. (stream의 flatMap과 유사하다.) flatMap을 사용하면 이차원 Optional이 일차원 Optional로 평준화된다. 두번째 코드를 보면 알 수 있다.

 

참고) 도메인 모델에 Optional을 사용했을 때 데이터를 직렬화할 수 없는 이유

Optional 클래스의 설계자는 Optional을 필드 형식으로 사용할 것을 가정하지 않았다. 단지 선택형 반환값을 지원하는 것이라고 말했다. 즉, Optional 클래스는 Serializable 인터페이스를 구현하지 않는다. 따라서 우리는 도메인 모델에 Optional을 사용하는 경우 직렬화 모델을 사용하는 도구나 프레임워크에 문제가 생길 수 있다.

해당 책에서는 그럼에도 불구하고 Optional을 사용하는 것이 바람직하지만, 직렬화 모델이 필요하다면 아래와 같이 메서드에 Optional로 리턴해주는 것을 권장한다.
public class Person {
    private Car car;
    public Optional<Car> getCarAsOptional() {
        return Optional.ofNullable(car);
    }
}

 

 

Optional 스트림 조작

Optional의 stream 메서드는 자바 9에서 추가된 내용이다. stream 메서드를 사용하면 Optional을 포함하는 값을 가진 스트림으로 변환할 때 유용하다.

public Set<String> getCarInsuranceNames(List<Person> persons) {
	return persons.stream()
    		  .map(Person::getCar) // 반환값: Optional<Car>의 stream
                  .map(optCar -> optCar.flatMap(Car::getInsurance)) // 반환값: Optional<Insurance>의 stream
                  .map(optIns -> optIns.map(Insurance::getName)) // 반환값: Optional<String>의 stream
                  .flatMap(Optional::stream) // 반환값: Stream<String>
                  .collect(toSet()); // 반환값: Set<String>

 

 

디폴트 액션과 Optional 언랩

Optional 인스턴스에 포함된 값을 읽는 방법을 알아보자.

메서드명 내용 값이 있는 경우 반환값 값이 없는 경우 반환값
get() 가장 간단한 메서드.
가장 안전하지 않은 메서드.
Optional에 값이 반드시 있는 경우가 아니면 사용하지 않는 것이 바람직함.
해당 값 NoSuchElementException
orElse(T other) Optional이 값을 포함하지 않을 때 기본값을 제공할 수 있음. 해당 값 T other
orElseGet(Supplier<? extends T> other) orElse 메서드에 대응하는 게으른 버전의 메서드. Optional에 값이 없을 때만 Supplier가 실행. 해당 값 T other
orElseThrow(Supplier<? extends X> exceptionSupplier) Optional이 비어있을 때 예외를 발생시킴(get과 유사). 단, 예외 선택 가능 해당 값 X exceptionSupplier
ifPresent(Consumer<? super T> consumer) 값이 존재할 때 인수로 넘겨준 동작을 실행. 없으면 아무일도 일어나지 않음. T consumer 실행 없음.
ifPresentOrElse(Consumer<? super T> actions, Runnable emptyAction) 자바9에 추가된 메서드.
Optional이 비었을 때 실행할 수 있는 Runnable을 인수로 받는다는 점만 ifPresent와 다름.
T actions 실행 Runnable emptyAction 실행

 

두 Optional 합치기

두 Optional을 합치는 방법이다.

// null 확인 코드가 크게 다른 점이 없다.
public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
    if (person.isPresent() && car.isPresent()) {
    	return Optional.of(findCheapestInsurance(person.get(), car.get()));
    }
}

// Optional의 flatMap을 사용할 경우
public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
	return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}

 

필터로 특정값 거르기

보험회사 이름이 'CambridgeInsurance'인지 확인해야하는 경우로 필터링해보자.

// null확인 코드로 구현한 코드
Insurance insurace = ...;
if (insurance != null && "CambridgeInsurance".equals(insurance.getName())) {
	System.out.println("ok");
}

// Optional을 사용한 코드
Optional<Insurance> optInsurance = ...;
optInsurnace.filter(insurance -> "CambridgeInsurance".equals(insurance.getName()))
			.ifPresent(x -> System.out.println("ok"));

 


Optional을 사용한 실용 예제

기존의 자바 API(Optional이 사용되지 않은)에서 Optional을 어떻게 활용할 수 있을 지 알아보자.

 

잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기

기존의 자바 API가 null을 반환하는 경우 받는 쪽에서 아래와 같이 Optional로 처리할 수 있다.

Optional<Object> value = Optional.ofNullable(map.get("key"));

 

 

예외와 Optional 클래스

기존의 자바 API가 값을 제공할 수 없을 때 null이 아니라 예외를 발생시키는 경우 아래와 같이 Optional로 처리할 수 있다.

public static Optional<Integer> stringtoInt(String s) {
	try {
    	return Optional.of(Integer.parseInt(s));
    } catch (NumberFormatException e) {
    	return Optional.empty();
    }
}

위 코드에서 문자열 s를 정수형으로 반환할 수 있다면 Optional<Integer>를 반환하고

정수형으로 반환할 수 없다면 Exception을 catch해서 빈 Optional을 반환한다.

 

 

기본형 Optional을 사용하지 말아야 하는 이유

Optional도 Stream처럼 기본형으로 특화된 OptionalInt, OptionalLong, OptionalDouble 등을 제공한다. 하지만 stream과는 다르게 기본형을 사용한다고 해도 성능을 개선할 수 없고, map, flatMap, filter를 제공하지 않으므로 기본형 특화 Optional을 사용하는 것을 권장하지 않는다.

 

 

응용

프로그램의 설정 인수로 Properties를 전달한다고 가정하자. Property에는 이름과 지속시간이 들어가는데 아래와 같은 메서드로 Properties에서 이름을 통해 지속시간을 읽어올 것이다. 단, 지속시간은 시간이므로 양수인 숫자다.

// null 확인 코드
public int readDuration(Properties props, String name) {
	String value = props.getProperty(name);
    if (value != null) {
    	try {
            int i = Integer.parseInt(value);
            if (i > 0) {
            	return i;
            }
        } catch (NumberFormatException nfe) { }
    }
    return 0;
}

// Optional로 개선한 코드
public int readDuration(Properties props, String name) {
	return Optional.ofNullable(props.getProperty(name))
    			.flatMap(OptionalUtility::stringToInt)
                	.filter(i -> i > 0)
                	.orElse(0);
}

 


CH12 새로운 날짜와 시간 API

 

기존의 java.util.Date 클래스에서는 날짜를 의미하는 Date라는 클래스의 이름과 달리 특정 시점을 날짜가 아닌 밀리초 단위로 표현한다는 점, 1900년을 기준으로 하는 오프셋, 그리고 0에서 시작하는 달 인덱스 등 모호한 설계로 유용성이 떨어졌다. 이번장에서는 자바 8에 새롭게 추가된 java.time 패키지에 대해 알아보자. java.time 패키지는 LocalDate, LocalTime, LocalDateTime, Instant, Duration, Period 등 새로운 클래스를 제공한다.

 

LocalDate, LocalTime, Instant, Duration, Period 클래스

LocalDate와 LocalTime 사용

/*
    LocalDate는 시간을 제외한 날짜를 표현하는 불변객체다.
*/

// 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 (윤년이 아님)

// 문자열로 인스턴스 말들기
LocalDate date = LocalDate.parse("2017-09-21");

// 현재 날짜 정보 얻기
LocalDate today = LocalDate.now();

// 시간 관련 객체에서 어떤 필드의 값에 접근할지 정의하는 인터페이스인 TemporalField를 정의하는
// 열거자 ChronoField를 get 메서드에 전달해서 정보 얻기
int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int day = date.get(ChronoField.DAY_OF_MONTH);

// 내장 메서드로 표현하기
int year = date.getYear();
int month = date.getMonthValue();
int day = date.getDayOfMonth();

 

/*
    LocalTime은 시간을 표현하는 클래스다.
*/

// LocalTime 만들고 값 읽기
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

// 문자열로 인스턴스 만들기
LocalTime time = LocalTime.parse("13:45:20");

 

날짜와 시간 조합

/*
    LocalDateTime은 LocalDate와 LocalTime을 쌍으로 갖는 복합 클래스다.
    날짜와 시간을 모두 표현하며 직접 LocalDateTime을 만들 수도 있고 날짜와 시간을 조합하는 방법도 있다.
*/

// LocalDateTime을 직접 만드는 방법과 날짜와 시간을 조합하는 방법
LocalDateTime dt1 = LocalDateTime.of(2017, Month.SEPTEMBER, 21, 13, 45, 20);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20); // 이전 파트의 LocalDate 인스턴스
LocalDateTime dt4 = date.atTime(time);      
LocalDateTime dt5 = time.atDate(date);       // 이전 파트의 LocalTime 인스턴스

// LocalDateTime 인스턴스에서 LocalDate와 LocalTime 인스턴스 추출하기
LocalDate date1 = dt1.toLocalDate(); // 2017-09-21
LocalTime time1 = dt1.toLocalTime(); // 13:45:20

 

Instant 클래스 : 기계의 날짜와 시간

/*
    기계가 이해할 수 있는 연속된 시간에서 특정 지점을 하나의 큰 수로 표현하는 Instant 클래스
    유닉스 에포크 시간(1970년 1월 1일 0시 0분 0초 UTC)를 기준으로 특정 지점까지의 시간을 초로 표현
*/

// Instant 클래스 인스턴스 생성하기. 모두 동일한 시간을 의미함
Instant.ofEpochSecond(3);                 // (초)를 넘겨줌
Instant.ofEpochSecond(3, 0);              // (초, 0 ~ 999,999,999 사이의 나노초)를 넘겨줌
Instant.ofEpochSecond(2, 1_000_000_000);  // 2초 이후의 1억 나노초(1초)
Instant.ofEpochSecond(4, -1_000_000_000); // 4초 이전의 1억 나노초(1초)

// Instant 클래스는 사람이 읽을 수 있는 시간 정보를 제공하지 않는다.
// UnsupportedTemporalTypeException: Unsupported field: DayOfMonth
// 대신 Duration, Period 클래스를 함께 활용할 수 있다.
int day = Instant.now().get(ChronoField.DAY_OF_MONTH);

 

Duration과 Period 정의

위에서 알아본 모든 클래스는 Temporal 인터페이스를 구현한다. Temporal 인터페이스는 특정 시간을 모델링하는 객체의 값을 어떻게 읽고 조작할지 정의한다. 이번에는 두 시간 객체 사이의 지속시간을 만들 수 있는 Duration 클래스에 대해 알아보자.

 

/*
    두 시간 객체 사이의 지속시간을 의미하는 duration
*/

// Duration 인스턴스 만들기
// 이때 사람이 사용하도록 만들어진 LocalDateTime과 기계가 사용하도록 만들어진 Instant는 혼용될 수 없다.
// 또한, Duration 클래스는 초와 나노초로 시간 단위를 표현하므로 between 메서드에 LocalDate를 사용할 수 없다.
Duration d1 = Duration.between(time1, time2);
Duration d1 = Duration.between(dateTime1, dateTime2);
Duration d2 = Duration.between(instant1, instant2);

// 제공하는 팩토리 메서드로 Duration 만들기
Duration threeMinutes = Duration.ofMinutes(3);
Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES);

 

/*
    두 LocalDate의 차이를 확인할 수 있는 Period 클래스
*/

// Period 인스턴스 만들기
Period tenDays = Period.between(LocalDate.of(2017, 9, 11),
                                LocalDate.of(2017, 9, 21));
                                
// 제공하는 팩토리 메서드로 Period 만들기
Period tenDays = Period.ofDays(10);
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);

 

아래 표는 Duration과 Period 클래스가 공통으로 제공하는 메서드다.

메서드 정적 설명
between 두 시간 사이의 간격을 생성
from 시간 단위로 간격을 생성
of 주어진 구성 요소에서 간격 인스턴스를 생성
parse 문자열을 파싱해서 간격 인스턴스를 생성
addTo 아니오 현재값의 복사본을 생성한 다음에 지정된 Temporal 객체에 추가
get 아니오 현재 간격 정보값을 읽음
isNegative 아니오 간격이 음수인지 확인함
isZero 아니오 간격이 0인지 확인함
minus 아니오 현재값에서 주어진 시간을 뺀 복사본을 생성함
multipliedBy 아니오 현재값에 주어진 값을 곱한 복사본을 생성
negated 아니오 주어진 값의 부호를 반전한 복사본을 생성
plus 아니오 현재값에 주어진 시간을 더한 복사본을 생성
subtractFrom 아니오 지정된 Temporal 객체에서 간격을 뺌

날짜 조정, 파싱, 포매팅

 

위에서 살펴본 클래스는 불변이다. 불변 클래스는 함수형 프로그래밍 그리고 스레드 안정성과 도메인 모델의 일관성을 유지하는 데 좋은 특징이다. 하지만 새로운 날짜와 시간 API에서는 변경된 객체 버전을 만들 수 있는 메서드를 제공한다.

// 절대적인 방식으로 LocalDate의 속성 바꾸기
// 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

// 상대적인 방식으로 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

 

메서드 정적 설명
from 주어진 Temporal 객체를 이용해서 클래스의 인스턴스를 생성
now 시스템 시계로 Temporal 객체를 생성
of 주어진 구성 요서에서 Temporal 객체의 인스턴스를 생성
parse 문자열을 파싱해서 Temporal 객체를 생성
atOffset 아니오 시간대 오프셋과 Temporal 객체를 합침
atZone 아니오 시간대 오프셋과 Temporal 객체를 합침
format 아니오 지정된 포매터를 이용해서 Temporal 객체를 문자열로 변환함(Instant는 지원하지 않음)
get 아니오 Temporal 객체의 상태를 읽음
minus 아니오 특정 시간을 뺀 Temporal 객체의 복사본을 생성
plus 아니오 특정 시간을 더한 Temporal 객체의 복사본을 생성
with 아니오 일부 상태를 바꾼 Temporal 객체의 복사본을 생성

 

TemporalAdjusters 사용하기

/*
	조금 더 복잡한 동작을 수행할 수 있도록 하는 기능을 제공하는 TemporalAdjuster
*/

// 미리 정의된 TemporalAdjusters 사용하기
import static java.time.temporal.TemporalAdjusters.*;

LocalDate date1 = LocalDate.of(2014, 3, 18);                // 2014-03-18
LocalDate date2 = date1.with(nextOfSame(DayOfWeek.SUNDAY)); // 2014-03-23
LocalDate date3 = date2.with(lastDayOfMonth());             // 2014-03-31

 

TemporalAdjusters 클래스의 팩토리 메서드

메서드 설명
dayOfWeekInMonth 서수 요일에 해당하는 날짜를 반환하는 TemporalAdjuster를 반환함(음수를 사용하면 월의 끝에서 거꾸로 계산)
firstDayOfMonth 현재 달의 첫 번째 날짜를 반환하는 TemporalAdjuster를 반환
firstDayOfNextMonth 다음 달의 첫 번째 날짜를 반환하는 TemporalAdjuster를 반환
firstDayOfNextYear 내년의 첫번째 날짜를 반환하는 TemporalAdjuster를 반환
firstDayOfYear 올해의 첫번째 날짜를 반환하는 TemporalAdjuster를 반환
firstInMonth 현재 달의 첫 번째 요일에 해당하는 날짜를 반환하는 TemporalAdjuster를 반환
lastDayOfMonth 현재 달의 마지막 날짜를 반환하는 TemporalAdjuster를 반환
lastDayOfNextMonth 다음 달의 마지막 날짜를 반환하는 TemporalAdjuster를 반환
lastDayOFNextYear 올해의 마지막 날짜를 반환하는 TemporalAdjuster를 반환
lastDayOfYear 현재 달의 마지막 요일에 해당하는 날짜를 반환하는 TemporalAdjuster를 반환
lastInMonth 현재 달에서 현재 날짜 이후로 지정한 요일이 처음으로 나타나는 날짜를 반환하는 TemporalAdjuster를 반환
previous 현재 날짜 이후로 지정한 요일이 처음/이전으로 나타나는 날짜를 반환하는 TemporalAdjuster를 반환함(현재 날짜도 포함)
nextOrSame 해당 날짜가 주어진 요일이면 그 날짜를 리턴하고 그렇지 않다면 그 이후의 가장 가까운 주어진 요일을 리턴
previousOrSame  

 

// TemporalAdjuster 인터페이스는 함수형 인터페이스
@FunctionalInterface
public interface TemporalAdjuster {
    Temporal adjustInto(Temporal temporal);
}

 

날짜와 시간 객체 출력과 파싱

// DateTimeFormatter로 문자열 만들기
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

// 문자열 파싱해서 날짜 객체 만들기
LocalDate date1 = LocalDate.parse("20140318", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2014-03-18", LocalTimeFormatter.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);

// 지역화된 DateTimeFormatter 만들기
DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d, MMMM yyyy", Locale.ITALIAN);
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date.format(italianForamtter); // 18. marzo 2014
LocalDAte date2 = LocalDate.parse(formattedDate, italianFormatter);

// DateTimeFormatter 만들기
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
	.appendText(ChronoField.DAY_OF_MONTH)
    .appendLiteral(". ")
    .appendText(ChronoField.MONTH_OF_YEAR)
    .appendLiteral(" ")
    .appendText(ChronoField.YEAR)
    .parseCaseInsensitive()
    .toFormatter(Locale.ITALIAN);

 

 


다양한 시간대와 캘린더 활용 방법

이번에는 다양한 시간대와 캘린더를 활용하는 방법을 살펴본다.

 

시간대 사용하기

시간대를 간단하게 처리하기 위해 기존의 java.util.TimeZone을 대체해서 나온 클래스가 java.time.ZoneId라는 불변 클래스이다.

// 해당 시간대의 규정 획득하기
// 지역 Id는 '지역/도시' 형식이다.
ZoneId romeZone = ZoneId.of("Europe/Rome");

// 기존의 TimeZone 객체를 ZoneId 객체로 변환하기
ZoneId zoneId = TimeZone.getDefault().toZoneId();

// 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 = instatn.atZone(romeZone);

// ZoneId를 이용해서 Instant를 LocalDateTime으로 바꾸기
Instant instant = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);

 

UTC/Greenwich 기준의 고정 오프셋

대안 캘린더 시스템 사용하기

위 두 내용은 제외한다.


CH13 디폴트 메서드

변화하는 API & 디폴트 메서드란 무엇인가?

기존의 자바에서는 인터페이스를 구현하는 경우 인터페이스 내에서 정의하는 모든 메서드 구현을 제공하거나 슈퍼클래스의 구현을 상속받아야 했다. 이 경우 인터페이스에 새로운 메서드를 추가하는 경우 모든 구현체에 해당 메서드를 추가해줘야 하는 문제가 발생한다. 자바 8에서는 이를 해결하기 위해 새로운 기능을 제공한다. 우리는 새로운 기능인 디폴트 메서드(Default method)에 대해 이번에 알아본다.

 

디폴트 메서드는 인터페이스의 기본 구현을 제공할 수 있도록 한다. 아래처럼 메서드 앞에 default 키워드를 붙여줌으로써 작성할 수 있다.

default void sort(Comparator<? super E> c) {
	Collections.sort(this, c);
}

 

디폴트 메서드를 사용하면 인터페이스를 구현하는 클래스를 모두 변경하지 않고도 공통적으로 원하는 기능을 인터페이스 내부에 추가할 수 있다.

 

참고) 바이너리 호환성, 소스 호환성, 동작 호환성
1. 바이너리 호환성: 뭔가를 바꾼 이후에도 에러 없이 기존 바이너리가 실행될 수 있는 상황. 예를 들어 인터페이스에 메서드를 추가했을 대 추가된 메서드를 호출하지 않는 한 문제가 일어나지 않는데 이를 바이너리 호환성이라고 함.
2. 소스 호환성: 코드를 고쳐도 기존 프로그램을 성공적으로 컴파일할 수 있음을 의미. 예를 들어 인터페이스에 메서드를 추가하면 소스 호환성이 아님. 추가한 메서드를 구현하도록 클래스를 고쳐야 하기 때문.
3. 동작 호환성: 코드를 바꾼 다음에도 같은 입력값이 주어지면 프로그램이 같은 동작을 실행한다는 의미. 예를 들어 인터페이스에 메서드를 추가하더라도 프로그램에 추가된 메서드를 호출할 일은 없으므로 동작 호환성은 유지됨.

 

 

디폴트 메서드 활용 패턴

디폴트 메서드를 이용하는 두가지 방식을 살펴보자.

 

첫번째는 선택형 메서드다.

기존에는 인터페이스를 구현하는 클래스에서 메서드의 내용이 비어있는 상황이 종종 있었다. 예를 들어 Iterator 인터페이스에는 hasNext, next, remove 메서드를 정의한느데 Remove 메서드는 잘 사용하지 않아 구현체에서 빈 상태로 제공되곤 했다. 여기서 디폴트 메서드를 이용하면 remove 같은 메서드의 기본 구현을 인터페이스 내에서 Defaul 키워드를 사용하여 제공할 수 있으므로 빈 구현을 제공할 필요가 없어진다.

 

두번째는 동작 다중 상속이다.

디폴트 메서드를 사용하면 기존에 불가능했던 동작 다중 상속 기능이 가능해진다. 자바에서 클래스는 한개만 상속받을 수 있지만 인터페이스는 여러개 구현할 수 있기 때문이다.

public class ArrayList<E> extends AbstractList<E> // 한개의 클래스를 상속받고
		implements List<E>, RandomAccess, Cloneable, Serializable { // 4개의 인터페이스를 구현
        ...
}

간단하게 사용예시를 들자면 다음과 같다.

우리는 rotatable, moveable, resizable한 monster를 만들고 싶고, moveable, resizable한 sun을 만들고 싶다고 하자. 다음과 같이 구현할 수 있다.

// Rotatable, Moveable, Resizable한 Monster
public class Monster implements Rotatable, Moveable, Resizable {
	...
}

// Moveable, Resizable한 Sun
public class Sun implements Moveable, Resizable {
	...
}

 

해석 규칙

이번에는 같은 시그니처를 갖는 디폴트 메서드를 상속받는 경우 어떤 메서드를 사용하게 되는지 알아보자.

 

아래와 같은 예제에서 무엇이 출력될까?

public interface A {
	default void hello() {
    	System.out.println("hello from A");
    }
}

public interface B extends A {
	default void hello() {
    	System.out.println("hello from B");
    }
}

public class C implements B, A {
	public static void main(String... args) {
    	new C().hello(); // 무엇이 출력될까?
    }
}

 

우리는 3가지 해결 규칙으로 위의 답을 알 수 있다.

  1. 클래스가 항상 이긴다. 클래스나 슈퍼클래스에서 정의한 메서드라 디폴트 메서드보다 우선권을 갖는다.
  2. 1번 규칙 이외의 상황에서는 서브인터페이스가 이긴다. 상속관계를 갖는 인터페이스에서 같은 시그니처를 갖는 메서드를 정의할 때는 서브인터페이스가 이긴다. 즉, B가 A를 상속받는다면 B가 A를 이긴다.
  3. 여전히 디폴트 메서드의 우선순위가 결정되지 않았다면 여러 인터페이스를 상속받는 클래스가 명시적으로 디폴트 메서드를 오버라이드하고 호출해야 한다.

해결 규칙에서 알 수 있듯이 위의 예제의 경우 B가 A를 상속받았으므로 서브인터페이스인 B의 hello를 선택해서 결국 출력되는 값은 "hello from B"가 된다.

 

그렇다면 아래와 같은 경우는 어떻게 될까?

public interface A {
	default void hello() {
    	System.out.println("hello from A");
    }
}

public interface B {
	default void hello() {
    	System.out.println("hello from B");
    }
}

public class C implements B, A {}

이 경우에는 1, 2번을 적용할 수 없으므로 

"Error: class C inherits unrelated defaults for hello() from types B and A." 와 같은 에러가 난다.

 

따라서 아래와 같이 클래스 C에서 사용하려는 메서드를 명시적으로 선택해야 한다.

public class C ipmlements B, A {
	void hello() {
    	B.super.hello();
    }
}

 

마지막으로 다이아몬드 문제를 살펴보자. 이 문제는 다이어그램의 모양이 다이아몬드를 닮아 다이아몬드 문제라고 불린다.

public interface A {
	default void hello() {
            System.out.println("Hello from A");
        }
}
public interface B extends A {}
public interface C extends A {}
public class D implements B, C {
	public static void main(String... args) {
            new D().hello(); // 무엇이 출력될까?
        }
}

이 경우 구현된 메서드가 A에 밖에 없으므로 "Hello from A"가 출력된다.

 

public interface A {
	default void hello() {
    	System.out.println("Hello from A");
    }
}
public interface B extends A {
	default void hello() {
    	System.out.println("Hello from B");
    }
}
public interface C extends A {}
public class D implements B, C {
	public static void main(String... args) {
    	new D().hello(); // 무엇이 출력될까?
    }
}

이 경우 규칙 2에 의해 B의 "Hello from B"가 출력된다.

 

public interface A {
	default void hello() {
    	System.out.println("Hello from A");
    }
}
public interface B extends A {
	default void hello() {
    	System.out.println("Hello from B");
    }
}
public interface C extends A {
	default void hello() {
    	System.out.println("Hello from C");
    }
}
public class D implements B, C {
	public static void main(String... args) {
    	new D().hello(); // 무엇이 출력될까?
    }
}

이 경우 B와 C 중 어느하나 선택할 수 없으므로 둘 중 하나의 메서드를 명시적으로 호출해 줘야 한다.

 


CH14 자바 모듈 시스템

압력: 소프트웨어 유추

자바 모듈 시스템은 자바 9에 추가된 기능이다. 모듈화는 추론하기 쉬운 소프트웨어를 만드는데 도움을 준다. 추론하기 쉬운 소프트웨어에는 크게 두가지 장점이 있다.

 

관심사분리(Soc, Separation of concerns)는 컴퓨터 프로그램을 고유의 기능으로 나누는 동작을 권장하는 원칙이다. Soc 원칙을 적용하면 개별 기능을 따로 작업할 수 있으므로 팀이 쉽게 협업할 수 있고, 개별 부분을 재사용하기 쉽고, 전체 시스템을 쉽게 유지보수 할 수 있다는 장점이 있다.

 

정보 은닉은 세부 구현을 숨기도록 장려하는 원칙이다. 이렇게 하는 경우 프로그램의 어떤 부분을 바꿨을 때 다른 부분까지 영향을 미칠 가능성이 적어진다.

 

이제 본격적으로 자바 모듈 시스템에 대해 알아보자.

 

자바 모듈 시스템을 설계한 이유

자바 9의 모듈 시스템이 추가되기 전에는 모듈화에 한계가 있었다.

 

1. 패키지간 가시성 제어가 제한적이었다.

자바는 public, protected, 패키지 수준, private 이렇게 4가지 가시성 접근자가 있다. 하지만 한 패키지의 클래스와 인터페이스를 다른 패키지로 공개하려면 public으로 이를 선언해야 하는 등의 문제가 있었다.

 

 

자바는 클래스를 모두 컴파일한 다음 한 개의 JAR 파일에 넣고 클래스 경로에 이 JAR 파일을 추가해 사용한다. 그러면 JVM이 동적으로 클래스 경로에 정의된 클래스를 필요할 때 읽는다.

 

2. 클래스 경로에는 같은 클래스를 구분하는 버전 개념이 없다.

라이브러리를 사용할 때 버전을 지정할 수 없어 클래스 경로에 두가지 버전의 같은 라이브러리가 존재하는 경우 문제가 생긴다.

 

3. 클래스 경로는 명시적인 의존성을 지원하지 않는다.

각각의 JAR 안에 있는 모든 클래스는 classes라는 한 주머니로 합쳐지기 때문에 한 JAR가 다른 JAR에 포함된 클래스 집합을 사용하라고 명시적으로 의존성을 정의하는 기능을 제공하지 않는다. 이 때 클래스 경로 때문에 문제가 발생할 수 있다. 결국 JVM이 ClassNotFoundException 같은 에러를 발생시키지 않고 정상적으로 실행될 때까지 클래스 경로에 클래스 파일을 더하거나 제거해보는 수밖에 없다.

 

이외에도 JDK 등 다양한 곳에 한계가 존재했다.

 

자바 모듈: 큰 그림

새로운 자바 프로그램 구조 단위인 모듈은 자바 8에서 제공되었다. 모듈은 module이라는 새 키워드에 이름과 바디를 추가해서 정의한다. 모듈 디스크립터는 module-info.java라는 특별한 파일에 저장된다.

 

자바 모듈 시스템으로 애플리케이션 개발하기

간단하게 자바 모듈 시스템을 적용해보자.

 

기능

  • 파일이나 URL에서 비용 목록을 읽는다.
  • 비용의 문자열 표현을 파싱한다.
  • 통계를 계산한다.
  • 유용한 요약 정보를 표시한다.
  • 각 태스크의 시작, 마무리 지점을 제공한다.

클래스와 인터페이스(관심사분리)

  • Reader 인터페이스: 소스 위치에 따른 HttpReader, FileReader 등
  • Parser 인터페이스: JSON -> Domain 객체
  • SummaryCalculator 클래스: 통계 계산해서 요약 정보 객체 반환하는 클래스

모듈화

  • expenses.readers
  • expenses.readers.http
  • expenses.readers.file
  • expenses.parsers
  • expenses.parsers.json
  • expenses.model
  • expenses.statistics
  • expenses.application

이처럼 작게 나눠서 캡슐화한다면 초기 비용은 높아지지만 프로젝트가 점점 커지면서 캡슐화와 추론의 장점이 두드러진다.

 

이제 위에서 나눈 모듈을 자바 모듈 시스템에 적용해보자.

|-- expenses.application
 |-- module-info.java // 모듈 디스크립터. 모듈의 의존성 & 어떤 기능을 외부로 노출할지 정의
 |-- com
  |--example
   |--expenses
    |--application
     |--ExpensesApplication.java

 

// 어떤 폴더와 클래스 파일이 생성된 JAR에 포함되어있는지를 보여주는 결과가 출력
javac module-info.java com/example/expenses/application/ExpensesApplication.java -d target

java cvfe expenses-application.jar com.example.expenses.application.ExpensesApplication -C target

// 생성된 JAR를 모듈화 애플리케이션으로 실행
java --module-path expenses-application.jar --module expenses/com.example.expenses.application.ExpensesApplication

 

여러 모듈 활용하기

exports 구문으로 expenses.readers 모듈을 선언

exports 구문은 다른 모듈에서 사용할 수 있도록 특정 패키지를 공개 형식으로 만든다.

기본적으로 모듈 내의 모든 것은 캡슐화된다. 따라서 다른 모듈에서 사용할 수 있는 기능은 명시적으로 결정해야한다.

module expenses.readers {
    exports com.example.expenses.readers; // 모듈명이 아니라 패키지명들임
    exports com.example.expenses.readers.file;
    exports com.example.expenses.readers.http;
}

 

아래와 같이 두 모듈의 디렉터리를 구성했다.

|-- expenses.application
 |--module-info.java
 |-- com
  |-- example
   |--expenses
    |-- application
     |-- ExpensesApplication.java
     
     
|-- expenses.readers
 |-- module-info.java
 |-- com
  |-- example
   |--expenses
    |-- readers
     |-- Reader.java
    |-- file
     |-- FileReader.java
    |-- http
     |-- HttpReader.java

 

Requires 구문으로 의존하고 있는 모듈을 지정

module expenses.readers {
    requires java.base; // 패키지명이 아니라 모듈명임
    
    exports com.example.expenses.readers; // 모듈명이 아니라 패키지명들임
    exports com.example.expenses.readers.file;
    exports com.example.expenses.readers.http;
}

 

컴파일과 패키징

프로젝트를 설정하고 모듈을 정의했으므로 메이븐 등의 빌드 도구로 프로젝트를 컴파일해본다.

 

먼저 각 모듈에 pom.xml을 추가한다. 모듈은 독립적으로 컴파일되므로 각각이 한개의 프로젝트이기 때문이다. 또한, 전체 프로젝트 빌드를 조정할 수 있도록 모든 모듈의 부모 모듈에도 pom.xml을 추가한다.

 

예시로 하나의 모듈에 추가된 pom.xml은 다음과 같다.

여기서 확인할 수 있듯이 명시적으로 부모 모듈을 지정해줬다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
     http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
     
     <groupId>com.example</groupId>
     <artifactId>expenses.readers</artifactId>
     <version>1.0</version>
     <packaging>jar</packaging>
     <parent>
     	<groupId>com.example</groupdId>
        <artifactId>expenses</artifactId>
        <version>1.0</version>
     </parent>
</project>

 

pom.xml 파일을 전부 정의했다면 mvn clean package 명령으로 프로젝트의 모듈을 아래와 같이 JAR로 만들 수 있다.

./expenses.application/target/expenses.application-1.0.jar
./expenses.readers/target/expenses.readers-1.0.jar

 

생성된 JAR를 아래와 같이 모듈 경로에 포함해서 모듈 애플리케이션을 실행할 수 있다.

java --module-path ./expenses.application/target/expensese.application-1.0.jar: ./expenses.readers/target/expenses.readers-1.0.jar --module expenses.application/com.example.expenses.application.ExpensesApplication

 

자동 모듈

위에서는 java.base를 참조하는 방법을 배웠다. 이번에는 외부 라이브러리를 참조하는 방법을 알아본다.

아파치 프로젝트의 httpclient와 같은 모듈화가 되어있지 않은 라이브러리를 추가한다고 가정해보자.

pom.xml 파일에 아래와 같이 추가한다.

<dependencies>
	<denpendency>
    	<groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.5.3</version>
    </dependency>
</denpendencies>

위와 같이 모듈화가 되어있지 않은 라이브러리를 추가하는 경우 자바는 JAR를 자동 모듈이라는 형태로 적절하게 변환한다. 모듈 경로상에 있으나 module-info 파일을 가지고 있지 않은 모든 JAR는 자동 모듈이 된다. 자동 모듈은 암묵적으로 자신의 모든 패키지를 노출시킨다.

 

// 자동 모듈의 이름 바꾸기
jar --file=./expenses.readers/target/dependency/httpclient-4.5.3.jar --describe-module httpclient@4.5.3 automatic

// httpclient JAR를 모듈 경로에 추가하고 애플리케이션을 실행
java --module-path ./expenses.application/target/expenses.application-1.0.jar: ./expensese.readers/target/expenses.readers-1.0.jar ./expenses.readers/target/dependency/httpclient-4.5.3.jar --module expenses.application/com.example.expenses.application.ExpensesApplication

 

모듈 정의와 구문들

이번에는 위에서 배운 requires, export 구문 외의 여러가지 구문을 배워본다.

// requires transitive: 다른 모듈이 제공하는 공개 형식을 한 모듈에서 사용할 수 있다고 지정할 수 있음
// 아래에서 com.iteratrlearning.application 모듈은 com.iteratrlearning.core에서 노출한 공개 형식에 접근할 수 있음.
module com.iteratrlearning.ui {
	requires transitive com.iteratrlearning.core;
    
    exports com.iteratrlearning.ui.panels;
    exports com.iteratrlearning.ui.widgets;
}

module com.iteratrlearning.application {
	requires com.iteratrlearing.ui;
}

// exports to: 사용자에게 공개할 기능을 제한함으로써 가시성을 좀 더 정교하게 제어함
// com.iteratrlearning.ui.widgets의 접근 권한을 가진 사용자의 권한을 com.iteratrlearning.ui.widgetuser로 제한할 수 있음.
module com.iteratrlearning.ui {
	requires com.iteratrlearning.core;
    
    exports com.iteratrlearning.ui.panels;
    exports com.iteratrlearning.ui.widgets to
    	com.iteratrlearning.ui.widgetuser;
}

// open: 모든 패키지를 다른 모듈에 반사적으로 접근을 허용할 수 있음.
// opens: 전체 모듈을 개방하지 않고도 개별 패키지만 개방할 수 있음.
open module com.iteratrlearning.ui {
}

// uses: 서비스 소비자를 지정.
// provides: 서비스 제공자를 지정.

 

728x90
반응형