모던 자바 인 액션 - 4장

Daniel_Yang·7일 전
0

CHAPTER 11 null 대신 Optional 클래스

NPE를 방지하고 더 명시적이고 안전한 API를 설계하기 위해 등장한 것이 바로 java.util.Optional 클래스

왜 null을 멀리해야 할까?

  • null은 참조형 변수에 값이 없다는 것을 의미하지만, 명확한 의미 전달이 어렵고 예외 발생 가능성이 크다.
  • 특히 연쇄적인 호출이 많은 도메인 모델에서는 if 조건문이 반복되는 "if문 지옥"으로...
  • data?.property처럼 ? (안전 내비게이션 연산자)로 안전하게 접근하는 방법이 없을까?

→ 그래서 등장한 것이 선택형값 Optional<T>. Optional은 "값이 있을 수도 있고, 없을 수도 있는 선택형 값을 캡슐화"한 클래스

Optional 특징

  • 최대 한 개의 값을 가지는 컬렉션

  • 값이 없을 때 Optional 반환하는 Optional.empty 는 Optional의 특별한 싱글톤 인스턴스 반환하는 정적 팩토리 메서드

  • 코드 간결화

    		Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
    
    		// null 체크 없이 이름 추출
    		Optional<String> name = optInsurance.map(Insurance::getName);

Optional 객체 만들기

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

// null이 아닌 값으로 Optional 만들기
Optional<Car> optCar = Optional.of(car); // car가 null이라면 즉시 NPE이 발생한다.

// null값으로 Optional 만들기
Optional<Car> optCar = Optional.ofNullable(car); // car가 null이면 빈 Optional 객체가 반환된다.

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

보통 객체의 정보를 추출할 때는 Optional을 사용할 때가 많다.
null을 확인하느라 조건 분기문을 추가해서 코드를 쉽게 이해할 수 있는 코드 가능

  • Map 반복 : 하지만 일반 map 연속되면 Optional<> 반복이므로 중첩
  • Flatmap : 중첩된 Optional 문제를 flatMap으로 해결
    - flatMap 연산으로 Optional을 평준화
    - 평준화: 두 Optional을 합치는 기능을 수행하면서 둘 중 하나라도 null이면 빈 Optional을 생성하는 연산. flatMap을 빈 Optional에 호출하면 아무 일도 일어나지 않고 그대로 반환된다.
// map()은 Optional에 값이 있을 때만 실행
// insurance가 null이면 map()은 실행되지 않고, 그대로 빈 Optional을 반환
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName); 

// 개선 시도 - 실패
Optional<String> name = 
	    optPerson.map(Person::getCar)  // Optional<Car>
             .map(Car::getInsurance)   // Optional<Optional<Insurance>>
             .map(Insurance::getName); // 컴파일 오류!

// flatMap() 사용 : Optional<Optional<Car>> → Optional<Car>
// 값이 없으면 자동으로 빈 Optional
public String getCarInsuranceName(Optional<Person> person) {
    return person.flatMap(Person::getCar)
                 .flatMap(Car::getInsurance)
                 .map(Insurance::getName)
                 .orElse("Unknown");
}

Optional을 도메인 모델 필드로 사용하면 안 되는 이유

  • 도메인 모델 클래스(예: Person, Car, Insurance)에서 Optional을 필드로 직접 사용하는 것은 직렬화(serialization) 관점에서 문제가 될 수 있다.
  • why?
    • Optional은 Java 8에서 "메서드 반환값에서 null을 안전하게 처리하기 위한 용도"로 만듬
    • Optional는 필드 형식으로 사용될 것을 가정하지 않았으므로 Serializable 인터페이스를 구현 x
    • 따라서 도메인 모델에 Optional을 사용한다면 직렬화 모델을 사용하는 도구나 프레임워크에서 문제가 생길 수 있다.
  • 방법: 만약 직렬화 모델이 필요하다면 변수는 일반 객체로 두되, Optional로 값을 반환받을 수 있는 메서드를 추가하는 방식이 권장된다.
public class Person {
	private Car car;
	public Optional<Car> getCarAsOptional() {
		return Optional.ofNullable(car);
	}
}

Optional + Stream 조합 활용

  • Optional.stream()이 추가되어 Stream과 결합이 쉽다.
    		// Optional::stream은 값이 있으면 1개의 스트림으로, 없으면 빈 스트림으로 바꿔준다.
    		public Set<String> getCarInsuranceNames(List<Person> persons) {
    		    return persons.stream()
    		                  .map(Person::getCar)
    		                  .map(optCar -> optCar.flatMap(Car::getInsurance))
    		                  .map(optIns -> optIns.map(Insurance::getName))
    		                  .flatMap(Optional::stream)  // Optional<String> → Stream<String>
    		                  .collect(Collectors.toSet());
    		}

Optional에 저장된 값을 확인하는 방법

// 피해야할 방법
optional.get(); // 값이 없으면 예외 발생!

// 안전한 방법
- orElse("기본값"): 값이 없을 때 기본값 리턴
- orElseGet(() -> "기본값"): 지연 계산 (Supplier 사용)
- orElseThrow(() -> new IllegalArgumentException()): 직접 예외 던지기
- ifPresent(value -> {...}): 값이 있을 때만 실행
- ifPresentOrElse(action, emptyAction): Java 9부터 제공

Optional에서 filter 사용해서 특정 값 거르기

// filter() 조건을 만족하지 않으면 빈 Optional을 반환
Optional<Insurance> optInsurance = ...;
optInsurance
    .filter(ins -> "CambridgeInsurance".equals(ins.getName()))
    .ifPresent(ins -> System.out.println("ok"));

Optional을 사용한 실용 예제

  • 잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기
  • 예외와 Optional 클래스
    • 예외가 발생할 가능성이 있는 연산을 Optional로 감싸면 훨씬 깔끔한 API를 제공 가능
      				// ex) 정수로 변환할 수 없는 문자열 문제를 빈 Optional로 해결 가능
      				public static Optional<Integer> stringToInt(String s) {
      					try {
      						return Optional.of(Integer.parseInt(s));
      					} catch (NumberFormatException e) {
      							return Optional.empty();
      					}
      				}
  • 기본형 Optional 사용 자제
    • 성능 개선 효과 미미 : Optional은 최대 1개의 값만 포함. 단일 값 처리에서는 박싱 오버헤드가 무시할 수준
    • 기능 제한 : 기본형 Optional(OptionalInt, OptionalLong 등)은 mapflatMapfilter와 같은 고차 함수를 지원 x
    • 호환성 문제 : 일반 Optional<T>와 기본형 Optional은 서로 변환 메서드가 없어 혼용 시 코드 복잡도 증가

추가

Optional을 잘못 사용하는 사례

잘못된 패턴대안
필드에 Optional 사용nullable 필드 + 게터에서 Optional 처리
Optional.of(null) 사용Optional.ofNullable 사용
Optional.get() 무조건 사용orElse, ifPresent 등 안전 API 사용
파라미터에 Optional 사용nullable 인자 + 내부 Optional 처리
List<Optional> 사용List 사용 + flatMap(Optional::stream)
직렬화 목적으로 Optional 사용DTO/엔티티에는 사용하지 않기

기본형 Optional 왜 만듬?

  • 박싱 비용 제거
  • 메모리 사용 최적화
    • 객체를 생성하지않는게 GC 부담 감소 및 더 적은 힙 메모리 사용.
  • 성능 민감한 코드에서 Null-Safety 보장
    • Java의 스트림 API 등에서 기본형 스트림(IntStream, DoubleStream)을 처리할 때,
      .reduce()와 같은 연산 결과를 OptionalInt로 반환할 수 있음.
    • 성능과 안전성 둘 다 확보

CHAPTER 12 새로운 날짜와 시간 API

기존에 에러가 많이 발생했던 날짜와 시간 관련 API
java.time 패키지는 LocalDate, LocalTime, LocalDateTime, Instant, Duration, Period 등 새로운 날짜와 시간에 관련된 클래스를 제공 => 불변 객체 => 스레드 안전성, 값 일관성

자바 8 이전의 날짜와 시간 API의 문제들

  • Date 클래스는 직관적이지 못하며 자체적으로 시간대 정보를 알고 있지 않다.
  • Date를 deprecated 시키고 등장한 Calendar 클래스 또한 쉽게 에러를 일으키는 설계 문제.
  • 날짜와 시간을 파싱하는데 등장한 DateFormat은 Date에만 지원되었으며, 스레드에 안전 X.
  • Date와 Calendar는 모두 가변 클래스이므로 유지보수가 아주 어렵다.

자바 8 이후의 날짜, 시간 API

LocalDate, LocalTime

LocalDate는 시간을 제외한 날짜를 표현한 불변 객체, LocalTime은 시간을 표혀한 불변 객체

  • 정적팩토리 메서드 of로 인스턴스 생성 가능
  • TemporalField는 시간 관련 객체에서 어떤 필드의 값에 접근할지 정의하는 인터페이스
    • ChronoField는 TemporalField의 구현체
      			// 다음 월요일 구하기
      			LocalDate nextMonday = LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.MONDAY));
  • parse 메서드에 DateTimeFormatter 전달 가능
  • LocalDateTime = LocalDate + LocalTime

Instant

  • java.time.Instant 클래스에서는 기계적인 관점에서 시간을 표현

위 클래스들은 Temporal 인터페이스를 구현하는데, Temporal 인터페이스는 특정 시간을 모델링하는 객체의 값을 어떻게 읽고 조작할지 정의한다.

Duration, Period

  • Duration 클래스를 사용하면 두 시간 객체 사이의 지속시간을 만들 수 있다
  • 초와 나노초로 시간 단위를 표현함
    - 년, 월, 일로 시간을 표현할 때는 Period 클래스를 사용!

Instant과 Duration 실용 예) API 응답 시간 측정

Instant start = Instant.now();
// some logic
Instant end = Instant.now();
Duration timeElapsed = Duration.between(start, end);
System.out.println("Elapsed time: " + timeElapsed.toMillis() + " ms");

날짜 조정, 파싱, 포매팅

날짜 조정

  • 시간, 날짜 클래스 불변이나 withAttribute 메서드를 사용하면 일부 속성이 수정된 상태의 새로운 객체를 반환 가능
    • get과 with 메서드로 Temporal 객체의 필드 값을 읽거나 수정 가능
  • TemporalAdjusters
    • 간단한 날짜 기능이 아닌 더 복잡한 날짜 조정기능이 필요할 때

날짜 파싱 및 포매팅

  • 날짜와 시간 객체 출력과 파싱
    • java.time.format 패키지가 이를 지원한다. 가장 중요하게 알아야 할 클래스는 DateTimeFormatter이다. 정적 팩토리 메서드와 상수를 이용해서 손쉽게 포매터를 만들 수 있다

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

  • ZoneId
    • 기존의 java.util.TimeZone을 대체 가능
    • 불변 클래스
    • ZoneId를 이용하면 서머타임 같은 복잡한 사항이 자동으로 처리
      ZoneId romeZone = ZoneId.of("Europe/Rome");

추가

Instant와 LocalDateTime의 차이

  • Instant는 UTC 기준의 절대적인 "순간"을 나타내므로, 시스템 간의 시간 비교와 정확한 경과 시간 측정에 매우 적합
  • LocalDateTime은 시간대 정보가 없기 때문에, 서버와 클라이언트가 다른 타임존에 있거나, 서버 자체의 타임존 설정이 바뀌면 정확한 비교가 불가능
구분InstantLocalDateTime
기준UTC(협정 세계시) 기준의 타임스탬프시간대 정보 없이 날짜와 시간만 표현
용도시스템 간 시간 비교, 기록, 연산, DB 저장사용자에게 보여주는 시간, 단일 리전 서비스, UI 표시 등
시간대 정보없음 (항상 UTC)없음 (로컬 시간, 타임존 불명확)
대표적 사용 예시API 응답 시간 측정, 로그 타임스탬프, 데이터 직렬화화면 표시, 특정 날짜/시간 입력 등
기계/사람 구분기계 친화적 (정확한 순간)사람 친화적 (달력/시계 개념)

불변 객체를 왤케 강조하는가?

  • 멀티스레드 환경에서 안전
    • 여러 스레드가 동시에 같은 객체를 읽더라도, 객체의 상태가 변하지 않으므로 동기화 작업 x 없이 안전한 공유 가능
  • 원본 변경 없이 메서드 체이닝으로 새로운 객체 생성 가능
    • 불변 객체의 메서드는 내부 상태를 바꾸지 않고, 항상 새로운 객체(복사본)를 반환 => 원본 잘못 건드림 x

CHAPTER 13 디폴트 메서드

디폴트 메서드가 무엇이며, 어떻게 디폴트 메서드로 변화할 수 있는 API를 만들 수 있는지
실용적인 디폴트 메서드 사용 패턴과 효과적으로 디폴트 메서드를 사용하는 방법
디폴트 메서드는 API 진화와 다중 상속 문제 해결의 열쇠 이다.

배경

기존 문제점

  • 자바 8 이전에는 인터페이스에 메서드를 추가하면, 그것을 구현한 모든 클래스에 구현을 강제해야 했다. 이는 유지보수나 API 확장 시 매우 불편하고 위험한 요소였다.

자바 8의 해결책

  • 자바 8은 다음 두 가지 방식으로 기본 구현을 인터페이스에 제공할 수 있게 했다
  1. 인터페이스 내부에 정적 메서드(static method) 사용
  2. 인터페이스의 기본 구현을 제공할 수 있도록 디폴트 메서드(default method) 기능 사용

정적 메서드 vs 디폴트 메서드

  • 정적 메서드는 유틸리티/헬퍼 메서드 제공, 객체 생성 없이 기능 제공. override 불가
  • 디폴트 메서드는 구현체에 "공통 동작의 기본값"을 제공하고 싶을 때. 구현 클래스에서 override 가능

결과적으로 기존 인터페이스를 구현하는 클래스는 자동으로 인터페이스에 추가된 새로운 메서드의 디폴트 메서드를 상속받게 된다. 이렇게 하면 기존의 코드 구현을 바꾸도록 강요하지 않으면서도 인터페이스를 바꿀 수 있다.


디폴트 메서드

  • 디폴트 메서드는 default 키워드를 사용하여 인터페이스 내부에 구현체가 포함된 메서드.
  • 이제 인터페이스는 자신을 구현하는 클래스에서 메서드를 구현하지 않을 수 있는 새로운 메서드 시그니처를 제공한다.
  • 덕분에 다음과 같은 특징을 갖는다
    • 소스 호환성 유지
      → 인터페이스에 새로운 메서드를 추가해도 기존 구현체는 영향을 받지 않음
    • 바이너리 호환성
      → 컴파일된 클래스 파일을 그대로 사용해도 문제 없음
    • 동작 호환성
      → 호출된 경우에만 디폴트 메서드의 동작이 적용됨

디폴트 메서드 vs 추상 클래스

항목추상 클래스인터페이스 (디폴트 메서드)
다중 상속 지원❌ 불가능✅ 가능
인스턴스 변수 사용✅ 가능(공통 상태)❌ 불가능 (상수만 가능)
생성자 사용✅ 가능❌ 불가능
메서드 구현✅ 일부 가능✅ default로 구현 가능

디폴트 메서드 활용 패턴

디폴트 메서드를 이용하는 두 가지 방식은 선택형 메서드(optional method)와 동작 다중 상속(multiple inheritance of behavoir)이다.

1. 선택형 메서드

  • 클래스가 꼭 구현하지 않아도 되는 기능을 인터페이스에 기본 제공할 수 있다.

    		// 자바 8의 Iterator 인터페이스
    		interface Iterator<T> {
    		    boolean hasNext();
    		    T next();
    
    			// `remove()`는 구현하지 않아도 된다.
    		    default void remove() {
    		        throw new UnsupportedOperationException();
    		    }
    		}
    		```

2. 동작 다중 상속

  • 기존 클래스가 여러 인터페이스에서 메서드를 상속받아도, 디폴트 메서드 덕분에 코드 중복 없이 기능을 재사용할 수 있다.
    		public class ArrayList<E> extends AbstractList<E>
    		    implements List<E>, RandomAccess, Cloneable, Serializable {
    		}

해석 규칙

  • 인터페이스는 다중 상속이 가능하다. 만약 같은 시그니처를 갖는 디폴트 메서드를 상속받는 경우 자바 컴파일러가 충돌을 어떻게 해결하는가?

1. 세가지 규칙

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

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

class C implements A, B {
    public void hello() {
        B.super.hello(); // 명시적으로 B 선택
    }
}
  1. 클래스가 항상 이긴다.
    • 클래스나 슈퍼클래스에서 정의한 메서드가 디폴트 메서드보다 우선한다.
  2. 1번 규칙 이외의 상황에서는 서브 인터페이스가 이긴다.
    • 상속관계를 갖는 인터페이스에서 같은 시그니처를 갖는 메서드를 정의할 때는 서브 인터페이스가 이긴다. (B 가 A 를 상속받으면 B 우선)
  3. 여러 인터페이스를 상속받는 클래스에서 명시적으로 호출
    • 디폴트 메서드의 우선순위가 정해지지 않았으면 클래스가 명시적으로 디폴트 메서드를 오버라이드하고 호출해야 한다.
    public class C implements B, A {
        void hello() {
            B.super.hello();
        }
    }

2. 발생할 수 있는 문제(주의점)

  • 충돌 그리고 명시적인 문제 해결
    - 인터페이스 간의 상속관계가 없어 2번 규칙을 적용할 수 없는 경우 자바 컴파일러는 컴파일 에러가 발생한다. 이럴 때 개발자는 사용하려는 메서드를 명시적으로 선택해야 된다
  • 다이아몬드 문제
    - 여러 인터페이스에서 같은 메서드를 상속받았을 때 충돌 가능성 존재

추가

추상클래스 > 디폴트 메서드 case

  • 상태를 가지는 동작(메서드) 구현이 필요한 경우 + 공통 상태(필드)-동작 함께 제공
  • 접근 제어자 및 보호된 메서드/필드가 필요한 경우
    • 인터페이스의 디폴트 메서드는 기본적으로 public만 허용
  • 여러 구현체가 동일한 로직, 필드, 유틸리티 메서드 등을 공유해야 할 때 추상 클래스가 유리

디폴트 메서드 과다 사용 시 문제

  • 모호한 메서드 해석(다이아몬드 문제 등)
  • 인터페이스 복잡성 증가 및 유지보수 어려움
    • 디폴트 메서드 과다 -> 원래 의도했던 "작고 명확한 역할"의 인터페이스 설계가 무너질 가능성 -> Interface Segregation Principle(인터페이스 분리 원칙) 위반 가능성 = 많은 책임
  • 구현체와 디폴트 메서드 간의 혼란

CHAPTER 14 자바 모듈 시스템

자바 모듈 시스템 도입 배경

  • 클래스, 패키지, JAR만으로는 소프트웨어의 구조적 복잡도를 관리하기에 부족.
  • SoC(관심사 분리)와 정보 은닉이 필요하지만 기존 Java에는 한계 존재.
  • 모듈화된 시스템 필요 → 자바 9에서 모듈 시스템 도입.

압력: 소프트웨어 유추

  • 앞 내용들을 이해하고 유지보수하기 쉬운 코드 but 저수준 코드
  • 하지만 소프트웨어 아키텍처(고수준) 에서는 기반 코드를 바꿔야할 때 유추하기 쉽게 생산성을 높일 수 있는 SW 프로젝트 필요

관심사분리(SoC, separation of concerns)

컴퓨터 프로그램을 고유의 기능으로 나누는 동작을 권장하는 원칙

  • SoC를 적용함으로 기능들을 모듈이라는 각각의 부분 즉, 코드 그룹으로 분리할 수 있다.
  • Soc 원칙은 모델, 뷰, 컨트롤러 같은 아키텍처 관점 그리고 복구 기법을 비즈니스 로직과 분리하는 등의 하위 수준 접근 등의 상황에 유용하다.

1. 개별 기능을 따로 작업할 수 있으므로 팀이 쉽게 협업할 수 있다.
2. 개별 부분을 재사용하기 쉽다.
3. 전체 시스템을 쉽게 유지보수할 수 있다.

정보은닉(information hiding)

캡슐화

  • 특정 코드 조각이 애플리케이션의 다른 부분과 고립되어 있음을 의미한다. 캡슐화된 코드의 내부적인 변화가 의도치 않게 외부에 영향을 미칠 가능성이 줄어든다
  • 하지만 자바 9 이전까지는 클래스와 패키지가 의도된 대로 공개되었는지를 컴파일러로 확인할 수 있는 기능이 없었다.

자바 소프트웨어에 적용한다면?

  • 관심사 분리
    - 자바는 객체 지향언로서 패키지, 클래스, 인터페이스로 코드 그룹화
  • 정보 은닉
    - 접근제한자 but 여전히 부족

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

1. 자바의 기존 모듈화 한계

  • 자바 9 이전에는 클래스, 패키지, JAR 수준의 그룹화만 제공됨.
  • 클래스 수준에서는 private 등으로 캡슐화 가능하지만, 패키지나 JAR 수준에서는 캡슐화 불가능.
  • 다른 패키지에 기능을 제공하려면 public으로 공개해야 했고, 이로 인해 내부 구현이 외부에 노출되는 문제 발생.

2. 클래스패스(classpath)의 한계

  • 클래스패스는 단순한 JAR 나열 방식이라 의존성 충돌 문제 발생 (ex. commons-lang 중복 로딩).
  • Maven/Gradle이 이를 어느 정도 해결하지만, 언어 수준의 의존성 제어는 불가능.

3. 무거운 JDK와 캡슐화 부족

  • 자바 개발 키트(JDK) 는 자바 프로그램을 만들고 실행하는데 도움을 주는 도구의 집합
  • JDK는 모든 API를 포함해 불필요하게 무거움.
  • 자바 8에서 도입한 Compact Profile로 일부 경량화했지만, 내부 API 노출 문제는 여전.

4. OSGi의 한계

  • 자바 9 이전에도 OSGi 같은 모듈 시스템이 있었지만, 복잡하고 표준이 아님.

자바 모듈 : 큰 그림

뒷 내용 개요

자바 9의 module 시스템은 기존 자바의 구조적 한계를 해결하기 위해 아래와 같은 기능을 도입함

  1. 명시적인 의존성 선언 (requires)
    → 모듈 간 관계를 컴파일 타임에 명확하게 정의
  2. 정확한 API 공개 범위 제어 (exports)
    → 어떤 패키지만 외부에 노출할지 명확히 지정 (캡슐화 강화)
  3. 캡슐화 강화
    → 모듈 외부에서 내부 구현 클래스를 직접 참조할 수 없도록 차단
  4. 더 작은 JDK 구성 가능 (런타임 경량화)
    → 필요 없는 모듈을 제거하고 필요한 모듈만 포함하는 구조 가능 (예: jlink)
  5. 모듈 간 충돌 방지
    → 같은 패키지를 여러 모듈에 동시에 정의할 수 없도록 하여 구조적 충돌 방지

1. 모듈

  • 자바 8에서 제공하는 자바 프로그램 구조 단위
  • module이라는 새 키워드에 이름과 바디를 추가해서 정의한다.

2. 모듈 디스크립터

  • module-info.java라는 특별한 파일에 저장된다.
  • 보통 패키지와 같은 폴더에 위치하며 한 개 이상의 패키지를 서술하고 캡슐화할 수 있지만 단순한 상황에서는 이들 패키지 중 한 개만 외부로 노출시킨다.


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

과정 설명

모듈화 설계

  • 초기에는 모듈 분리가 비용이 크지만, 규모가 커질수록 캡슐화와 유지보수성이 좋아짐.
  • 세부 vs. 거친 모듈화: 기능 단위로 작게 나눌수록 유연하지만 복잡도 증가.

자바 모듈 시스템의 기초

  • ex) 메인 애플리케이션을 지원하는 한 개의 모듈만 갖는 기본적인 모듈화 애플리케이션

    		|- expenses.application
    			|- module-info.java // 모듈 디스크리터(모듈의 소스 코드 파일 root 위치)
    			|- com
    				|- example 
    					|- expenses
    						|- application
    							|- ExpensesApplication.java
  • 모듈 기초 구조 : module-info.java

    • 모듈 = 패키지를 포함하는 논리적 단위
    • 어떤 패키지를 공개(export) 하고, 어떤 모듈에 의존(require) 하는지 명시
      			module expesnses.application {
      				// @@@
      			}
  • 모듈화 애플리케이션 실행

    • 모듈 소스 디렉토리에서 실행
    • (어떤 폴더와 클래스 파일이 생성된 JAR에 포함되어있는지 결과 출력)
    • 생성된 JAR를 모듈화 애플리케이션으로 실행

 모듈 간 상호작용

두 모듈 간의 상호작용은 자바 9에서 지정한 export, requires 이용해서 진행
=> 좀 더 정교하게 클래스 접근 제어 가능 => 제한 클래스만 공개, 한 모듈의 내에서만 공개 가능

exports 구문

  • exports는 다른 모듈에서 사용할 수 있도록 특정 패키지를 공개 형식으로 만든다.
    - 기본적으로 모듈 내의 모든 것이 캡슐화된다.
    - 모듈 시스템은 whitelist 기법을 이용해 강력한 캡슐화 제공 -> 다른 모듈에서 사용할 수 있는 기능이 무엇인지 명시해야한다.

requires 구문

  • 의조하고 있는 모듈 지정(java.base 외 모듈 import시)

모듈 이름 정하기

  • 오라클에서 패키지명처럼 인터넷 도메인명을 역순으로 지정하도록 권고
  • 노출된 주요 API 패키지와 이름이 같아야 한다는 규칙도 따라야 한다

컴파일과 패키징

  • 빌드 도구로 프로젝트 컴파일
    ex) maven
    • 각 모듈은 독립적으로 컴파일 되므로 부모 모듈과 함께 각 모듈에 pom.xml 을 추가한다
    • 올바른 모듈 소스 경로를 이용하도록 메이븐이 javac 설정을 한다.

자동 모듈

메이븐 컴파일러 플러그인은 module-info.java를 포함하는 프로젝트를 빌드할 때 모든 의존성 모듈을 경로에 놓아 적정한 JAR를 내려받고 이들이 프로젝트에 인식되도록한다.

  • 모듈화가 되어있지 않은 라이브러리를 사용할 경우 자바는 JAR를 자동 모듈이라는 형태로 적절하게 변환한다.
  • module-info.java 없는 JAR도 자동 모듈로 변환됨.
  • JAR 파일명을 기반으로 모듈 이름 생성됨.
  • 자동 모듈은 모든 패키지를 암묵적으로 공개하므로 보안/캡슐화에 주의.

모듈 정의와 구문들

  • requires
    - 컴파일 타임과 런타임에 한 모듈이 다른 모듈에 의존함을 정의하며, 모듈명을 인수로 받는다
  • exports
    - 지정한 패키지를 다른 모듈에서 이용할 수 있도록 공개 형식으로 만든다. 패키지명을 인수로 받는다
  • require transitive
    - 다른 모듈이 제공하는 공개 형식을 한 모듈에서 사용할 수 있다고 지정할 수 있다
  • exports to
    - 사용자에게 공개할 기능을 제한함으로 가시성을 좀 더 정교하게 제어할 수 있다
  • open과 opens
    - 모듈 선언에 open 한정자를 이용하면 모든 패키지를 다른 모듈에 반사적으로 접근을 허용할 수 있다
  • uses와 provides
    - 자바 모듈 시스템에서는 provides 구문으로 서비스를 제공하고, uses 구문으로 서비스를 소비하는 기능을 제공

추가

여전히 대부분의 프로젝트에서 module system을 안 쓰는 이유는?

Java Module System은 매우 이상적인 설계지만, 현실에서는 프레임워크·레거시·학습 비용에 의해 보편화되지 못함. 대부분의 실무에서는 classpath 기반 구조가 지배적
=> Java Module System(JPMS)을 사용하지 않는다면 현재의 프로젝트는 그냥 classpath 기반 구조

  1. 레거시 라이브러리 호환성 문제
  2. 빌드 툴 복잡성
  3. Spring 등 프레임워크의 반모듈적 구조
    • Spring이 의존하는 자동성, 리플렉션, 동적 프록시는 JPMS의 정적이고 제한적인 설계 철학과 맞지 않다.
  4. 학습 비용...
  5. 자바 생태계는 classpath 기반으로 20년 이상된 고인물...
Java Module System 도입 고려 기준
  • java 9(아마 java11부터 쓰겠죠?)만 지원하는 순수 java 애플리케이션
  • 보안, 캡슐화, 런타임 최적화가 중요

0개의 댓글