NPE를 방지하고 더 명시적이고 안전한 API를 설계하기 위해 등장한 것이 바로
java.util.Optional
클래스
null
은 참조형 변수에 값이 없다는 것을 의미하지만, 명확한 의미 전달이 어렵고 예외 발생 가능성이 크다.if
조건문이 반복되는 "if문 지옥"으로...data?.property
처럼 ? (안전 내비게이션 연산자)로 안전하게 접근하는 방법이 없을까?→ 그래서 등장한 것이 선택형값 Optional<T>
. Optional은 "값이 있을 수도 있고, 없을 수도 있는 선택형 값을 캡슐화"한 클래스
최대 한 개의 값을 가지는 컬렉션
값이 없을 때 Optional 반환하는 Optional.empty 는 Optional의 특별한 싱글톤 인스턴스 반환하는 정적 팩토리 메서드
코드 간결화
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
// null 체크 없이 이름 추출
Optional<String> name = optInsurance.map(Insurance::getName);
// 빈 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을 사용할 때가 많다.
null을 확인하느라 조건 분기문을 추가해서 코드를 쉽게 이해할 수 있는 코드 가능
// 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");
}
Person
, Car
, Insurance
)에서 Optional
을 필드로 직접 사용하는 것은 직렬화(serialization) 관점에서 문제가 될 수 있다.Optional
은 Java 8에서 "메서드 반환값에서 null을 안전하게 처리하기 위한 용도"로 만듬public class Person {
private Car car;
public Optional<Car> getCarAsOptional() {
return Optional.ofNullable(car);
}
}
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.get(); // 값이 없으면 예외 발생!
// 안전한 방법
- orElse("기본값"): 값이 없을 때 기본값 리턴
- orElseGet(() -> "기본값"): 지연 계산 (Supplier 사용)
- orElseThrow(() -> new IllegalArgumentException()): 직접 예외 던지기
- ifPresent(value -> {...}): 값이 있을 때만 실행
- ifPresentOrElse(action, emptyAction): Java 9부터 제공
// filter() 조건을 만족하지 않으면 빈 Optional을 반환
Optional<Insurance> optInsurance = ...;
optInsurance
.filter(ins -> "CambridgeInsurance".equals(ins.getName()))
.ifPresent(ins -> System.out.println("ok"));
// ex) 정수로 변환할 수 없는 문자열 문제를 빈 Optional로 해결 가능
public static Optional<Integer> stringToInt(String s) {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
map
, flatMap
, filter
와 같은 고차 함수를 지원 xOptional<T>
와 기본형 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/엔티티에는 사용하지 않기 |
IntStream
, DoubleStream
)을 처리할 때,.reduce()
와 같은 연산 결과를 OptionalInt
로 반환할 수 있음.기존에 에러가 많이 발생했던 날짜와 시간 관련 API
java.time 패키지는 LocalDate, LocalTime, LocalDateTime, Instant, Duration, Period 등 새로운 날짜와 시간에 관련된 클래스를 제공 => 불변 객체 => 스레드 안전성, 값 일관성
LocalDate는 시간을 제외한 날짜를 표현한 불변 객체, LocalTime은 시간을 표혀한 불변 객체
// 다음 월요일 구하기
LocalDate nextMonday = LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.MONDAY));
위 클래스들은 Temporal 인터페이스를 구현하는데, Temporal 인터페이스는 특정 시간을 모델링하는 객체의 값을 어떻게 읽고 조작할지 정의한다.
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");
ZoneId romeZone = ZoneId.of("Europe/Rome");
구분 | Instant | LocalDateTime |
---|---|---|
기준 | UTC(협정 세계시) 기준의 타임스탬프 | 시간대 정보 없이 날짜와 시간만 표현 |
용도 | 시스템 간 시간 비교, 기록, 연산, DB 저장 | 사용자에게 보여주는 시간, 단일 리전 서비스, UI 표시 등 |
시간대 정보 | 없음 (항상 UTC) | 없음 (로컬 시간, 타임존 불명확) |
대표적 사용 예시 | API 응답 시간 측정, 로그 타임스탬프, 데이터 직렬화 | 화면 표시, 특정 날짜/시간 입력 등 |
기계/사람 구분 | 기계 친화적 (정확한 순간) | 사람 친화적 (달력/시계 개념) |
디폴트 메서드가 무엇이며, 어떻게 디폴트 메서드로 변화할 수 있는 API를 만들 수 있는지
실용적인 디폴트 메서드 사용 패턴과 효과적으로 디폴트 메서드를 사용하는 방법
디폴트 메서드는 API 진화와 다중 상속 문제 해결의 열쇠 이다.
기존 문제점
자바 8의 해결책
정적 메서드 vs 디폴트 메서드
- 정적 메서드는 유틸리티/헬퍼 메서드 제공, 객체 생성 없이 기능 제공. override 불가
- 디폴트 메서드는 구현체에 "공통 동작의 기본값"을 제공하고 싶을 때. 구현 클래스에서 override 가능
결과적으로 기존 인터페이스를 구현하는 클래스는 자동으로 인터페이스에 추가된 새로운 메서드의 디폴트 메서드를 상속받게 된다. 이렇게 하면 기존의 코드 구현을 바꾸도록 강요하지 않으면서도 인터페이스를 바꿀 수 있다.
default
키워드를 사용하여 인터페이스 내부에 구현체가 포함된 메서드. 항목 | 추상 클래스 | 인터페이스 (디폴트 메서드) |
---|---|---|
다중 상속 지원 | ❌ 불가능 | ✅ 가능 |
인스턴스 변수 사용 | ✅ 가능(공통 상태) | ❌ 불가능 (상수만 가능) |
생성자 사용 | ✅ 가능 | ❌ 불가능 |
메서드 구현 | ✅ 일부 가능 | ✅ default로 구현 가능 |
디폴트 메서드를 이용하는 두 가지 방식은 선택형 메서드(optional method)와 동작 다중 상속(multiple inheritance of behavoir)이다.
클래스가 꼭 구현하지 않아도 되는 기능을 인터페이스에 기본 제공할 수 있다.
// 자바 8의 Iterator 인터페이스
interface Iterator<T> {
boolean hasNext();
T next();
// `remove()`는 구현하지 않아도 된다.
default void remove() {
throw new UnsupportedOperationException();
}
}
```
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable {
}
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 선택
}
}
public class C implements B, A {
void hello() {
B.super.hello();
}
}
public
만 허용SoC
(관심사 분리)와 정보 은닉
이 필요하지만 기존 Java에는 한계 존재.모듈 시스템
도입.컴퓨터 프로그램을 고유의 기능으로 나누는 동작을 권장하는 원칙
1. 개별 기능을 따로 작업할 수 있으므로 팀이 쉽게 협업할 수 있다.
2. 개별 부분을 재사용하기 쉽다.
3. 전체 시스템을 쉽게 유지보수할 수 있다.
캡슐화
private
등으로 캡슐화 가능하지만, 패키지나 JAR 수준에서는 캡슐화 불가능.public
으로 공개해야 했고, 이로 인해 내부 구현이 외부에 노출되는 문제 발생.자바 9의 module
시스템은 기존 자바의 구조적 한계를 해결하기 위해 아래와 같은 기능을 도입함
requires
)exports
)과정 설명
자바 모듈 시스템의 기초
ex) 메인 애플리케이션을 지원하는 한 개의 모듈만 갖는 기본적인 모듈화 애플리케이션
|- expenses.application
|- module-info.java // 모듈 디스크리터(모듈의 소스 코드 파일 root 위치)
|- com
|- example
|- expenses
|- application
|- ExpensesApplication.java
모듈 기초 구조 : module-info.java
module expesnses.application {
// @@@
}
모듈화 애플리케이션 실행
두 모듈 간의 상호작용은 자바 9에서 지정한 export, requires 이용해서 진행
=> 좀 더 정교하게 클래스 접근 제어 가능 => 제한 클래스만 공개, 한 모듈의 내에서만 공개 가능
메이븐 컴파일러 플러그인은 module-info.java를 포함하는 프로젝트를 빌드할 때 모든 의존성 모듈을 경로에 놓아 적정한 JAR를 내려받고 이들이 프로젝트에 인식되도록한다.
Java Module System은 매우 이상적인 설계지만, 현실에서는 프레임워크·레거시·학습 비용에 의해 보편화되지 못함. 대부분의 실무에서는 classpath 기반 구조가 지배적
=> Java Module System(JPMS)을 사용하지 않는다면 현재의 프로젝트는 그냥 classpath 기반 구조