Optional
#Java/adv3
옵셔널이 필요한 이유
NPE
- 자바에서
null은 값이 없음을 표현하는 기본적인 방법이다. - 하지만,
null을 잘못 사용하거나null참조에 대해 메서드를 호출하면NullPointerException(NPE)이 발생하여 프로그램이 예기치 않게 종료될 수 있다.- 메서드 뎁스가 깊고,
null체크를 누락하면 추척하기 어렵고, 디버깅 비용이 증가한다.
- 메서드 뎁스가 깊고,
가독성 저하
null을 반환하거나 사용하는 경우, 조건문을 통해 객체가 null인지 계속 확인해야 한다. 이러한null체크 로직이 누적되면 코드가 복잡해지고 가독성이 저하된다.
의도가 드러나지 않음
- 메서드 시그니처(
String findNameById(Long id))만 보고서는 이 메서드가null을 반환할 수도 있다는 사실을 명확히 알기 어렵다. - 메서드를 호출하는 입장에서는 "반드시 값이 존재할 것"이라고 가정했다가, 런타임에
null이 나와서 문제가 생길 수 있다.- ex)
String findNameById(Long id)보고findNameById(id).toUpperCase()를 호출했는데, 알고보니 null을 반환해서 NPE가 발생해버렸다.
- ex)
Optional의 등장
- 이러한 문제를 해결하고자 자바 8부터
Optional클래스를 도입했다. Optional은 "값이 있을 수도 있고 없을 수도 있음"을 명시적으로 표현해주어, 메서드의 계약(Contract)이나 호출 의도를 좀 더 분명하게 드러낸다.Optional을 사용하면 "빈 값"을 표현할 때, 더 이상null자체를 넘겨주지 않고Optional.empty()처럼 의도를 드러내는 객체를 사용할 수 있다.Optional을 사용함으로써 얻을 수 있는 이점null체크 로직이 간결해지고, NPE가 발생할 수 있는 부분을 컴파일러, IDE, 코드 리뷰어가 쉽게 파악할 수 있다.
null을 직접 반환하는 경우
String name = findNameById(id);// 결과 존재X 시 null 반환System.out.println("name = " + name.toUpperCase());- NPE 발생!!
if (name != null) System.out.println(id + ": " + name.toUpperCase()); else {...}null체크 로직을 빠트리면 NPE가 발생한다.
Optional을 반환하는 경우
// 이름이 있으면 이름을 대문자로 출력, 없으면 "UNKNOWN"을 출력해라.
static void findAndPrint(Long id) {
Optional<String> optName = findNameById(id);
// 다른 개발자, 클라이언트 입장에서는 반환타입이 Optional임을 보고 값이 있을 지 없을 지 명시적으로 알 수 있다.
String name = optName.orElse("UNKNOWN");
System.out.println(id + ": " + name.toUpperCase());
}
static Optional<String> findNameById(Long id) {
String findName = map.get(id);
Optional<String> optName = Optional.ofNullable(findName);
return optName;
}
Optional.ofNullable(findId)를 통해null이 될 수도 있는 값을Optional로 감싼다.- 메서드를 사용하는 클라이언트는 메서드 시그니처(
Optional<String> findNameById(Long id))만 보고도 "반환 결과가 있을 수도, 없을 수도 있겠구나"라고 명시적으로 인지할 수 있다. Optional.orElse("대체값"): 옵셔널에 값이 있으면 해당 값을 반환하고, 값이 없다면 대체값을 반환한다.- 이 방식은 "값이 없을 수도 있다"는 점을 호출하는 측에 명확히 전달하므로, 놓치기 쉬운
null체크를 강제하고 코드의 안정성을 높인다.
Optional 소개
java.util.Optional<T>는 "존재할 수도 있고 존재하지 않을 수도 있는" 값을 감싸는 일종의 컨테이너 클래스null을 내부적으로 다루는 대신,Optional객체에 감싸서Optional.empty()또는Optional.of(value)형태로 다룬다.- Optional 등장 이유
- "값이 없을 수 있다"는 상황을 프로그래머가 명시적으로 처리하도록 유도
- 런타임 NPE를 사전에 예방하기 위해 도입됨.
- Optional은 언제 사용하면 좋을까?
Optional은 "값이 없을 수도 있다"는 상황을 반환할 때 주로 사용된다.- "항상 값이 있어야 하는 상황"에서는
Optional을 사용할 필요 없이 그냥 해당 타입을 바로 사용하거나 예외를 던지는 방식이 더 좋을 수 있다.
Optional의 생성과 값 획득
Optional 생성 : 3가지 방법
Optional.of(T value)- 내부 값이 확실히
null이 아닐 때 사용.null을 전달하면NullPointerException발생
- 내부 값이 확실히
Optional.ofNullable(T value)- 값이
null일 수도 있고 아닐 수도 있을 때 사용.null이면Optional.empty()를 반환한다. Optional<String> opt2 = Optional.ofNullable("Hello!");Optional<String> opt3 = Optional.ofNullable(null);
- 값이
Optional.empty()- 명시적으로 "값이 없음"을 표현할 때 사용
Optional<String> opt4 = Optional.empty();
Optional 값 획득: Optional에 들어있는 값을 꺼내는 방법
isPresent()- 값이 있으면
true, 없으면false isEmpty(): 자바 11 이상에서 사용 가능, 값이 비어있으면true, 값이 있으면false를 반환
- 값이 있으면
get()- 값이 있는 경우 그 값을 반환, 값이 없으면
NoSuchElementException발생.
- 직접 사용 시 주의해야 하며, 가급적이면
orElse,orElseXxx계열 메서드를 사용하는 것이 안전.
- 값이 있는 경우 그 값을 반환, 값이 없으면
orElse(T other)- 값이 있으면 그 값을 반환, 값이 없으면
other를 반환.
- 값이 있으면 그 값을 반환, 값이 없으면
orElseGet(Supplier<? extends T> supplier)- 값이 있으면 그 값을 반환, 값이 없으면
supplier호출하여 생성된 값을 반환.
- 값이 있으면 그 값을 반환, 값이 없으면
orElseThrow(...)- 값이 있으면 그 값을 반환, 값이 없으면 지정한 예외를 던짐.
or(Supplier<? extends Optional<? extends T>> supplier)- 값이 있으면 해당 값의
Optional을 그대로 반환, 값이 없으면supplier가 제공하는 다른Optional반환 - 값 대신
Optional을 반환한다는 특징
- 값이 있으면 해당 값의
Optional<String> optValue = Optional.of("Hello");
Optional<String> optEmpty = Optional.empty();
// get() : 값이 없으면 예외 발생. 가급적 사용을 피하자.
String getValue = optValue.get();
//String getValue2 = optEmpty.get(); // 예외 발생 -> 주석 처리
// orElse() : 값이 없으면 지정된 기본값 사용
String value1 = optValue.orElse("기본값"); // Hello
String empty1 = optEmpty.orElse("기본값"); // 기본값
// orElseGet() : 값이 없으면 람다를 실행하여 기본값 생성
String value2 = optValue.orElseGet(() -> {
System.out.println("람다 호출 - optValue"); // 호출 안됨
return "New Value";
});
String empty2 = optEmpty.orElseGet(() -> {
System.out.println("람다 호출 - optEmpty"); // 호출됨
return "New Value";
});
// orElseThrow() : 값이 없으면 예외 발생
String value3 = optValue.orElseThrow(() -> new RuntimeException("값이 없습니다!"));
try {
// optEmpty는 값이 없으므로 예외 발생
String empty3 = optEmpty.orElseThrow(() -> new RuntimeException("값이 없습니다!"));
System.out.println("empty3 = " + empty3); // 실행되지 않음
} catch (Exception e) {
System.out.println("예외 발생: " + e.getMessage());
}
// or() : Optional 반환
Optional<String> result1 = optValue.or(() -> Optional.of("Fallback"));
System.out.println(result1); // Optional[Hello], 값이 이미 존재 -> 원본 그대로
Optional<String> result2 = optEmpty.or(() -> Optional.of("Fallback"));
System.out.println(result2); // Optional[Fallback], 비어있으므로 대체 Optional 반환
Optional 값 처리:
Optional 에서는 값이 존재할 때와 존재하지 않을 때를 처리하기 위한 다양한 메서드들을 제공한다
ifPresent(Consumer<? super T> action)- 값이 존재하면
action실행, 값이 없으면 아무것도 안 함 ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)- 값이 존재하면
action실행, 값이 없으면emptyAction실행
- 값이 존재하면
map(Function<? super T, ? extends U> mapper)- 값이 있으면
mapper를 적용한 결과(Optional<U>) 반환Optional<String>문자열 길이로 매핑Optional<Integer>
- 값이 없으면
Optional.empty()반환
- 값이 있으면
flatMap(Function<? super T, ? extends Optional<? extends U>> mapper)map과 유사하지만,Optional을 반환할 때 중첩되지 않고 평탄화(flat)해서 반환- Optional 안의 Optional을 하나의 Optional로 평탄화한다~
filter(Predicate<? super T> predicate)- 값이 있고 조건을 만족하면 그대로 반환,
- 조건 불만족이거나 비어있으면
Optional.empty()반환
stream()- 값이 있으면 단일 요소를 담은
Stream<T>반환, 값이 없으면 빈 스트림 반환
- 값이 있으면 단일 요소를 담은
- 값이 존재하면
Optional<String> optValue = Optional.of("Hello");
Optional<String> optEmpty = Optional.empty();
// ifPresent(): 값이 존재하면 Consumer 실행, 없으면 아무 일도 하지 않음
optValue.ifPresent(v -> System.out.println("optValue 값: " + v));
optEmpty.ifPresent(v -> System.out.println("optEmpty 값: " + v)); // 실행 X
// ifPresentOrElse():값이 있으면 Consumer 실행, 없으면 Runnable 실행
optValue.ifPresentOrElse(
v -> System.out.println("optValue 값: " + v),
() -> System.out.println("optValue는 비어있음")
);
optEmpty.ifPresentOrElse(
v -> System.out.println("optEmpty 값: " + v), // 실행 X
() -> System.out.println("optEmpty는 비어있음")
);
// map(): 값이 있으면 Function 적용 후 Optional로 반환, 없으면 Optional.empty()
Optional<Integer> lengthOpt1 = optValue.map(String::length); // Optional[5]
Optional<Integer> lengthOpt2 = optEmpty.map(String::length); // Optional.empty
// flatMap(): map()과 유사하나, 이미 Optional을 반환하는 경우 중첩을 제거
System.out.println("=== 4. flatMap() ===");
System.out.println("[map]");
// Optional[Hello] -> Optional[Optional[HELLO]] (중첩됨)
Optional<Optional<String>> nestedOpt = optValue.map(s -> Optional.of(s.toUpperCase()));
System.out.println("[flatMap]"); //flatMap을 사용하면 한 번에 평탄화
// Optional[Hello] -> Optional[HELLO]
Optional<String> flattenedOpt = optValue.flatMap(s -> Optional.of(s.toUpperCase()));
// filter(): 값이 있고 조건을 만족하면 그 값을 그대로, 불만족시 Optional.empty()
Optional<String> filtered1 = optValue.filter(s -> s.startsWith("H")); //Optional[Hello]
Optional<String> filtered2 = optValue.filter(s -> s.startsWith("X")); //Optional.empty
// stream(): 값이 있으면 단일 요소 스트림, 없으면 빈 스트림
optValue.stream().forEach(s -> System.out.println("optValue.stream() -> " + s));// 실행O
optEmpty.stream().forEach(s -> System.out.println("optEmpty.stream() -> " + s));// 실행X
Optional의 다양한 메서드를 활용하면, 값이 존재할 때와 존재하지 않을 때의 로직을 명확하고 간결하게 구현할 수 있다.
즉시 평가와 지연 평가1
orElse()와 orElseGet()의 차이를 이해하기 위해서는 즉시 평가와 지연 평가를 이해해야 한다.
- 즉시 평가(eager evaluation):
- 값(혹은 객체)을 바로 생성하거나 계산해 버리는 것
- 지연 평가(lazy evaluation):
- 값이 실제로 필요할 때(즉, 사용될 때)까지 계산을 미루는 것
로거 예제: 일반적인 상황에서는 로그를 남기지 않다가, 디버깅이 필요한 경우에만 디버깅용 로그를 추가로 출력
public class Logger {
private boolean isDebug = false;
// Getter, Setter
// DEBUG로 설정한 경우만 출력 - 데이터를 받음
public void debug(Object message) {
if (isDebug) {
System.out.println("[DEBUG] " + message);
}
}
}
debug()에 전달한 메시지는isDebug값을true로 설정한 경우에만 메시지를 출력한다.
public class LogMain1 {
public static void main(String[] args) {
Logger logger = new Logger();
logger.setDebug(true);
logger.debug(10 + 20); // 디버그 켜짐, 연산 결과 출력
System.out.println("=== 디버그 모드 끄기 ===");
logger.setDebug(false);
logger.debug(100 + 200); // 디버그 꺼짐, 연산 결과 출력 안됨
}
}
디버그 모드가 꺼져있는 경우
// 자바 언어의 연산자 우선순위상 메서드를 호출하기 전에 괄호 안의 내용이 먼저 계산된다.
logger.debug(100 + 200); // 1. 여기서는 100 + 200이 즉시 평가된다.
logger.debug(300); // 2. 100 + 200 연산의 평가 결과는 300이 된다.
debug(300) // 3. 메서드를 호출한다. 이때 계산된 300의 값이 인자로 전달된다.
public void debug(Object message = 300) { // 4. message에 계산된 300이 할당된다.
if (isDebug) { // 5. debug 모드가 꺼져있으므로 false이다.
System.out.println("[DEBUG] " + message); // 6. 실행되지 않는다.
}
}
앞서 계산한 100 + 200 연산은 어디에도 사용되지 않는다. 결과적으로 연산은 계산된 후에 버려진다.
즉시 평가와 지연 평가2
100 + 200이 실제로 연산되었는지 확인하기 위해 메서드로 분리하여 호출해보자.
public class LogMain2 {
public static void main(String[] args) {
Logger logger = new Logger();
logger.setDebug(true);
logger.debug(value100() + value200()); // 디버그 켜짐
System.out.println("=== 디버그 모드 끄기 ===");
logger.setDebug(false);
logger.debug(value100() + value200()); // 디버그 꺼짐
}
static int value100() {
System.out.println("value100 호출");
return 100;
}
static int value200() {
System.out.println("value200 호출");
return 200;
}
}
로그를 보면 디버그 모드를 끈 경우에도 value100(), value200() 이 실행된 것을 확인할 수 있다. debug() 메서드를 호출하기 전에 괄호 안의 내용이 먼저 평가(계산)되는 것을 확인할 수 있다.
그렇다면 debug 모드가 켜져있을 때는 해당 연산을 처리하고, debug 모드가 꺼져있을 때는 해당 연산을 처리하지 않으려면 어떻게 해야 할까? 가장 간단한 방법은 debug모드인 경우에만 debug()메서드를 호출하도록 if문을 사용하는 것이다.
if (logger.isDebug()) {
logger.debug(value100() + value200());
}
이렇게 하면 디버그 모드를 체크한 이후에 아무런 로그가 남지 않는다. debug()메서드가 실행되지 않은 것을 확인할 수 있다.
하지만 이렇게 하려면 디버그를 출력할 때 마다 계속 if 문을 사용해야 한다. 코드 한 줄을 작성하는데 코드 2줄이 더 필요하다! (if 문을 닫는 괄호 포함)
// 1. if 문을 통한 확실한 체크, 코드는 지저분해지지만, 필요 없는 연산 수행X
if (logger.isDebug()) {
logger.debug(value100() + value200());
}
// 2. 필요없는 연산이 추가되지만 코드는 깔끔
logger.debug(value100() + value200());
두 마리 토끼를 모두 잡을 수는 없을까? (2번의 코드의 깔끔함과 1번의 필요 없는 연산은 수행하지 않는 것)
이렇게 하려면 100 + 200, value100() + value200() 같은 연산을 정의하는 시점과 해당 연산을 실행하는 시점을 분리해야 한다. 그래서 연산의 실행을 최대한 지연해서 평가(계산)해야 한다.
즉시 평가와 지연 평가3
자바 언어에서 연산을 정의하는 시점과 해당 연산을 실행하는 시점을 분리하는 방법은 여러 가지가 있다.
- 익명 클래스를 만들고, 메서드를 나중에 호출
- 람다를 만들고, 해당 람다를 나중에 호출
람다를 사용해서 연산을 정의하는 시점과 실행하는 시점을 분리해서 문제를 해결해보자.
Logger에 람다(Supplier)를 받는 debug 메서드를 하나 추가하자
// supplier에 람다가 전달된다. (람다는 아직 실행되지 않았다.)
public void debug(Supplier<?> supplier = lambda) {
if (isDebug) { // 4. 디버그 모드이면 if 문이 수행된다. 디버그 모드가 아니면 람다는 실행되지 않는다.
// 5. supplier.get()을 실행하는 시점에 람다에 있는 value100() + value200()이 평가(계산)된다.
// 6. 평가 결과인 300을 반환하고 출력한다.
System.out.println("[DEBUG] " + supplier.get());
}
}
public class LogMain3 {
public static void main(String[] args) {
Logger logger = new Logger();
logger.setDebug(true);
logger.debug(() -> "Operation 1: " + (value100() + value200())); // 람다 전달
System.out.println("=== 디버그 모드 끄기 ===");
logger.setDebug(false);
logger.debug(() -> "Operation 2: " + (value100() + value200())); // 람다 전달 (실행 안됨)
}
람다를 사용해서 연산을 정의하는 시점과 실행(평가)하는 시점을 분리할 수 있고, 값이 실제로 필요할 때 까지 계산을 미룰 수 있다.
orElse() vs orElseGet()
이제 orElse()와 orElseGet()의 차이를 이해할 수 있을 것이다.orElse()는 보통 데이터를 받아서 인자가 즉시 평가되고, orElseGet()은 람다를 받아서 인자가 지연 평가된다.
public class OrElseGetMain {
public static void main(String[] args) {
Optional<Integer> optValue = Optional.of(100);
Optional<Integer> optEmpty = Optional.empty();
System.out.println("단순 계산");
// 10 + 20이 이미 계산된 후 orElse()가 호출된다. (즉시 연산)
Integer i1 = optValue.orElse(10 + 20); // 10 + 20 계산 후 버림
Integer i2 = optEmpty.orElse(10 + 20); // 10 + 20 계산 후 사용
// 값이 있으면 그 값, 없으면 지정된 기본값 사용 (orElse)
System.out.println("값이 있는 경우");
Integer value1 = optValue.orElse(createData()); // createData() 즉시 호출됨
System.out.println("value1 = " + value1);
System.out.println("값이 없는 경우");
Integer empty1 = optEmpty.orElse(createData()); // createData() 즉시 호출됨
System.out.println("empty1 = " + empty1);
// 값이 있으면 그 값, 없으면 지정된 람다 사용 (orElseGet)
System.out.println("값이 있는 경우");
Integer value2 = optValue.orElseGet(() -> createData()); // createData() 호출 안됨
System.out.println("값이 없는 경우");
Integer empty2 = optEmpty.orElseGet(() -> createData()); // createData() 호출됨
}
public static int createData() {
System.out.println("데이터를 생성합니다...");
try {
Thread.sleep(1000); // 데이터 생성에 1초 걸린다고 가정
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
int createValue = new Random().nextInt(100);
System.out.println("데이터 생성이 완료되었습니다. 생성 값: " + createValue);
return createValue;
}
}
orElse(createData())Optional에 값이 있어도createData()가 즉시 호출된다. 호출된 값은 버려진다.- 자바 연산 순서상
createData()를 호출해야 그 결과를orElse()에 인자로 전달할 수 있다.
orElseGet(() -> createData())Optional값이 있으면createData()가 호출되지 않는다.orElseGet()에 람다를 전달한다. 해당 람다는 이후에orElseGet()안에서 실행될 수 있다.Optional내부에 값이 있다면, 인자로 전달한 람다를 내부에서 실행하지 않는다.Optional내부에 값이 없다면, 인자로 전달한 람다를 내부에서 실행하고, 그 결과를 반환한다.
두 메서드의 차이
orElse(T other)는 "빈 값이면other를 반환"하는데,other를 "항상" 미리 계산한다.- 따라서
other를 생성하는 비용이 큰 경우, 실제로 값이 있을 때도 쓸데없이 생성 로직이 실행될 수 있다. orElse()에 넘기는 표현식은 호출 즉시 평가하므로 즉시 평가(eager evaluation)가 적용된다.
- 따라서
orElseGet(Supplier supplier)은 빈 값이면supplier를 통해 값을 생성하기 때문에, 값이 있을 때는supplier가 호출되지 않는다.- 생성 비용이 높은 객체를 다룰 때는
orElseGet()이 더 효율적이다. orElseGet()에 넘기는 표현식은 필요할 때만 평가하므로 지연 평가(lazy evaluation)가 적용된다.
- 생성 비용이 높은 객체를 다룰 때는
사용 용도orElse(T other)
- 값이
이미 존재할가능성이 높거나, 혹은 orElse()에 넘기는 객체(또는 메서드)가 생성 비용이 크지 않은 경우 사용해도 괜찮다. - 연산이 없는 상수나 변수의 경우 사용해도 괜찮다.
orElseGet(Supplier supplier)
- 주로 orElse()에 넘길 값의 생성 비용이 큰 경우, 혹은 값이 들어있을 확률이 높아 굳이 매번 대체 값을 계산할 필요가 없는 경우에 사용한다.
정리하면, 단순한 대체 값을 전달하거나 코드가 매우 간단하다면 orElse()를 사용하고, 객체 생성 비용이 큰 로직이 들어있고, Optional에 값이 이미 존재할 가능성이 높다면 orElseGet()을 고려해볼 수 있다.
실전 활용1 - 주소 찾기
public class User {
private String name;
private Address address; // 주소가 없을 수 있음
// 생성자, getter() 생략
package optional.model;
public class Address {
private String street;
// 생성자, getter() 생략
null을 직접 다루는 방식
// 사용자의 주소(street) 출력, 없으면 "Unknown"
static void printStreet(User user) {
String userStreet = getUserStreet(user);
if (userStreet != null) {
System.out.println(userStreet);
} else {
System.out.println("Unknown");
}
}
// 사용자의 주소(street) 반환, 없으면 null 반환
static String getUserStreet(User user) {
if (user == null) {
return null;
}
Address address = user.getAddress(); // Address가 null일 수 있음
if (address == null) {
return null;
}
return address.getStreet(); // Street 반환
}
- null 체크 구문이 자주 등장하고 복잡하다.
Optional로 개선
private static void printStreet(User user) {
getUserStreet(user).ifPresentOrElse(
System.out::println, // 값이 있을 때 출력
() -> System.out.println("Unknown") // 값이 없을 때 "Unknown" 출력
);
}
static Optional<String> getUserStreet(User user) {
return Optional.ofNullable(user) // user가 null일 수 있으므로 ofNullable 사용
.map(User::getAddress) // user.getAddress() 처리
.map(Address::getStreet); // address.getStreet() 처리
// 여기서 map 체이닝 중간에 null이면 Optional.empty()를 반환
}
- null 체크 구문이 사라지고 의도가 명확해졌다.
getUserStreet()메서드는 이제Optional<String>을 반환하므로, 호출 측에서ifPresentOrElse(),orElse(),orElseGet()등을 통해 안전하게 처리할 수 있다.- 여러
map()체이닝을 통해 내부에서null이 발생하면 자동으로Optional.empty()로 전환된다. - 참고로
Optional.ofNullable(user).map(u -> u.getAddress().getStreet());이건 불가능하다. Address가 null일 수도 있기 때문이다. (null.getStreet())
실전 활용2 - 배송
public class Order {
private Long id;
private Delivery delivery; // 배송 정보가 없을 수 있음
// 생성자, getter() 생략
public class Delivery {
private String status;
private boolean canceled; // 배송 취소 여부
// 생성자, getter() 생략
orderRepository 라는 맵에서 주문 정보를 찾은 다음, 배송 정보를 조회하여 출력하는 코드를 작성해보자.
- 배송 정보가
null이거나,canceled == true인 경우에는"배송X"를 출력한다.
public class DeliveryMain {
static Map<Long, Order> orderRepository = new HashMap<>();
static {
orderRepository.put(1L, new Order(1L, new Delivery("배송완료", false)));
orderRepository.put(2L, new Order(2L, new Delivery("배송중", false)));
orderRepository.put(3L, new Order(3L, new Delivery("배송중", true))); //취소됨.배송X
orderRepository.put(4L, new Order(4L, null)); // 배송 정보 없음. 배송X
}
public static void main(String[] args) {
System.out.println("1 = " + getDeliveryStatus(1L));
System.out.println("2 = " + getDeliveryStatus(2L));
System.out.println("3 = " + getDeliveryStatus(3L)); // 취소됨 -> 배송X
System.out.println("4 = " + getDeliveryStatus(4L)); // 배송 정보 없음 -> 배송X
}
static String getDeliveryStatus(Long orderId) {
return findOrder(orderId)
.map(Order::getDelivery) // Order -> Delivery
.filter(delivery -> !delivery.isCanceled())
.map(Delivery::getStatus) // Delivery -> String
.orElse("배송X"); // 값이 없으면 "배송X"
}
static Optional<Order> findOrder(Long orderId) {
return Optional.ofNullable(orderRepository.get(orderId));
}
}
- 주문 정보가
null이면Optional.empty() - 배송이 없거나 취소된 경우(
filter에서 걸러짐)에도Optional.empty()체이닝 - 최종적으로
.orElse("배송X")처리
옵셔널 - 베스트 프랙티스
Optional이 좋아보여도 무분별하게 사용하면 오히려 코드 가독성과 유지보수에 도움이 되지 않을 수 있다.Optional은 주로 메서드의 반환값에 대해 값이 없을 수도 있음을 표현하기 위해 도입되었다.- 여기서 핵심은 메서드의 반환값에
Optional을 사용하라는 것이다.
- 여기서 핵심은 메서드의 반환값에
1. 반환 타입으로만 사용하고, 필드에는 가급적 쓰지 말기
Optional은 주로 메서드의 반환값에 대해 "값이 없을 수도 있음"을 표현하기 위해 도입되었다.
- 클래스의 필드(멤버 변수)에
Optional을 직접 두는 것은 권장하지 않는다.Optional자체도 참조 타입이기 때문에, 혹시라도 개발자가 부주의로Optional필드에null을 할당하면, 그 자체가NullPointerException을 발생시킬 여지를 남긴다.
- 값이 없음을 명시하기 위해 사용하는 것이
Optional인데, 정작 필드 자체가null이면 혼란이 가중된다. - 만약
Optional로 필드 값을 받고 싶다면, 필드는Optional을 사용하지 않고, 반환하는 시점에Optional로 감싸주는 것이 일반적으로 더 나은 방법이다.
2. 메서드 매개변수로 Optional 을 사용하지 말기
- 자바 공식 문서에
Optional은 메서드의 반환값으로 사용하기를 권장하며, 매개변수로 사용하지 말라고 명시되어 있다. - 호출하는 측에서는 단순히
null전달 대신Optional.empty()를 전달해야 하는 부담이 생기며, 결국null을 사용하든Optional.empty()를 사용하든 큰 차이가 없어 가독성만 떨어진다. - 권장 예시
- 오버로드된 메서드를 만들거나,
- 명시적으로
null허용 여부를 문서화하는 방식을 택합니다.
3. 컬렉션(Collection)이나 배열 타입을 Optional 로 감싸지 말기
List<T>,Set<T>,Map<K, V>등 컬렉션(Collection) 자체는 비어있는 상태(empty)를 표현할 수 있으므로, Optional로 컬렉션을 감싸면Optional.empty()와 "빈 리스트"(Collections.emptyList()) 요렇게 이중 표현이 되어 혼란을 야기한다.- 컬렉션이 비어있는 경우를 고려한다면, Optional로 반환하는게 아니라 빈 컬렉션을 반환하자.
4. isPresent()와 get() 조합을 직접 사용하지 않기
Optional의get()메서드는 가급적 사용하지 않아야 한다.
if (opt.isPresent()) { ... opt.get() ... } else { ... }는 사실상null체크와 다를 바 없으며, 깜빡하면NoSuchElementException같은 예외가 발생할 위험이 있다.- 대신
orElse,orElseGet,orElseThrow,ifPresentOrElse,map,filter등의 메서드를 활용하면 간결하고 안전하게 처리할 수 있다.get()메서드는 가급적 사용하지 말고, 예제 코드나, 간단한 테스트에서만 사용하는 것을 권장한다.
- 어쩔 수 없이
get()메서드를 사용해야 하는 상황이라면, 이럴 때는 반드시isPresent()와 함께 사용하자.
5. orElseGet() vs orElse()차이를 분명히 이해하기
- 비용이 크지 않은(또는 간단한 상수 정도) 대체값이라면 간단하게
orElse()를 사용하자.
- 복잡하고 비용이 큰 객체 생성이 필요한 경우, 그리고 Optional 값이 이미 존재할 가능성이 높다면
orElseGet()를 사용하자.
6. 무조건 Optional 이 좋은 것은 아니다
Optional은 분명히 편의성과 안전성을 높여주지만, 모든 곳에서 "무조건" 사용하는 것은 오히려 코드 복잡성을 증가시킬 수 있다.
- 다음과 같은 경우
Optional사용이 오히려 불필요할 수 있다.- "항상 값이 있는" 상황
- 비즈니스 로직상
null이 될 수 없는 경우, 그냥 일반 타입을 사용하거나, 방어적 코드로 예외를 던지는 편이 낫다.
- 비즈니스 로직상
- "값이 없으면 예외를 던지는 것"이 더 자연스러운 상황
- 예를 들어, ID 기반으로 무조건 존재하는 DB 엔티티를 찾아야 하는 경우,
Optional대신 예외를 던지는 게 API 설계상 명확할 수 있다. 물론 이런 부분은 비즈니스 상황에 따라 다를 수 있다.
- 예를 들어, ID 기반으로 무조건 존재하는 DB 엔티티를 찾아야 하는 경우,
- "흔히 비는 경우"가 아니라 "흔히 채워져 있는" 경우
Optional을 쓰면 매번.get(),orElse(),orElseThrow()등 처리가 강제되므로 오히려 코드가 장황해질 수 있다.
- "성능이 극도로 중요한" 로우레벨 코드
Optional은 래퍼 객체를 생성하므로, 수많은 객체가 단기간에 생겨나는 영역(예: 루프 내부)에서는 성능 영향을 줄 수 있다. (일반적인 비즈니스 로직에서는 문제가 되지 않는다. 극한 최적화가 필요한 코드라면 고려 대상)
- "항상 값이 있는" 상황
클라이언트 메서드 vs 서버 메서드
사실 Optional 을 고려할 때 가장 중요한 핵심은 Optional을 생성하고 반환하는 서버쪽 메서드가 아니라, Optional 을 반환하는 코드를 호출하는 클라이언트 메서드에 있다. 결과적으로 Optional 을 반환받는 클라이언트의 입장을 고려해서 선택하자.
- "이 로직은
null을 반환할 수 있는가?" - "
null이 가능하다면, 호출하는 사람 입장에서 '값이 없을 수도 있다'는 사실을 명시적으로 인지할 필요가 있는가?" - "
null이 적절하지 않고, 예외를 던지는 게 더 맞진 않은가?"
Optional 기본형 타입 지원
다음과 같은 이유로 잘 안 쓴다.
Optional<T>와 달리map(),flatMap()등의 다양한 연산 메서드를 제공하지 않는다. 그래서 범용적으로 활용하기보다는 특정 메서드(isPresent(),getAsInt()등)만 사용하게 되어, 일반Optional<T>처럼 메서드 체인을 이어 가며 코드를 간결하게 작성하기 어렵다.
- 기존에 이미
Optional<T>를 많이 사용하고 있는 코드베이스에서, 특정 상황만을 위해OptionalInt등을 섞어 쓰면 오히려 가독성을 떨어뜨린다.
원시 타입 Optional을 고려해볼 만한 경우
- 예외적으로 미세한 성능을 극도로 추구하거나, 기본형 타입 스트림을 직접 다루면서 중간에
OptionalInt,OptionalLong,OptionalDouble을 자연스럽게 얻는 상황이라면 이를 사용하는 것도 괜찮다.
'Java > Modern Java(8~)' 카테고리의 다른 글
| [Java] 68. 병렬 스트림 (0) | 2025.07.04 |
|---|---|
| [Java] 67. 디폴트 메서드 (0) | 2025.07.04 |
| [Java] 65. 스트림 API3 - 컬렉터 (0) | 2025.07.04 |
| [Java] 64. 스트림 API2 - 기능 (0) | 2025.07.04 |
| [Java] 63. 스트림 API1 - 기본 (0) | 2025.07.04 |