[Java 개념노트 60] Optional 클래스 이해하기
안녕하세요. Java 개념노트 시리즈 예순 번째 글입니다.
지난 글에서는 Stream 중간 연산과 최종 연산을 자세히 정리했습니다. 이번 글에서는 값이 있을 수도 있고 없을 수도 있는 상황을 안전하게 다루는 Optional 클래스에 대해 알아보겠습니다.
자바를 공부하다 보면 NullPointerException을 자주 만나게 됩니다. 참조변수가 실제 객체를 가리키지 않는 null 상태인데 메서드를 호출하면 프로그램이 실행 중 오류를 낼 수 있습니다. Optional은 이런 null 가능성을 더 명확하게 표현하고 안전하게 처리하기 위한 클래스입니다.
1. Optional이란?
Optional은 값이 있을 수도 있고 없을 수도 있는 상황을 표현하는 컨테이너 클래스입니다.
즉, 어떤 값이 반드시 존재한다고 가정하지 않고, 값이 없을 가능성까지 코드에서 명확하게 다루도록 도와줍니다.
쉽게 말하면Optional은 “값이 있을 수도 있고 없을 수도 있으니, 확인하고 안전하게 꺼내자”라는 뜻을 코드로 표현하는 상자입니다.
2. null이 위험한 이유
먼저 null 때문에 문제가 생기는 코드를 보겠습니다.
public class Main {
public static void main(String[] args) {
String name = null;
System.out.println(name.length());
}
}
실행하면 다음과 같은 예외가 발생할 수 있습니다.
Exception in thread "main" java.lang.NullPointerException
name은 실제 문자열 객체를 가리키지 않습니다. 그런데 length() 메서드를 호출했기 때문에 문제가 발생합니다.
3. null 체크로 처리하는 방식
기존에는 null 가능성이 있는 값은 직접 null 체크를 했습니다.
public class Main {
public static void main(String[] args) {
String name = null;
if (name != null) {
System.out.println(name.length());
} else {
System.out.println("이름이 없습니다.");
}
}
}
실행 결과는 다음과 같습니다.
이름이 없습니다.
이 방식도 틀린 것은 아닙니다. 하지만 코드 곳곳에 null 체크가 반복되면 코드가 길어지고, 체크를 빠뜨리면 오류가 발생할 수 있습니다.
4. Optional이 필요한 이유
Optional은 값이 없을 수 있다는 사실을 타입 자체로 드러냅니다.
Optional<String> name;
위 코드는 단순히 String 값이 아니라, String이 있을 수도 있고 없을 수도 있는 상태를 표현합니다.
| 방식 | 의미 |
String name |
문자열 값이라고 기대하지만 null일 수도 있음 |
Optional<String> name |
문자열 값이 없을 가능성을 명확하게 표현 |
핵심 포인트
Optional은 null을 완전히 없애는 마법이 아니라, 값이 없을 수 있다는 상황을 더 안전하고 명확하게 다루기 위한 도구입니다.
5. Optional import하기
Optional은 java.util 패키지에 있습니다. 사용하려면 import가 필요합니다.
import java.util.Optional;
기본 구조는 다음과 같습니다.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional<String> optional = Optional.of("Java");
System.out.println(optional);
}
}
6. Optional.of()
Optional.of()는 null이 아닌 값을 Optional로 감쌀 때 사용합니다.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional<String> optional = Optional.of("Java");
System.out.println(optional);
}
}
실행 결과는 다음과 같습니다.
Optional[Java]
값 "Java"가 Optional 안에 들어 있습니다.
7. Optional.of()에 null을 넣으면?
Optional.of()에는 null을 넣으면 안 됩니다. null을 넣으면 NullPointerException이 발생합니다.
Optional<String> optional = Optional.of(null); // 오류 발생
값이 null일 가능성이 있다면 Optional.of()가 아니라 Optional.ofNullable()을 사용해야 합니다.
주의
값이 절대 null이 아니라고 확신할 때만 Optional.of()를 사용합니다. null 가능성이 있으면 Optional.ofNullable()을 사용합니다.
8. Optional.ofNullable()
Optional.ofNullable()은 값이 null일 수도 있을 때 사용합니다. 값이 있으면 값을 담은 Optional을 만들고, null이면 빈 Optional을 만듭니다.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
String name = null;
Optional<String> optional = Optional.ofNullable(name);
System.out.println(optional);
}
}
실행 결과는 다음과 같습니다.
Optional.empty
name이 null이기 때문에 빈 Optional이 만들어졌습니다.
9. Optional.empty()
Optional.empty()는 값이 없는 빈 Optional을 직접 만들 때 사용합니다.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional<String> optional = Optional.empty();
System.out.println(optional);
}
}
실행 결과는 다음과 같습니다.
Optional.empty
값이 없는 상태를 명확하게 표현할 수 있습니다.
10. Optional 생성 방법 비교
Optional을 만드는 대표 방법은 다음과 같습니다.
| 메서드 | 사용 상황 | null 허용 |
Optional.of(value) |
값이 반드시 있을 때 | 허용하지 않음 |
Optional.ofNullable(value) |
값이 null일 수도 있을 때 | 허용 |
Optional.empty() |
빈 Optional을 만들 때 | 값 없음 |
11. isPresent()로 값이 있는지 확인하기
isPresent()는 Optional 안에 값이 있으면 true, 없으면 false를 반환합니다.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional<String> optional = Optional.of("Java");
System.out.println(optional.isPresent());
}
}
실행 결과는 다음과 같습니다.
true
Optional 안에 "Java"가 있으므로 true가 출력됩니다.
12. isEmpty()로 값이 없는지 확인하기
isEmpty()는 Optional 안에 값이 없으면 true를 반환합니다.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional<String> optional = Optional.empty();
System.out.println(optional.isEmpty());
}
}
실행 결과는 다음과 같습니다.
true
isEmpty()는 Java 11부터 사용할 수 있습니다. Java 8 기준으로 공부하고 있다면 !optional.isPresent() 형태를 사용할 수 있습니다.
13. get()으로 값 꺼내기
get()은 Optional 안의 값을 꺼낼 때 사용합니다.
Optional<String> optional = Optional.of("Java");
String value = optional.get();
System.out.println(value);
실행 결과는 다음과 같습니다.
Java
하지만 get()은 값이 없을 때 사용하면 예외가 발생합니다. 그래서 조심해야 합니다.
14. 빈 Optional에서 get()을 호출하면?
값이 없는 Optional에서 get()을 호출하면 NoSuchElementException이 발생할 수 있습니다.
Optional<String> optional = Optional.empty();
String value = optional.get(); // 오류 발생 가능
따라서 get()을 무작정 사용하는 것은 좋지 않습니다. 값이 없을 가능성이 있다면 orElse(), orElseGet(), orElseThrow() 같은 메서드를 사용하는 것이 더 안전합니다.
주의optional.get()은 값이 있을 때만 안전합니다. 값이 없을 수 있다면 기본값이나 예외 처리를 함께 고려해야 합니다.
15. orElse()로 기본값 사용하기
orElse()는 Optional 안에 값이 있으면 그 값을 사용하고, 값이 없으면 기본값을 사용합니다.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional<String> optional1 = Optional.of("Java");
Optional<String> optional2 = Optional.empty();
System.out.println(optional1.orElse("기본값"));
System.out.println(optional2.orElse("기본값"));
}
}
실행 결과는 다음과 같습니다.
Java
기본값
첫 번째 Optional에는 값이 있으므로 Java가 출력되고, 두 번째 Optional은 비어 있으므로 기본값이 출력됩니다.
16. orElseGet()으로 기본값 늦게 만들기
orElseGet()도 값이 없을 때 기본값을 제공합니다. 다만 기본값을 직접 넣는 것이 아니라, 기본값을 만들어주는 람다식이나 Supplier를 전달합니다.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional<String> optional = Optional.empty();
String result = optional.orElseGet(() -> "새로 만든 기본값");
System.out.println(result);
}
}
실행 결과는 다음과 같습니다.
새로 만든 기본값
() -> "새로 만든 기본값"은 값이 없을 때 실행되어 기본값을 제공합니다.
17. orElse()와 orElseGet() 차이
orElse()와 orElseGet()은 비슷하지만 중요한 차이가 있습니다.
| 메서드 | 특징 | 사용 상황 |
orElse() |
기본값을 바로 전달합니다. | 기본값이 간단할 때 |
orElseGet() |
값이 없을 때 Supplier를 실행합니다. | 기본값을 만드는 비용이 있거나 로직이 있을 때 |
String result1 = optional.orElse("기본값");
String result2 = optional.orElseGet(() -> "기본값");
기본값이 단순 문자열이면 orElse()도 충분합니다. 기본값을 만들기 위해 메서드를 호출해야 한다면 orElseGet()이 더 적합할 수 있습니다.
18. orElseThrow()로 예외 발생시키기
orElseThrow()는 값이 있으면 값을 꺼내고, 값이 없으면 예외를 발생시킵니다.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional<String> optional = Optional.empty();
String value = optional.orElseThrow(() -> new IllegalArgumentException("값이 없습니다."));
System.out.println(value);
}
}
실행하면 다음과 같은 예외가 발생할 수 있습니다.
java.lang.IllegalArgumentException: 값이 없습니다.
값이 반드시 있어야 하는 상황이라면 orElseThrow()로 명확하게 예외를 발생시킬 수 있습니다.
19. ifPresent()로 값이 있을 때만 실행하기
ifPresent()는 Optional 안에 값이 있을 때만 코드를 실행합니다. 값이 없으면 아무것도 실행하지 않습니다.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional<String> optional = Optional.of("Java");
optional.ifPresent(value -> System.out.println(value));
}
}
실행 결과는 다음과 같습니다.
Java
Optional 안에 값이 있으므로 람다식이 실행되었습니다.
20. ifPresentOrElse()
ifPresentOrElse()는 값이 있을 때와 없을 때의 동작을 나누어 작성할 수 있습니다.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional<String> optional = Optional.empty();
optional.ifPresentOrElse(
value -> System.out.println("값: " + value),
() -> System.out.println("값이 없습니다.")
);
}
}
실행 결과는 다음과 같습니다.
값이 없습니다.
ifPresentOrElse()는 Java 9부터 사용할 수 있습니다. Java 8 기준이라면 isPresent()와 else 흐름으로 처리할 수 있습니다.
21. map()으로 Optional 안의 값 변환하기
Optional의 map()은 Optional 안의 값을 다른 값으로 바꿀 때 사용합니다. 값이 있으면 변환하고, 값이 없으면 빈 Optional을 유지합니다.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional<String> optional = Optional.of("Java");
Optional<Integer> length = optional.map(value -> value.length());
System.out.println(length.orElse(0));
}
}
실행 결과는 다음과 같습니다.
4
문자열 "Java"가 길이값 4로 변환되었습니다.
22. Optional map()과 메서드 참조
람다식이 단순히 기존 메서드를 호출한다면 메서드 참조를 사용할 수 있습니다.
람다식
Optional<Integer> length = optional.map(value -> value.length());
메서드 참조
Optional<Integer> length = optional.map(String::length);
String::length는 Optional 안에 있는 문자열의 길이를 구하는 메서드 참조입니다.
23. filter()로 Optional 값 조건 검사하기
Optional의 filter()는 값이 조건을 만족하면 그대로 유지하고, 조건을 만족하지 않으면 빈 Optional로 만듭니다.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional<String> optional = Optional.of("Java");
Optional<String> result = optional.filter(value -> value.length() >= 4);
System.out.println(result.orElse("조건 불만족"));
}
}
실행 결과는 다음과 같습니다.
Java
"Java"는 길이가 4이므로 조건을 만족합니다. 따라서 값이 그대로 유지됩니다.
24. filter() 조건을 만족하지 않는 경우
조건을 만족하지 않으면 빈 Optional이 됩니다.
Optional<String> optional = Optional.of("SQL");
Optional<String> result = optional.filter(value -> value.length() >= 4);
System.out.println(result.orElse("조건 불만족"));
실행 결과는 다음과 같습니다.
조건 불만족
"SQL"은 길이가 3이므로 조건을 만족하지 않습니다. 그래서 빈 Optional이 되고, orElse()의 기본값이 출력됩니다.
25. Stream에서 Optional이 나오는 경우
Stream의 findFirst(), findAny(), min(), max() 같은 메서드는 Optional을 반환할 수 있습니다. 조건에 맞는 값이 없을 수 있기 때문입니다.
import java.util.ArrayList;
import java.util.Optional;
public class Main {
public static void main(String[] args) {
ArrayList<String> subjects = new ArrayList<>();
subjects.add("SQL");
subjects.add("JPA");
Optional<String> result = subjects.stream()
.filter(subject -> subject.length() >= 4)
.findFirst();
System.out.println(result.orElse("찾는 값 없음"));
}
}
실행 결과는 다음과 같습니다.
찾는 값 없음
길이가 4 이상인 문자열이 없기 때문에 빈 Optional이 반환되었습니다.
26. Optional 전체 예제
이번에는 회원 목록에서 특정 아이디를 가진 회원을 찾고, 이름을 안전하게 출력하는 예제를 보겠습니다.
import java.util.ArrayList;
import java.util.Optional;
public class Main {
public static void main(String[] args) {
ArrayList<Member> members = new ArrayList<>();
members.add(new Member("user01", "건"));
members.add(new Member("user02", "자바"));
Optional<Member> foundMember = findMemberById(members, "user03");
String result = foundMember
.map(member -> member.getName())
.orElse("회원 없음");
System.out.println(result);
}
public static Optional<Member> findMemberById(ArrayList<Member> members, String id) {
return members.stream()
.filter(member -> member.getId().equals(id))
.findFirst();
}
}
class Member {
private String id;
private String name;
public Member(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
}
실행 결과는 다음과 같습니다.
회원 없음
user03 아이디를 가진 회원이 없기 때문에 findFirst()는 빈 Optional을 반환합니다. 이후 map()은 실행할 값이 없으므로 건너뛰고, orElse()의 기본값이 사용됩니다.
27. Optional을 사용할 때의 흐름
Optional을 사용할 때는 보통 다음 흐름으로 생각하면 됩니다.
값이 있을 수도 없을 수도 있음
↓
Optional로 감싸기
↓
값이 있으면 map(), filter(), ifPresent() 처리
↓
값이 없으면 orElse(), orElseGet(), orElseThrow() 처리
핵심은 값을 바로 꺼내기보다, 값이 없을 때의 흐름까지 함께 작성하는 것입니다.
28. Optional 사용 시 자주 하는 실수
Optional을 처음 사용할 때는 아래와 같은 실수를 자주 합니다.
| 실수 | 문제점 | 해결 방법 |
Optional.of(null) 사용 |
NullPointerException 발생 | null 가능성이 있으면 ofNullable() 사용 |
빈 Optional에서 get() 호출 |
NoSuchElementException 발생 가능 | orElse(), orElseThrow() 등 사용 |
| Optional을 쓰고도 다시 null 체크처럼 사용 | Optional의 장점이 줄어듦 | map(), filter(), ifPresent() 활용 |
| 모든 곳에 Optional을 사용 | 코드가 불필요하게 복잡해질 수 있음 | 값이 없을 수 있는 반환값 중심으로 사용 |
| Optional을 필드에 무조건 사용 | 객체 설계가 복잡해질 수 있음 | 주로 메서드 반환 타입에서 사용 |
29. Optional은 언제 쓰면 좋을까?
Optional은 특히 메서드 반환값이 없을 수도 있을 때 사용하기 좋습니다.
public static Optional<Member> findMemberById(String id) {
// 회원을 찾으면 Optional.of(member)
// 회원이 없으면 Optional.empty()
}
이렇게 작성하면 이 메서드를 호출하는 쪽에서 “회원이 없을 수도 있구나”를 바로 알 수 있습니다.
반면 모든 변수, 모든 필드, 모든 매개변수에 Optional을 붙이는 것은 추천되지 않습니다. Optional은 주로 반환값의 부재 가능성을 표현할 때 가장 자연스럽습니다.
30. 직접 연습해보기
아래 코드를 직접 작성하고 실행해보세요.
import java.util.ArrayList;
import java.util.Optional;
public class Main {
public static void main(String[] args) {
ArrayList<Product> products = new ArrayList<>();
products.add(new Product("P001", "키보드", 50000));
products.add(new Product("P002", "마우스", 30000));
Optional<Product> foundProduct = findProductByCode(products, "P003");
String productName = foundProduct
.filter(product -> product.getPrice() >= 40000)
.map(Product::getName)
.orElse("조건에 맞는 상품 없음");
System.out.println(productName);
}
public static Optional<Product> findProductByCode(ArrayList<Product> products, String code) {
return products.stream()
.filter(product -> product.getCode().equals(code))
.findFirst();
}
}
class Product {
private String code;
private String name;
private int price;
public Product(String code, String name, int price) {
this.code = code;
this.name = name;
this.price = price;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
실행 결과는 다음과 같습니다.
조건에 맞는 상품 없음
상품 코드 P003에 해당하는 상품이 없기 때문에 빈 Optional이 반환됩니다. 그래서 마지막에 orElse()의 기본 문구가 출력됩니다.
31. 이번 글 정리
이번 글에서는 값이 있을 수도 없을 수도 있는 상황을 안전하게 다루는 Optional 클래스에 대해 정리했습니다. 핵심 내용은 다음과 같습니다.
Optional은 값이 있을 수도 없을 수도 있는 상황을 표현하는 클래스이다.- Optional은 null 가능성을 더 명확하게 다루기 위한 도구이다.
Optional.of()는 null이 아닌 값을 감쌀 때 사용한다.Optional.ofNullable()은 값이 null일 수도 있을 때 사용한다.Optional.empty()는 빈 Optional을 만든다.isPresent()는 값이 있는지 확인한다.isEmpty()는 값이 없는지 확인한다.get()은 값을 꺼내지만, 빈 Optional에서 호출하면 예외가 발생할 수 있다.orElse()는 값이 없을 때 기본값을 사용한다.orElseGet()은 값이 없을 때 Supplier로 기본값을 만든다.orElseThrow()는 값이 없을 때 예외를 발생시킨다.ifPresent()는 값이 있을 때만 코드를 실행한다.map()은 Optional 안의 값을 다른 값으로 변환한다.filter()는 Optional 안의 값이 조건을 만족하는지 검사한다.- Stream의
findFirst(),findAny(),min(),max()는 Optional을 반환할 수 있다. - Optional은 주로 값이 없을 수도 있는 메서드 반환 타입에 사용하면 좋다.
한 줄 요약Optional은 값이 없을 수 있는 상황을 명확하게 표현하고, 기본값 처리나 예외 처리까지 안전하게 작성하도록 도와주는 자바 클래스입니다.
다음 글 예고
다음 글에서는 [Java 개념노트 61] 파일 입출력 기본 이해하기라는 주제로 프로그램에서 파일을 읽고 쓰는 기본 흐름과 File, Reader, Writer 개념을 정리해보겠습니다.
GWDEVELBlog Java 개념노트 시리즈

'Computer Science > Java' 카테고리의 다른 글
| [Java 개념노트 62] BufferedReader와 BufferedWriter 자세히 보기 (0) | 2026.07.21 |
|---|---|
| [Java 개념노트 61] 파일 입출력 기본 이해하기 (0) | 2026.07.20 |
| [Java 개념노트 59] Stream 중간 연산과 최종 연산 자세히 보기 (1) | 2026.07.18 |
| [Java 개념노트 57] 메서드 참조 이해하기 (0) | 2026.07.17 |
| [Java ] 개념노트 56 함수형 인터페이스 이해하기 (0) | 2026.06.18 |