Optional 소개
NullPointerException(NPE) 문제
> null을 잘못 사용하거나 null 참조에 대해 메서드 호출 시 NPE가 발생
> if(obj != null) {...} 코드 사용 시 코드가 복잡해지고 가독성이 떨어짐
Optional 등장
> 자바 8부터 도입한 클래스로 '값이 있을 수도 있고 없을 수도 있음'을 명시적으로 표현
> NPE을 사전에 예방하기 위해 도입됨
> 빈 값을 표현할 때 null 자체를 넘겨주지 않고 Optional.empty()처럼 의도를 드러내는 객체 사용
Optional의 생성과 값 획득
Optional 생성 메서드
1) Optional.of(T value)
: 내부 값이 확실히 null이 아닐 때 사용
: null 전달 시 NPE 발생
2) Optional.ofNullable(T value)
: 값이 null일 수도 있고 아닐 수도 있을 때 사용
: null이면 Option.empty() 반환
3) Optional.empty()
: 명시적으로 값이 없음을 표현할 때 사용
OptionalCreationMain.java
package optional;
import java.util.Optional;
public class OptionalCreationMain {
public static void main(String[] args) {
// 1. of() : 값이 null이 아님이 확실할 때 사용
// null이면 NPE 발생
String nonNullValue = "Hello Optional!";
Optional<String> opt1 = Optional.of(nonNullValue);
System.out.println("opt1 : " + opt1); // Optional[Hello Optional!]
// 2. ofNullable() : 값이 null일 수도 아닐 수도 있을 때
Optional<String> opt2 = Optional.ofNullable("Hello!");
Optional<String> opt3 = Optional.ofNullable(null);
System.out.println("opt2 : " + opt2); // Optional[Hello!]
System.out.println("opt3 : " + opt3); // Optional.empty
// 3. empty() : 비어있는 Optional을 명시적으로 생성
Optional<Object> opt4 = Optional.empty();
System.out.println("opt4 : " + opt4); // Optional.empty
}
}
Optional의 값을 확인하거나 획득하는 메서드
1) isPresent(), isEmpty()
: 값이 있으면 true, 없으면 false
: isEmpty()는 반대
2) get()
: 값이 있는 경우 그 값 반환
: 값이 없으면 NoSuchElementExcpeion 발생
: 직접 사용 시 주의해야 하며, 가급적이면 orElse, orElseXxx 계열 메서드를 사용하는 것이 안전
3) orElse(T other)
: 값이 있으면 그 값 반환
: 없으면 other를 반환
4) orElseGet(Supplier<? extends T> supplier)
: 값이 있으면 그 값을 반환
: 값이 없으면 supplier 호출해 생성된 값 반환
5) orElseThrow(...)
: 값이 있으면 그 값을 반환
: 없으면 지정한 예외를 던짐
6) or(Supplier<? extends Optional<? extends T>> supplier)
: 값이 있으면 해당 값의 Optional 그대로 반환
: 없으면 supplier가 제공하는 다른 Optional 반환
: 값 대신 Optional을 반환한다는 특징
OptionalRetrievalMain.jkava
package optional;
import java.util.Optional;
public class OptionalRetrievalMain {
public static void main(String[] args) {
// 문자열 "Hello"가 있는 Optional과 빈 Optional 준비
Optional<String> optValue = Optional.of("Hello");
Optional<String> optEmpty = Optional.empty();
// isPresent() : 값이 있으면 true
System.out.println("optValue.isPresent() : " + optValue.isPresent()); // true
// isEmpty() : 값이 없으면 true
System.out.println("optEmpty.isEmpty() : " + optEmpty.isEmpty()); // true
// get() : 직접 내부 값을 꺼내고 없으면 예외(NoSuchElementException)
String getValue = optValue.get();
System.out.println("getValue : " + getValue); // Hello
// String getValue2 = optEmpty.get();
// System.out.println("getValue2 : " + getValue2); // NoSuchElementException 발생
// orElse() : 값이 있으면 그 값, 없으면 지정 기본값 사용
String val1 = optValue.orElse("기본값");
System.out.println("val1 : " + val1); // Hello
String val2 = optEmpty.orElse("기본값");
System.out.println("val2 = : " + val2 ); // 기본값
// orElseGet() : 값이 없을 때만 람다(Supplier)가 실행되어 기본값 생성
String value1 = optValue.orElseGet(() -> {
System.out.println("람다 호출");
return "new Value";
});
System.out.println("value1 : " + value1); // Hello, 람다 호출
String value2 = optEmpty.orElseGet(() -> {
System.out.println("람다 호출");
return "new Value";
});
System.out.println("value2 : " + value2); // new Value
// orElseThrow() : 값이 있으면 반환, 없으면 예외 발생
String value3 = optValue.orElseThrow(() -> new RuntimeException("값이 없음!"));
System.out.println("value3 : " + value3); // Hello
// String value4 = optEmpty.orElseThrow(() -> new RuntimeException("값이 없음!"));
// System.out.println("value4 : " + value4); // RuntimeException 발생
// or() : Optional 반환
Optional<String> result1 = optValue.or(() -> Optional.of("Fallback"));
System.out.println("result1 : " + result1); // Optional[Hello]
Optional<String> result2 = optEmpty.or(() -> Optional.of("Fallback"));
System.out.println("result2 : " + result2); // Optional[Fallback]
}
}
Optional 값 처리
Optional 값 처리 메서드
1) ifPresent(Consumer<? super T> action)
: 값이 존재하면 action 실행
: 없으면 아무것도 안함
2) ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)
: 값이 존재하면 action 실행
: 없으면 emptyAction 실행
3) map(Function<? super T, ? extends U> mapper)
: 값이 있으면 mapper를 적용한 결과 반환
: 없으면 Optional.empty() 반환
4) flatMap(Function<? super T, ? extends Optional<? extends U>> mapper)
: map과 유사하지만, Optional 반환할 때 중첩되지 않고 평탄화해 반환
5) filter(Predicate<? super T> predicate)
: 값이 있고 조건 만족 시 그대로 반환
: 조건 불만족이거나 비어있으면 Optional.empty() 반환
5) stream()
: 값이 있으면 단일 요소를 담은 Stream<T> 반환
: 없으면 빈 스트림 반환
OptionalProcessingMain.java
package optional;
import java.util.Optional;
import java.util.stream.Stream;
public class OptionalProcessingMain {
public static void main(String[] args) {
Optional<String> optValue = Optional.of("Hello");
Optional<String> optEmpty = Optional.empty();
// ifPresent() : 값이 존재하면 Consumer 실행, 없으면 아무것도 안함
optValue.ifPresent(v -> System.out.println(v)); // Hello
optEmpty.ifPresent(v -> System.out.println(v)); // 아무것도 출력 X
// ifPresentOfElse() : 값이 있으면 Consumer 실행, 없으면 Runnalbe 실행
optValue.ifPresentOrElse(v -> System.out.println(v),
() -> System.out.println("비어있음")); // Hello
optEmpty.ifPresentOrElse(v -> System.out.println(v),
() -> System.out.println("비어있음")); // 비어있음
// map() : 값이 있으면 Function 적용 후 Optional로 반환, 없으면 Optional.empty()
Optional<Integer> length1 = optValue.map(s -> s.length());
System.out.println(length1); // Optional[5]
Optional<Integer> length2 = optEmpty.map(String::length);
System.out.println(length2); // Optional.empty
// flatMap() : map()과 유사하나 이미 Optional을 변환하는 경우 중첩 제거
Optional<Optional<Integer>> answer1 = optValue.map(s -> Optional.of(s.length()));
System.out.println(answer1); // Optional[Optional[5]]
Optional<Integer> flatAnswer1 = optValue.flatMap(s -> Optional.of(s.length()));
System.out.println(flatAnswer1); // Optional[5] -> 평탄화!
Optional<Integer> answer2 = optEmpty.flatMap(s -> Optional.of(s.length()));
System.out.println(answer2); // Optional.empty
// filter() : 값이 있고 조건을 만족하면 그 값을 그대로, 불만족시 Optional.empty()
Optional<String> filtered1 = optValue.filter(s -> s.startsWith("H"));
Optional<String> filtered2 = optValue.filter(s -> s.startsWith("X"));
System.out.println(filtered1); // Optional[Hello]
System.out.println(filtered2); // Optional.empty
// stream() : 값이 있으면 단일 요소 스트림, 없으면 빈 스트림
optValue.stream().forEach(s -> System.out.println(s)); // Hello
optEmpty.stream().forEach(s -> System.out.println(s)); // 출력 X
}
}
즉시 평가와 지연 평가
앞에서 살펴본 orElse()와 orElseGet()의 차이가 잘 느껴지지 않을 수도 있다.
이를 위해 즉시 평가와 지연평가를 이해해야 한다.
1. 즉시 평가(eager evaluation)
: 값(혹은 객체)을 바로 생성하거나 계산해 버리는 것
2. 지연 평가(lazy evaluation)
: 값이 실제로 필요할 때(사용될 때)까지 계산을 미루는 것
-> 이때 평가를 계산이라고 생각하면 된다.
Logger.java
package optional;
public class Logger {
private boolean isDebug = false;
public boolean isDebug() {
return isDebug;
}
public void setDebug(boolean debug) {
isDebug = debug;
}
// debug 설정한 경우만 출력
public void debug(Object msg) {
if(isDebug) {
System.out.println("[DEBUG] " + msg);
}
}
}
LogMain1.java
package optional;
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)은 디버거 코드가 꺼졌기에 출력되지 않는다.
-> 100 + 200 연산은 즉시 평가(바로 수행)되지만, 어디에도 사용되지 않으며 계산된 후에 버려진다.
LogMain2.java
package optional;
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());
// 코드 마지막에 추가
System.out.println("== 디버그 모드 체크 ==");
if(logger.isDebug()) {
logger.debug(value100() + value200());
}
}
static int value100() {
System.out.println("value100 호출");
return 100;
}
static int value200() {
System.out.println("value200 호출");
return 200;
}
}
실행 결과

-> 여기서도 value100() + value200() 연산은 미래에 전혀 사용하지 않을 값을 계산해 아까운 CPU만 낭비했다.
-> logger.isDebug()로 디버그 모드 체크를 할 수 있지만 코드의 길이가 길어진다.
람다를 사용해 연산을 정의하는 시점과 실행하는 시점을 분리해 문제를 해결할 수 있다.
Logger.java : Logger에 람다(Supplier)를 받는 debug 메서드 추가

LogMain3.java : 람다 사용
package optional;
public class LogMain3 {
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;
}
}
실행 결과

실행 분석
1) 디버거 모드가 켜져있을 때
> 람다 생성
logger.debug(() -> value100() + value200())
> debug()를 호출하면서 인자로 람다 전달
> supplier에 람다가 전달되며 디버거 코드이므로 if문 수행
public void debug(Supplier<?> supplier) {
if(isDebug) {
System.out.println("[DEBUG] " + supplier.get());
}
}
> supplier.get() 실행 시점에 value100() + value200()이 평가(계산)됨
> 평가 결과 반환 후 출력
2) 디버거 코드 껴져있을 때
> 람다 생성
> debug()를 호출하면서 인자로 람다 전달
> supplier에 람다 전달
> 디버그 모드가 아니므로 if문 수행 X
=> 람다를 사용해 연산을 정의하는 시점과 실행하는 시점을 분리했다.
orElse() vs orElseGet()
orElse()는 보통 데이터를 받아 인자가 즉시 평가되고, orElseGet()은 람다를 받아 인자가 지연 평가된다.
OrElseGetMain.java
package optional;
import java.util.Optional;
import java.util.Random;
public class OrElseGetMain {
public static void main(String[] args) {
Optional<Integer> optValue = Optional.of(100);
Optional<Integer> optEmpty = Optional.empty();
System.out.println("단순 계산");
Integer i1 = optValue.orElse(10 + 20); // 계산 후 버림
Integer i2 = optEmpty.orElse(10 + 20); // 계산 후 사용
System.out.println(i1); // 값이 있으므로 100 출력
System.out.println(i2); // 값이 없으므로 30 출력
// 값이 있으면 그 값, 없으면 지정된 기본값 사용
System.out.println("=== orElse ===");
Integer value1 = optValue.orElse(createData());
Integer value2 = optEmpty.orElse(createData());
System.out.println(value1); // 데이터 생성 ... 100
System.out.println(value2); // 데이터 생성 ... 4
System.out.println("=== orElseGet ===");
Integer result1 = optValue.orElseGet(() -> createData());
Integer result2 = optEmpty.orElseGet(() -> createData());
System.out.println(result1); // 100
System.out.println(result2); // 데이터 생성 ... 4
}
static int createData() {
System.out.println("데이터 생성 ...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return new Random().nextInt(100);
}
}
1) orElse(T other)
> 빈 값이면 other를 반환하는데 other를 항상 미리 계산함
> 호출 즉시 평가하므로 즉시 평가 적용
> 값이 존재하지 않을 가능성이 높거나 orElse()에 넘기는 객체가 생성 비용이 크지 않은 경우 사용
> 연산이 없는 상수나 변수일 경우 사용
2) orElseGet(Supplier supplier)
> 값이 있을 때는 supplier가 호출되지 않음
> 생성 비용이 높은 객체를 다룰 때 더 효과적
> 필요할 때만 평가하므로 지연 평가 적용
=> 단순한 대체 값을 전달하거나 코드가 간단하다면 orElse() 사용, 객체 생성 비용이 큰 로직이고 Optional에 값이 이미 존재할 가능성이 높다면 orElseGet() 고려
Optional - 베스트 프랙티스
Optional이 좋아보여도 무분별하게 사용하면 오히려 코드 가독성과 유지보수에 도움이 되지 않을 수 있다.
1. 반환 타입으로만 사용하고 필드에는 가급적 쓰지 말기
> Optional은 메서드 반환값에 대해 값이 없을 수도 있음을 표현하기 위해 도입됨
> 클래스의 필드에 Optional을 직접 두는 것은 권장 X
(-)
public class Product {
// 안티 패턴: 필드를 Optional로 선언
private Optional<String> name;
}
-> name이 null일수도, Optional.empty()일수도, Optional.of(value)일수도 있음
-> Optional 자체도 참조 타입이기 때문에 개발자가 부주의로 Optional 필드에 null을 할당하면 그 자체가 NPE 발생 여지를 남김
(+)
public class Product {
// 필드는 원시 타입(혹은 일반 참조 타입) 그대로 둔다.
private String name;
}
public Optional<String> getNameAsOptional() {
return Optional.ofNullable(name);
}
-> 반환 시점에 Optional로 감싸주는 것이 더 나은 방법
2. 메서드 매개변수로 Optional을 사용하지 말기
> 자바 공식 문서에 Optional은 메서드 반환값으로 사용하길 권장하며, 매개변수로 사용하지 말라고 명시되어 있음
> 호출쪽에서 null 전달 대신 Optional.empty()를 전달해야 하는 부담이 생기며 결국 null을 사용하든 Optional.empty()를 사용하든 큰 차이가 없어 가독성만 떨어짐
(-)
public void processOrder(Optional<Long> orderId) {
if (orderId.isPresent()) {
System.out.println("Order ID: " + orderId.get());
} else {
System.out.println("Order ID is empty!");
}
}
-> 호출하는 입장에서 processOrder(Optional.empty())처럼 호출해야 하는데, 이는 processOrder(null)과 큰 차이가 없고 오히려 Optional.empty()를 만드는 비용이 추가됨
(+)
// 1. 오버로드 예시
public void processOrder(long orderId) {
// 이 메서드는 orderId가 항상 있어야 하는 경우
System.out.println("Order ID: " + orderId);
}
public void processOrder() {
// 이 메서드는 orderId가 없을 때 호출할 경우
System.out.println("Order ID is empty!");
}
// 2. 방어적 코드
public void processOrder(Long orderId) {
if (orderId == null) {
System.out.println("Order ID is empty!");
return;
}
System.out.println("Order ID: " + orderId);
}
-> 오버로드된 메서드를 만들거나, 명시적으로 null 허용 여부를 문서화하는 방식을 택하는 것이 나음
3. 컬렉션이나 배열 타입을 Optional로 감싸지 말기
> List, Set, Map 등 컬렉션 자체는 비어있는 상태를 표현할 수 있음
> Optional<List T>> 처럼 다시 감싸면 Optional.empty()와 빈 리스트가 이중 표현이 되고 혼란을 야기함
(-)
public Optional<List<String>> getUserRoles(String userId) {
List<String> userRolesList ...;
if (foundUser) {
return Optional.of(userRolesList);
} else {
return Optional.empty();
}
}
// 반환받은 쪽
Optional<List<String>> optList = getUserRoles("someUser");
if (optList.isPresent()) {
// ...
}
-> 반환받은 내부 리스트가 empty일 수도 있어 한 번 더 체크해야 하는 모호함이 생김
(+)
public List<String> getUserRoles(String userId) {
if (!foundUser) {
// 권장: 빈 리스트 반환
return Collections.emptyList();
}
return userRolesList;
}
-> 빈 컬렉션을 반환하게
4. isPresent()와 get() 조합을 직접 사용하지 않기
> Optional의 get()은 가급적 사용하지 않아야 함
> 사실상 null 체크 구문과 다름이 없으며 깜빡하면 NoSuchElementException 예외 발생
> 대신 orElse, orElseGet, orElseThrow, isPresentOrElse, map, filter 등의 메서드 활용
5. orElsetGet() vs orElse() 차이 분명히 이해하기
> 비용이 크지 않은 대체값이라면 orElse() 사용
> 복잡하고 비용이 큰 객체 생성이 필요한 경우, Optional 값이 이미 존재할 가능성이 높다면 orElseGet() 사용
=> 무조건 Optional이 좋은 것은 아니다.
* 클라이언트 메서드 vs 서버 메서드
사실 Optional을 고려할 때 가장 중요한 핵심은 Optional을 생성하고 반환하는 서버쪽 메서드가 아닌, Optional을 반환하는 코드를 호출하는 클라이언트 메서드에 있다.
결과적으로 Optional을 반환받는 클라이언트의 입장을 고려해서 하는 선택이 가장 Optional을 잘 사용하는 방법이다.
Q. 이 로직은 null을 반환할 수 있는가?
Q. null이 가능하다면 호출하는 입장에서 '값이 없을 수도 있다'는 사실을 명시적으로 인지할 필요가 있는가?
Q. null이 적절하지 않고, 예외를 던지는게 더 맞지 않은가?
* Optional 기본형 타입 지원
OptionalInt, OptionalLong 등과 같은 기본형 타입의 Optional도 있지만 다음의 이유로 잘 사용되지 않는다.
1) Optional<T>와 달리 map(), flatMap()등 다양한 연산 메서드를 제공하지 않는다.
2) 기존 이미 Optional<T>를 많이 사용하고 있는 코드베이스에서 특정 상황만을 위해 OptionalInt 등을 섞어 쓰면 오히려 가독성을 떨어뜨린다.
기본형 Optional의 존재는 박싱/언박싱을 없애고 성능을 조금 더 높일 수 있다는 선택지를 제공하지만, 실제로는 별도로 쓰는게 좋을 정도로 성능 문제가 크게 나타나느냐?를 고민해야 함 -> 대부분의 경우 그렇지 않기에 잘 사용되지 않음
'인프런 > 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍' 카테고리의 다른 글
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 12. 병렬 스트림 (0) | 2025.07.03 |
|---|---|
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 11. 디폴트 메서드 (0) | 2025.06.30 |
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 9. 스트림 API 3 - 컬렉터 (0) | 2025.06.23 |
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 9. 스트림 API 2 - 기능 (0) | 2025.06.17 |
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 8. 스트림 API 1 - 기본 (3) | 2025.06.14 |