Day 28 — Optional: null을 안전하게 다루기
목차 27
지난 시간, 우리는 흐름에서 딱 하나를 집어오는 findFirst·findAny·max 를 만났어요. 그런데 이 친구들은 값을 그냥 건네주지 않았죠. "값이 있을 수도, 없을 수도 있는 상자"에 담아서 줬어요. 흐름이 비었거나 조건에 맞는 게 없으면 "빈 상자"가 나왔고요. 우리는 급한 대로 .orElse(기본값) 으로 그 상자를 열었어요.
특히 max 를 쓸 때가 좀 번거로웠어요. .orElse(null) 로 꺼낸 다음, top == null 인지 직접 확인해서 처리했잖아요. "값이 없을 수도 있으니 꺼낸 뒤에 또 확인" — 이 두 번 손이 가는 게 살짝 거슬렸죠.
오늘은 그 상자의 정식 이름을 배워요. 바로 Optional(옵셔널) 이에요. "이 값은 비어 있을 수도 있어"를 코드로 솔직하게 드러내서, null 때문에 프로그램이 갑자기 멈추는 사고를 막는 안전장치예요. 지난 시간 흘려둔 그 상자가 오늘의 출발점이에요. 시작해볼게요!
🎯 학습 목표
null이 왜 위험한지,NullPointerException(널 포인터 예외) 이 어떻게 터지는지 직접 보고 이해해요.Optional이 "비어 있을 수도 있는 값"을 어떻게 타입으로 드러내는지 알아요.Optional.of·ofNullable·empty로 상자를 만들 수 있어요.isPresent·isEmpty·ifPresent로 상자를 확인하고,get()이 왜 위험한지 알아요.orElse·orElseGet·orElseThrow로 값을 안전하게 꺼낼 수 있어요.map·flatMap·filter로 상자를 열지 않고 안에서 값을 다룰 수 있어요.- 저장소가
Optional을 돌려줄 때의 실전 패턴과,Optional을 쓰면 안 되는 자리(안티패턴)를 구분해요.
오늘의 로드맵
- Step 1 —
null이 일으키는 사고:NullPointerException직접 터뜨려보기. - Step 2 — 그 상자의 정식 이름,
Optional. - Step 3 — 상자 만들기:
of·ofNullable·empty. - Step 4 — 상자 확인하기:
isPresent·isEmpty·ifPresent(그리고get()의 위험). - Step 5 — 안전하게 꺼내기:
orElse·orElseGet·orElseThrow. - Step 6 — 상자 안에서 변환하기:
map. - Step 7 — 상자 속 상자 펴기:
flatMap과 거르기filter. - Step 8 — 실전: 저장소가 상자를 돌려주면.
- Step 9 — 이럴 땐 쓰지 마세요:
Optional안티패턴.
Step 1: null 이 일으키는 사고: NullPointerException 직접 터뜨려보기
Optional 을 배우기 전에, 먼저 "왜 이게 필요한가"부터 느껴봐야 해요. 그 출발점이 null 이에요.
null(널) 은 "값이 없음을 뜻하는 빈 자리"예요. 우리 Member 를 떠올려볼게요. 회원가입용 생성자(이름, 이메일)로 만들면 이메일이 채워져요. 그런데 숫자 정보만 받는 생성자(이름, 팔로워, 게시물, ...)로 만들면 이메일을 채우지 않아서, getEmail() 이 null 을 돌려줘요.
문제는 이 null 을 모르고 그냥 쓸 때 생겨요.
// com/instagram/javabasic/modern/NullProblemDemo.java
// 위험한 방식 — 이메일이 null 이면 .length() 를 부르는 순간 NullPointerException 이 터져요.
public static int unsafeEmailLength(Member member) {
return member.getEmail().length();
}
이메일이 있는 회원이라면 잘 동작해요. 그런데 이메일이 null 인 회원을 넘기면 어떻게 될까요? null.length() 를 부르는 셈이라, "빈 자리에 대고 동작을 시키는" 거예요. 이 순간 자바는 NullPointerException(줄여서 NPE) 을 던지고 프로그램이 그 자리에서 멈춰버려요.
member.getEmail() ─▶ null (이메일이 없는 회원)
│
└─ null.length() ─▶ 💥 NullPointerException!
(빈 자리에 대고 length() 를 부름)
💡 알아두면 좋은 이야기:
null을 처음 만든 개발자(토니 호어) 는 훗날 "그건 내 10억 달러짜리 실수였다"고 후회했어요. 그만큼null때문에 터지는 사고가 전 세계 프로그램에서 끝없이 반복됐다는 뜻이에요. 오늘 배우는Optional이 바로 그 사고를 줄이려고 나온 도구예요.
그래서 지금까지는 이렇게 막았어요. 쓰기 전에 null 인지 직접 확인하는 거죠.
// 방어한 방식 — null 인지 매번 직접 확인해요. 안전하지만, 이런 if 가 코드 곳곳에 번져요.
public static int safeEmailLength(Member member) {
String email = member.getEmail();
if (email == null) {
return 0;
}
return email.length();
}
이메일이 null 이면 0 을 돌려주고, 있으면 글자 수를 세요. 안전해요. 그런데 한 가지 불편이 있어요. 값을 쓰는 곳마다 이 if (email == null) 를 빠짐없이 적어야 해요. 한 군데라도 깜빡하면 거기서 NPE 가 터지고요. 그리고 더 무서운 건, 깜빡한 자리는 코드를 짤 때는 멀쩡해 보이다가 실제로 빈 값이 들어온 순간에야 터진다는 거예요.
"값이 없을 수도 있다"는 사실이 String 이라는 타입 어디에도 적혀 있지 않은 게 근본 문제예요. 받는 사람은 그게 null 일 수 있는지 알 길이 없으니, 매번 조심하거나 매번 깜빡하거나 둘 중 하나죠. 이 문제를 푸는 게 오늘의 Optional 이에요.
Step 2: 그 상자의 정식 이름, Optional
지난 시간 findFirst 가 돌려준 "있을 수도 없을 수도 있는 상자", 기억나시죠? 그 상자의 정식 이름이 Optional(옵셔널) 이에요. 영어 단어 그대로 "있을 수도, 없을 수도 있는(선택적인)" 이라는 뜻이에요.
Optional 은 값을 직접 들고 다니는 대신, 값을 한 겹 감싸는 상자예요. 상자는 두 가지 상태만 가져요.
[ 값이 있는 상자 ] Optional[jaehoon@example.com] ← 안에 값이 들어 있음
[ 빈 상자 ] Optional.empty ← 안이 비어 있음 (값 없음)
null 과 뭐가 다를까요? null 은 "값이 없음"을 그냥 빈 자리로 표현해서, 타입만 봐서는 비어 있을 수 있는지 전혀 알 수 없었어요. 반면 Optional<String> 이라는 타입을 보면, 받는 사람이 한눈에 알아요. "아, 이건 비어 있을 수도 있구나. 꺼내기 전에 확인해야겠다."
String email Optional<String> email
──────────── ──────────────────────
의미 값이거나 null 값이 든 상자거나 빈 상자
비어 있을 타입에 안 드러남 타입에 드러남
가능성 (받는 쪽이 모름) (받는 쪽이 바로 앎)
실수하면 런타임에 NPE 💥 컴파일러가 미리 막아줌
즉 Optional 의 핵심은 "이 값은 비어 있을 수도 있어"라는 경고를 타입 안에 박아두는 거예요. 그래서 받는 사람이 빈 경우를 처리하는 걸 깜빡하기 어려워져요. 지난 시간 max 가 Optional 을 돌려준 것도 같은 이유였어요. "흐름이 비면 줄 값이 없으니, 그 사실을 상자로 정직하게 알려준" 거죠.
이제 이 상자를 어떻게 만들고, 열고, 다루는지 하나씩 배워볼게요.
Step 3: 상자 만들기: of·ofNullable·empty
상자를 만드는 방법은 세 가지예요. 언제 무엇을 쓰는지가 중요해요.
// com/instagram/javabasic/modern/OptionalCreateDemo.java
// of — 이름은 항상 채워져 있으니 of 로 담아요. (of 에 null 을 넣으면 그 자리에서 NPE 가 나요.)
public static Optional<String> wrapUsername(Member member) {
return Optional.of(member.getUsername());
}
// ofNullable — 이메일은 null 일 수 있으니 ofNullable 로 담아요. null 이면 자동으로 빈 상자가 돼요.
public static Optional<String> wrapEmail(Member member) {
return Optional.ofNullable(member.getEmail());
}
// empty — 처음부터 비어 있는 상자가 필요할 때 써요.
public static Optional<String> emptyBox() {
return Optional.empty();
}
세 가지를 정리하면 이래요.
Optional.of(값) 값이 절대 null 이 아닐 때. null 넣으면 → 💥 NPE
Optional.ofNullable(값) 값이 null 일 수도 있을 때. null 이면 → 빈 상자
Optional.empty() 처음부터 빈 상자가 필요할 때
여기서 가장 헷갈리는 게 of 와 ofNullable 의 차이예요. 둘 다 값을 상자에 담는데, 차이는 "null 을 받아들이느냐"예요.
of는 "이 값은 절대null이 아니다"라고 약속하는 거예요. 만약null을 넣으면, 약속을 어긴 거라 그 자리에서 바로 NPE 가 나요. 그래서username처럼 항상 채워져 있는 값에만 써요.ofNullable은 "null일 수도 있어"라고 인정하는 거예요. 값이 있으면 든 상자,null이면 빈 상자를 알아서 만들어줘요. 그래서email처럼 비어 있을 수 있는 값에 써요.
⚠️ 헷갈리면
ofNullable이 안전해요: "이게null일 수 있나 없나" 가 애매할 땐ofNullable을 쓰면 돼요.of는 "절대null아님"이 확실할 때만. 반대로null인 줄 모르고of에 넣으면, NPE 를 막으려고 쓴Optional에서 오히려 NPE 가 나는 황당한 일이 생겨요.
상자를 그대로 출력해보면 상태가 글로 보여요. 값이 있으면 Optional[jaehoon@example.com], 비어 있으면 Optional.empty 처럼요. 이렇게 상자를 만들었으니, 다음은 이 상자를 어떻게 열어보는지예요.
Step 4: 상자 확인하기: isPresent·isEmpty·ifPresent
상자를 만들었으면, 이제 "값이 들어 있나?"를 확인하고 싶을 거예요. 가장 직관적인 방법부터 볼게요.
// com/instagram/javabasic/modern/OptionalCheckDemo.java
// isPresent — 상자에 값이 있으면 true.
public static boolean hasEmail(Member member) {
return Optional.ofNullable(member.getEmail()).isPresent();
}
// isEmpty — 상자가 비어 있으면 true. (isPresent 의 반대)
public static boolean missingEmail(Member member) {
return Optional.ofNullable(member.getEmail()).isEmpty();
}
isPresent(값이 있나?) 는 상자에 값이 있으면 true, 비어 있으면 false 예요. isEmpty(비어 있나?) 는 그 반대고요. 둘 중 읽기 편한 걸 쓰면 돼요.
값이 있을 때만 무언가를 하고 싶다면 ifPresent 가 깔끔해요.
// ifPresent — 값이 있을 때만 람다를 실행해요. 없으면 아무 일도 안 하고 조용히 넘어가요.
public static String ifPresentMessage(Member member) {
StringBuilder sb = new StringBuilder("회원 확인");
Optional.ofNullable(member.getEmail())
.ifPresent(email -> sb.append(" · 이메일 ").append(email));
return sb.toString();
}
ifPresent 는 상자에 값이 있을 때만 람다를 실행해요. 이메일이 있으면 " · 이메일 ..." 을 덧붙이고, 없으면 아무 일도 안 하고 조용히 넘어가요. if (email != null) { ... } 를 한 줄로 바꾼 셈이에요. 람다는 Day 25 에서 배운 그 문법 그대로고요.
그런데 여기서 꼭 짚고 넘어갈 게 하나 있어요. 상자에서 값을 곧장 꺼내는 get() 이라는 메서드예요.
// get — 빈 상자에 부르면 NoSuchElementException 이 터져요. 위험해서 권하지 않는 방식이에요.
public static String dangerousGet(Member member) {
return Optional.ofNullable(member.getEmail()).get();
}
get() 은 상자를 열어 값을 바로 꺼내요. 값이 있으면 잘 돌아가요. 그런데 빈 상자에 get() 을 부르면 NoSuchElementException(찾는 값이 없다는 예외) 이 터져요.
[ 값이 있는 상자 ] .get() ─▶ "jaehoon@example.com" (잘 동작)
[ 빈 상자 ] .get() ─▶ 💥 NoSuchElementException
어라, 이거 어디서 본 그림 같지 않나요? 맞아요. 처음에 null 을 모르고 쓰다가 NPE 가 터진 거랑 똑같아요. 상자만 한 겹 씌웠을 뿐, "확인 없이 꺼내면 터진다"는 문제가 그대로 남은 거죠. 그래서 get() 은 실무에서 거의 안 써요. 왜 위험한지는 Step 9 안티패턴에서 다시 정리할게요. 대신 "비어 있어도 안전하게" 꺼내는 방법을 다음 Step 에서 배워요.
🙋 학생 질문 — "위험한 get() 은 왜 아예 없애지 않았어요?"
get() 도 "이 상자는 절대 비어 있을 리 없다"가 확실한 드문 경우엔 쓸 수 있어요. 예를 들어 바로 윗줄에서 값을 넣어 만든 상자라면요. 하지만 그런 경우는 아주 드물고, 대부분은 "비어 있을 수도 있어서" Optional 을 쓰는 거잖아요. 그래서 get() 은 "정말 확실할 때만 여는 비상구"쯤으로 남겨두고, 평소엔 orElse·orElseThrow 처럼 빈 경우를 함께 정하는 메서드를 쓰는 게 안전해요.
Step 5: 안전하게 꺼내기: orElse·orElseGet·orElseThrow
get() 의 위험을 봤으니, 이제 진짜 제대로 된 꺼내기를 배워요. 핵심은 "비어 있을 때 어떻게 할지"를 함께 정하는 거예요. 세 가지가 있어요.
// com/instagram/javabasic/modern/OptionalExtractDemo.java
// orElse — 비어 있으면 미리 준비한 기본값을 줘요. (기본값은 상자가 차 있어도 늘 만들어져요.)
public static String emailOrDefault(Member member) {
return Optional.ofNullable(member.getEmail()).orElse("이메일 미등록");
}
// orElseGet — 비어 있을 때만 람다(Supplier)를 실행해 기본값을 만들어요. 만드는 비용이 클 때 유리해요.
public static String emailOrComputed(Member member) {
return Optional.ofNullable(member.getEmail())
.orElseGet(() -> "guest-" + member.getUsername() + "@temp.com");
}
// orElseThrow — 비어 있으면 우리가 만든 예외를 던져요. "없으면 진행 불가" 인 자리에 딱이에요.
public static String emailOrThrow(Member member) {
return Optional.ofNullable(member.getEmail())
.orElseThrow(() -> new MemberNotFoundException(
member.getUsername() + " 의 이메일이 등록돼 있지 않아요."));
}
지난 시간에 쓴 .orElse(기본값) 이 여기 다시 나왔죠? "값이 있으면 그 값, 없으면 기본값"이에요. 그런데 그 친구가 둘이나 더 있어요.
orElse(기본값)— 비어 있으면 준비한 기본값을 줘요. 가장 단순해요.orElseGet(() -> ...)— 비어 있을 때만 람다를 실행해 기본값을 만들어요. Day 25 에서 배운Supplier(공급자) 가 여기 쓰여요.orElseThrow(() -> 예외)— 비어 있으면 예외를 던져요. Day 21~24 에서 만든MemberNotFoundException을 여기서 다시 써요. "이 값이 없으면 더 진행할 수 없다"는 자리에 딱이에요.
orElse 와 orElseGet 은 뭐가 다를까?
둘 다 "비어 있으면 기본값"인데 왜 나뉘어 있을까요? 차이는 기본값을 언제 만드느냐예요.
orElse("기본값") 기본값을 항상 먼저 만들어 둠
→ 상자에 값이 있어도 기본값 계산은 이미 끝난 상태
orElseGet(() -> "기본값") 기본값은 상자가 비었을 때만 만듦
→ 상자에 값이 있으면 람다는 아예 실행 안 함
orElse 는 기본값을 항상 미리 준비해요. 상자에 값이 멀쩡히 들어 있어도, 안 쓸 기본값을 일단 만들어 둬요. 기본값이 그냥 "이메일 미등록" 같은 짧은 문자열이면 아무 문제 없어요.
그런데 기본값을 만드는 데 비용이 크다면? 예를 들어 기본값을 만들려고 데이터를 한참 조회하거나 계산해야 한다면, orElse 는 상자에 값이 있어도 그 계산을 매번 해버려서 낭비예요. 이럴 때 orElseGet 을 쓰면, 정말 비어 있을 때만 람다가 돌아서 헛수고를 안 해요.
💡 기억하기 쉽게: 기본값이 이미 있는 값(상수·짧은 문자열) 이면
orElse, 기본값을 그때 만들어야 하면orElseGet. 헷갈리면orElseGet이 항상 안전한 선택이에요.
orElseThrow 는 지난 며칠 배운 예외와 다시 만나요. 저장소에서 회원을 찾았는데 없으면, 빈 상자 대신 MemberNotFoundException 을 던져서 "그 회원 없어요"를 분명히 알리는 거죠. 이 패턴은 Step 8 에서 실전으로 다시 만나요.
Step 6: 상자 안에서 변환하기: map
지금까지는 상자에서 값을 꺼내는 데 집중했어요. 그런데 Optional 의 진짜 매력은 "상자를 열지 않고도 안의 값을 다룰 수 있다"는 거예요. 그 첫 번째 도구가 map(변환) 이에요.
// com/instagram/javabasic/modern/OptionalMapDemo.java
// 이메일 글자 수 — 상자 안 문자열을 길이(숫자)로 변환해요. 비면 빈 상자(변환 안 함).
public static Optional<Integer> emailLength(Member member) {
return Optional.ofNullable(member.getEmail()).map(String::length);
}
map 은 상자 안의 값을 다른 값으로 바꿔요. 여기서는 이메일 문자열을 글자 수(숫자) 로 바꿨어요. 결과도 상자예요. Optional<String> 이 Optional<Integer> 로 바뀌는 거죠.
Day 26 에서 배운 흐름의 map 과 똑같이 동작해요. 흐름의 map 은 여러 원소를 하나씩 변환했죠. Optional 의 map 은 원소가 0개(빈 상자) 또는 1개(값 있는 상자) 인 작은 흐름 위에서 똑같이 동작한다고 생각하면 돼요.
여기서 map 의 진짜 고마운 점이 나와요. 상자가 비어 있으면 변환을 그냥 건너뛰어요.
[ 값이 있는 상자 ] Optional["jaehoon@example.com"] .map(String::length) ─▶ Optional[19]
[ 빈 상자 ] Optional.empty .map(String::length) ─▶ Optional.empty
└ 변환을 건너뜀 (NPE 안 남!)
빈 상자에 map 을 걸어도 NPE 가 안 나요. 그냥 빈 상자가 그대로 나오죠. if (email != null) 확인 없이도 안전하게 변환되는 거예요. 이게 Optional 을 쓰는 큰 이유 중 하나예요.
변환한 다음 꺼내기까지 한 흐름으로 이어볼 수 있어요.
// 이메일 도메인 — @ 뒤를 잘라내요. 비면 기본값으로 마무리해요.
public static String emailDomain(Member member) {
return Optional.ofNullable(member.getEmail())
.map(email -> email.substring(email.indexOf("@") + 1))
.orElse("(도메인 없음)");
}
이메일에서 @ 뒤 도메인만 잘라냈어요. jaehoon@example.com 이면 example.com 이 나오고요. 이메일이 없으면? map 이 변환을 건너뛰어 빈 상자가 되고, 마지막 orElse 가 "(도메인 없음)" 으로 마무리해요. 어느 단계가 비어도 NPE 없이 흘러가는 게 보이시죠?
Step 7: 상자 속 상자 펴기: flatMap 과 거르기 filter
map 을 배웠으니 한 걸음 더 가볼게요. 비어 있을 수 있는 단계가 연달아 이어질 때가 있어요. 우리 도메인에 딱 그런 게 있어요.
게시물(Post) 에는 작성자(Member) 가 있어요. 그런데 게시물을 이름 문자열로만 만들면 작성자 객체가 null 일 수 있어요. 게다가 그 작성자의 이메일도 null 일 수 있고요. "있을 수도 없을 수도"가 두 번 겹치는 거예요.
// com/instagram/javabasic/modern/OptionalFlatMapDemo.java
// 작성자 상자 — 문자열 이름으로만 만든 게시물은 작성자(Member)가 없어요(null).
public static Optional<Member> authorOf(Post post) {
return Optional.ofNullable(post.getAuthor());
}
작성자를 Optional<Member> 로 감쌌어요. 이제 이 작성자의 이메일을 꺼내고 싶은데, 이메일도 비어 있을 수 있으니 Optional<String> 이에요. 만약 map 을 쓰면 어떻게 될까요? 상자(작성자) 안에서 또 상자(이메일) 를 만드니까, Optional<Optional<String>> 이라는 "상자 속 상자"가 돼버려요. 너무 번거롭죠.
이때 쓰는 게 flatMap(평평하게 펴서 변환) 이에요.
// 작성자의 이메일 — map 으로 하면 Optional<Optional<String>> 이 돼요.
// flatMap 은 그 두 겹을 한 겹(Optional<String>)으로 펴줘요.
public static Optional<String> authorEmail(Post post) {
return authorOf(post).flatMap(author -> Optional.ofNullable(author.getEmail()));
}
flatMap 은 두 겹의 상자를 한 겹으로 펴줘요. 결과가 깔끔하게 Optional<String> 이 되죠.
map 을 쓰면: Optional[작성자] → Optional[ Optional["...email..."] ] ← 상자 속 상자 😵
flatMap 을 쓰면: Optional[작성자] → Optional["...email..."] ← 한 겹으로 평평하게 ✨
이것도 Day 26 흐름의 flatMap 과 마찬가지예요. 흐름에서 "리스트 안의 리스트"를 한 줄기로 펴줬던 그 도구가, 여기선 "상자 안의 상자"를 한 겹으로 펴주는 거예요.
작성자가 없든, 작성자는 있는데 이메일이 없든, 어느 쪽이 비어도 NPE 없이 빈 상자로 흘러요.
public static String authorEmailOrDefault(Post post) {
return authorEmail(post).orElse("(작성자 이메일 없음)");
}
작성자 객체 자체가 null 이어도, 작성자는 있는데 이메일이 null 이어도, 모두 "(작성자 이메일 없음)" 으로 안전하게 마무리돼요. if 두 번 중첩할 일을 한 흐름으로 푼 거예요.
조건으로 거르기 — filter
상자 안의 값이 조건을 만족할 때만 남기고 싶을 때는 filter(거르기) 를 써요.
// filter — 좋아요 100개 이상인 글만 통과시켜요. 못 미치면 빈 상자가 돼요.
public static Optional<Post> popularOnly(Post post) {
return Optional.of(post).filter(p -> p.getLikeCount() >= 100);
}
filter 는 상자 안 값이 조건을 통과하면 그대로 두고, 못 통과하면 빈 상자로 바꿔요. 좋아요 250개짜리 글은 통과해서 값이 든 상자로 남고, 40개짜리 글은 조건에 막혀 빈 상자가 돼요. 흐름의 filter 가 조건에 안 맞는 원소를 빼버린 것처럼, 여기선 조건에 안 맞으면 상자를 비우는 거예요.
map·flatMap·filter — 이 셋을 이어 붙이면, 상자를 열지 않고도 "변환하고, 펴고, 거르는" 일을 한 줄기로 할 수 있어요. 그 종합은 잠시 뒤에 다시 만나요.
Step 8: 실전: 저장소가 상자를 돌려주면
이제 오늘 배운 걸 실전 자리에 놓아볼게요. 가장 흔하게 만나는 Optional 의 무대는 바로 "저장소에서 데이터 찾기"예요.
지난 시간들에 만든 저장소를 떠올려보세요. id 로 회원을 찾는데, 없으면 저장소 안에서 곧장 예외를 던졌어요. 그런데 여기엔 숨은 고민이 하나 있어요. "없을 때 어떻게 할지를 저장소가 정해버린다"는 거예요. 어떤 화면은 회원이 없으면 예외로 막고 싶고, 어떤 화면은 "게스트"로 대신 보여주고 싶을 수도 있잖아요. 저장소가 항상 예외만 던지면, 그 둘을 다르게 처리하기가 어려워요.
Optional 은 이 역할을 깔끔하게 나눠줘요. 저장소는 "찾았다 / 못 찾았다"만 정직하게 보고하고, "없을 때 무엇을 할지"는 호출하는 쪽이 정하는 거예요.
// com/instagram/javabasic/modern/OptionalRepositoryDemo.java
private final Map<Long, Member> store = new HashMap<>();
// 조회는 Optional 을 돌려줘요 — 없으면 빈 상자. 여기서 예외를 던지지 않아요(정책은 호출자 몫).
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
Map.get 은 키가 없으면 null 을 돌려줘요. 그 null 가능성을 ofNullable 로 감싸서 Optional<Member> 로 내보내는 거예요. 저장소는 여기서 아무 판단도 안 해요. 그저 "있으면 든 상자, 없으면 빈 상자"를 줄 뿐이죠.
이제 그 상자를 받은 호출자가 정책을 정해요. 같은 findById 하나로 서로 다른 결정을 내릴 수 있어요.
// 호출자 A — 없으면 예외. orElseThrow 로 우리 예외를 던져요.
public Member getOrThrow(Long id) {
return findById(id)
.orElseThrow(() -> new MemberNotFoundException("id " + id + " 회원이 없어요."));
}
// 호출자 B — 없으면 "게스트". 같은 findById 인데 정책이 달라요.
public String usernameOrGuest(Long id) {
return findById(id).map(Member::getUsername).orElse("게스트");
}
┌─ 호출자 A: .orElseThrow(...) ─▶ 없으면 예외 (엄격)
findById(id) ─▶ Optional ┤
└─ 호출자 B: .map(...).orElse("게스트") ─▶ 없으면 게스트 (관대)
getOrThrow 는 "회원이 꼭 있어야 한다"는 자리라 orElseThrow 로 예외를 던져요. usernameOrGuest 는 "없으면 게스트로 보여주자"는 자리라 orElse 로 기본값을 줘요. 저장소 코드는 한 줄도 안 바꾸고, 호출하는 쪽에서 정책만 다르게 고른 거예요. 이게 Optional 이 주는 유연함이에요.
흐름(Stream)과 상자(Optional)가 만나면
Optional 은 지난 시간 배운 흐름과도 잘 어울려요. 여러 id 를 한꺼번에 찾아서, 존재하는 회원만 모으고 싶다고 해볼게요.
// Optional + Stream — 여러 id 를 찾아, 존재하는 회원만 모아요.
// Optional.stream() 은 값이 있으면 원소 1개, 비면 0개짜리 흐름이라 flatMap 으로 빈 상자가 저절로 걸러져요.
public List<Member> findExisting(List<Long> ids) {
return ids.stream()
.map(this::findById)
.flatMap(Optional::stream)
.toList();
}
여기서 Optional.stream() 이라는 재밌는 도구가 나와요. 상자를 작은 흐름으로 바꿔주는데, 값이 있으면 원소 1개짜리 흐름, 비어 있으면 원소 0개짜리(빈) 흐름이에요. 그래서 flatMap 으로 펴면, 빈 상자(없는 회원) 는 0개라 저절로 사라지고, 있는 회원만 남아요.
ids: [ 1, 99, 2 ]
findById: [상자✓, 빈상자, 상자✓]
Optional.stream() + flatMap:
[member1] [](빈) [member2] ─▶ flatMap 으로 펴면 ─▶ [member1, member2]
└ 0개라 저절로 빠짐
if (회원 != null) 리스트에 추가 같은 코드를 한 줄도 안 쓰고, 없는 건 알아서 걸러진 깔끔한 목록을 얻었어요. Day 26~27 에서 익힌 흐름 위에 오늘 배운 상자가 자연스럽게 얹히는 모습이에요.
Step 9: 이럴 땐 쓰지 마세요: Optional 안티패턴
Optional 이 좋다고 아무 데나 쓰면 오히려 코드가 지저분해져요. "이렇게 쓰면 안 된다"는 자리를 알아두는 게, 잘 쓰는 것만큼 중요해요.
안티패턴 1 — isPresent() 로 확인하고 get() 으로 꺼내기
가장 흔한 실수예요. Step 4 에서 본 get() 을 isPresent() 와 같이 쓰는 거죠.
// com/instagram/javabasic/modern/OptionalAntiPatternDemo.java
// 번거로운 방식 — 상자를 만들어 놓고 if 로 열어보고 get 으로 꺼내요. (예전 null 검사와 다를 게 없어요.)
public static String clumsy(Member member) {
Optional<String> box = Optional.ofNullable(member.getEmail());
if (box.isPresent()) {
return box.get();
}
return "이메일 미등록";
}
언뜻 안전해 보이지만, 가만히 보면 예전 if (email != null) 와 똑같아요. 상자만 한 겹 씌웠을 뿐 달라진 게 없죠. Optional 을 쓰는 의미가 없어진 거예요. 같은 일을 이렇게 하면 한 줄로 끝나요.
// 깔끔한 방식 — 같은 결과를 orElse 한 줄로. Optional 을 제대로 쓰는 모습이에요.
public static String clean(Member member) {
return Optional.ofNullable(member.getEmail()).orElse("이메일 미등록");
}
두 메서드는 결과가 완전히 똑같아요. 하지만 아래가 의도도 또렷하고 실수할 여지도 없죠. isPresent() + get() 조합이 떠오르면, 거의 항상 orElse·orElseGet·map 으로 바꿀 수 있다고 기억해두세요.
안티패턴 2 — Optional 을 필드나 매개변수로 쓰기
Optional 은 "메서드가 값을 돌려줄 때, 그게 비어 있을 수 있음을 알리는" 용도로 설계됐어요. 그래서 다음 자리엔 쓰지 않는 게 좋아요.
- 클래스의 필드로 — 예를 들어
Member안에Optional<String> email같은 필드를 두는 건 권하지 않아요. 객체를 저장하거나 다룰 때 오히려 복잡해지고, 원래Optional이 노린 목적과도 안 맞아요. 필드는 그냥String email로 두고, 비어 있을 수 있으면 꺼낼 때ofNullable로 감싸는 게 깔끔해요. - 메서드 매개변수로 —
void doSomething(Optional<String> name)처럼 받지 마세요. 호출하는 쪽이 매번 상자에 담아 넘겨야 해서 번거로워요. 그냥 값을 받고, 안에서 필요하면 감싸면 돼요.
⚠️ 한 줄 원칙:
Optional은 메서드의 반환 타입에 쓰는 도구예요. "이 메서드는 값을 못 찾을 수도 있어"를 알리는 자리죠. 필드·매개변수·컬렉션 원소로는 쓰지 않아요.
안티패턴 3 — 빈 컬렉션이면 될 걸 Optional 로 감싸기
리스트를 돌려주는 메서드라면, 결과가 없을 때 Optional<List<...>> 가 아니라 그냥 빈 리스트(List.of()) 를 돌려주는 게 나아요. 받는 쪽이 빈 리스트면 그냥 0번 반복하고 끝나니까, 굳이 상자로 감쌀 이유가 없어요.
정리하면, Optional 은 "단 하나의 값이 있을 수도, 없을 수도 있는" 자리에 메서드 반환용으로 쓰는 도구예요. 그 밖의 자리에선 더 단순한 방법(평범한 값 + ofNullable, 빈 리스트) 이 보통 더 나아요.
마무리
오늘은 지난 시간 흘려둔 "있을 수도 없을 수도 있는 상자"의 정식 이름 Optional 을 제대로 배웠어요.
null의 문제 — "값이 없을 수도 있다"가 타입에 안 드러나서, 확인을 깜빡하면NullPointerException이 실제 빈 값이 들어온 순간 터져요.Optional이란 — 값을 감싸는 상자. "비어 있을 수 있음"을 타입으로 솔직하게 드러내, 받는 쪽이 빈 경우를 잊지 못하게 해요.- 만들기 —
of(절대 null 아님) ·ofNullable(null 가능) ·empty(빈 상자). - 확인 —
isPresent·isEmpty·ifPresent.get()은 빈 상자에 부르면 터지니 거의 안 써요. - 꺼내기 —
orElse(상수 기본값) ·orElseGet(만드는 비용 클 때) ·orElseThrow(없으면 예외). - 변환 —
map(안의 값 변환) ·flatMap(상자 속 상자 펴기) ·filter(조건으로 거르기). 비어 있으면 알아서 건너뛰어 NPE 가 안 나요. - 실전 — 저장소는
Optional로 "찾았다/못 찾았다"만 보고하고, 정책은 호출자가. 흐름과는Optional.stream()으로 어울려요. - 안티패턴 —
isPresent()+get()조합, 필드·매개변수로 쓰기, 빈 컬렉션 대신 감싸기는 피해요.
지난 시간 findFirst·max 가 돌려준 상자를 .orElse(null) 로 어색하게 열던 걸 기억하시죠? 이제 그 상자를 map·orElseThrow 로 우아하게 다룰 수 있게 됐어요. null 을 일일이 확인하던 번거로움이 사라진 거예요.
다음 시간엔 — 값을 담는 객체를 짧게 만들기
오늘 우리는 Member 같은 객체에서 값을 꺼내 다뤘어요. 그런데 이런 "값을 담기만 하는 객체"를 만들 때마다 생성자·getter·equals·hashCode·toString 을 잔뜩 적어야 했던 것, 기억나시죠? Day 16 에서 Member 를 만들 때 꽤 길었잖아요.
다음 시간엔 그 길고 반복되는 코드를 단 한 줄로 줄여주는 새 문법, Record(레코드) 를 배워요. "불변 데이터 클래스"를 아주 간결하게 만드는 도구예요. 오늘 다룬 안티패턴에서 "필드는 평범하게 두라"고 했던 그 값 객체를, 다음 시간엔 훨씬 짧고 안전하게 설계하게 될 거예요. 정말 수고 많으셨어요!
과제
오늘 배운 Optional 을 직접 써보며 익히는 과제예요. 모두 우리 인스타그램 도메인(Member·Post) 위에서 풀어봐요. modern 패키지의 오늘 예제들을 옆에 두고 비교하면서 작성하면 편해요.
과제 1: 회원 명단에서 한 명 찾기 — 없으면 빈 상자
상황 배경: 회원 목록(List<Member>) 에서 username 으로 한 명을 찾으려 해요. 그런데 그 이름의 회원이 없을 수도 있죠. 지난 시간 같으면 null 을 돌려주고 받는 쪽에서 확인했겠지만, 오늘은 Optional 로 정직하게 표현해봐요.
🎯 해결 미션:
findByUsername(List<Member> members, String username)메서드를 만들어, 찾으면 값이 든 상자를, 없으면 빈 상자를 돌려주세요. (흐름의filter+findFirst를 떠올려보세요.findFirst가Optional을 돌려준다는 걸 지난 시간에 봤죠.)getByUsernameOrThrow(List<Member> members, String username)메서드를 추가해, 회원이 없으면MemberNotFoundException을 던지세요. (orElseThrow를 써요. "그 회원이 꼭 있어야 다음을 진행할 수 있는" 경우예요.)
이렇게 만들면 "찾기"는 한 곳에 두고, "없을 때 어떻게 할지"는 호출하는 쪽이 고르게 돼요.
과제 2: 작성자 등급 안전하게 뽑기 — ofNullable → filter → map → orElse
상황 배경: 게시물(Post) 의 작성자가 "영향력 있는 작성자"(팔로워 1000명 이상) 일 때만 그 등급(grade()) 을 보여주려 해요. 그런데 게시물에 작성자 객체가 아예 없을 수도 있어요(이름 문자열로만 만든 게시물). 작성자가 없거나 팔로워가 모자라면 "등급 없음" 으로 처리하고요.
🎯 해결 미션: topAuthorGrade(Post post) 메서드를 만드세요. 한 흐름으로 이어보는 게 핵심이에요.
ofNullable로 작성자를 감싸고 (작성자가null일 수 있으니까요),filter로 "팔로워 1000명 이상"만 통과시키고,map으로 등급(Member::grade) 으로 변환하고,orElse("등급 없음")으로 마무리하세요.
작성자가 없든, 팔로워가 모자라든, 어느 쪽이 비어도 NPE 없이 "등급 없음" 으로 흘러야 해요. if 중첩 없이 한 줄기로 풀어보세요.
과제 3: 흐름 + 상자 종합 — 등록된 이메일 도메인만 모으기
상황 배경: 회원 목록에서 "이메일을 등록한 회원들의 도메인"만 중복 없이 모으려 해요. 어떤 회원은 이메일이 null 이고, 어떤 회원은 @ 가 없는 엉뚱한 값일 수도 있어요. 지난 시간 배운 흐름(Stream) 위에 오늘 배운 상자(Optional) 를 얹는 종합 과제예요.
🎯 해결 미션: registeredDomains(List<Member> members) 메서드를 만들어, 이메일이 등록되고 @ 가 있는 회원들의 도메인만 중복 없이 리스트로 돌려주세요.
- 회원에서 이메일을 뽑고(
map), - 이메일이
null인 회원은 걸러내고 (Optional.ofNullable(...).stream()을flatMap으로 펴면 빈 상자가 저절로 빠져요 — Step 8 을 떠올려보세요), @가 있는 이메일만 남기고(filter),- 도메인만 잘라내고(
map), - 중복을 없애(
distinct) 리스트로 모으세요(toList).
jaehoon@example.com·dana@gmail.com·이메일 없는 회원·@ 없는 값이 섞여 있으면, 결과는 [example.com, gmail.com] 이 나와야 해요.
생각해볼 주제
혼자 고민해도 좋고, 동료와 토론해도 좋아요. 정답을 외우기보다, "나라면 어떻게 설명할까"를 떠올리며 읽어보세요.
1. orElse 와 orElseGet, 결과가 같은데 왜 둘 다 있을까?
Step 5 에서 둘 다 "비어 있으면 기본값"이라고 배웠어요. 상자에 값이 있을 때도, 없을 때도 최종 결과는 똑같이 나와요. 그런데 자바는 왜 굳이 둘을 나눠놨을까요? 기본값을 만드는 데 시간이 오래 걸리는 작업(예: 한참 계산하거나 데이터를 새로 조회하는 일) 이라면, 상자에 값이 이미 있을 때 두 방식이 각각 어떻게 동작할지 떠올려보세요. "결과는 같은데 과정이 다르다"가 왜 중요한지 생각해보는 주제예요.
2. Optional 은 왜 필드로 쓰면 안 될까?
Step 9 에서 "Optional 은 메서드 반환 타입에 쓰고, 필드로는 쓰지 마라"고 했어요. 그런데 얼핏 보면 Member 안에 Optional<String> email 필드를 두면 "이 회원은 이메일이 없을 수도 있어"가 더 분명해 보이기도 해요. 그런데도 권하지 않는 이유가 뭘까요? 객체를 만들고, 비교하고, 저장하는 상황을 떠올리면서, 필드를 Optional 로 두면 어떤 불편이 생길지 생각해보세요.
3. Optional 이 있으면 이제 null 은 안 써도 될까?
오늘 Optional 로 null 의 위험을 많이 줄였어요. 그렇다면 앞으로 모든 자리에서 null 을 없애고 전부 Optional 로 바꾸면 더 안전할까요? 그런데 Optional 도 결국 객체라서 만들 때마다 약간의 비용이 들고, get() 처럼 잘못 쓰면 똑같이 터지는 메서드도 있어요. "어디엔 Optional 이 어울리고, 어디엔 평범한 값이나 빈 리스트가 더 나은지"를 구분하는 기준을 한번 정리해보세요.
✅ 예시 답안정답 보기
오늘 과제는 전부 "비어 있을 수 있는 값을 Optional 로 정직하게 다루기" 한 박자예요. modern 패키지의 오늘 예제(OptionalExtractDemo·OptionalFlatMapDemo·OptionalRepositoryDemo)를 옆에 두고 비교하면서 보면 편해요. 특히 과제 3 은 지난 시간 배운 흐름(Stream) 위에 오늘 배운 상자(Optional) 를 얹는 거라, 두 시간이 이어지는 재미가 있어요.
과제 예시답안
과제 1 예시답안 — 회원 명단에서 한 명 찾기, 없으면 빈 상자
핵심 접근
"명단에서 한 명 찾기"는 흐름으로 풀면 깔끔해요. filter 로 username 이 같은 회원만 남기고, findFirst 로 맨 앞 하나를 집어요. 그런데 지난 시간 배웠듯이 findFirst 는 값을 그냥 주지 않고 Optional 에 담아 줘요. 그래서 "찾으면 든 상자, 없으면 빈 상자"가 자연스럽게 나와요. 우리는 그 상자를 그대로 돌려주기만 하면 돼요. "없을 때 무엇을 할지"는 이 메서드가 정하지 않고, 부르는 쪽에 맡기는 거죠.
예시 구현
// com/instagram/javabasic/modern/solution/day28/MemberFinder.java
public static Optional<Member> findByUsername(List<Member> members, String username) {
return members.stream()
.filter(member -> member.getUsername().equals(username))
.findFirst();
}
public static Member getByUsernameOrThrow(List<Member> members, String username) {
return findByUsername(members, username)
.orElseThrow(() -> new MemberNotFoundException(username + " 회원을 찾을 수 없어요."));
}
findByUsername 은 흐름을 만들어 → 이름이 같은 회원만 거르고 → findFirst 로 하나를 집어 Optional 로 돌려줘요. minji 가 명단에 있으면 값이 든 상자, "nobody" 처럼 없는 이름이면 빈 상자가 나와요. getByUsernameOrThrow 는 그 상자를 받아 orElseThrow 로 "없으면 예외"라는 정책을 얹은 거예요. 찾기는 한 곳에 모여 있고, 없을 때 처리만 호출자가 고르는 구조죠.
채점 포인트
| 확인할 점 | 왜 중요한가 |
|---|---|
findFirst 의 결과를 그대로 Optional 로 돌려줬는가 |
findFirst 가 이미 Optional 이라, 굳이 꺼냈다 다시 감쌀 필요가 없어요 |
filter 의 비교가 member.getUsername().equals(username) 인가 |
문자열 비교는 == 가 아니라 equals 라야 정확해요 |
getByUsernameOrThrow 가 orElseThrow 를 썼는가 |
"없으면 진행 불가"인 자리라, 빈 상자를 예외로 바꿔야 해요 |
던지는 예외가 MemberNotFoundException 인가 |
Day 21~24 에서 만든 우리 예외를 재사용해, 이름만 봐도 상황을 알 수 있어요 |
흔한 실수
findFirst().get()으로 바로 꺼내려다, 없는 이름에서 프로그램이 멈춰요.get()은 빈 상자에서 터지니,Optional을 그대로 돌려주거나orElseThrow로 안전하게 처리해요.findByUsername안에서null을 돌려주려다Optional의 의미가 사라져요. "없음"을null이 아니라 빈 상자로 표현하는 게 오늘의 핵심이에요.equals대신==로 이름을 비교하면, 내용이 같아도 다른 객체라false가 나올 수 있어요. 문자열 비교는equals예요.
과제 2 예시답안 — 작성자 등급 안전하게 뽑기
핵심 접근
"작성자가 있고, 팔로워가 충분할 때만 등급"은 단계가 이어지는 일이에요. 각 단계가 비어 있을 수 있으니, Optional 한 흐름으로 이으면 if 중첩 없이 풀려요. 작성자를 ofNullable 로 감싸고(없을 수 있으니까), filter 로 팔로워 조건을 걸고, map 으로 등급으로 바꾸고, orElse 로 마무리하는 거예요. 어느 단계가 비어도 그 뒤는 자동으로 건너뛰어 빈 상자가 되고, 마지막 orElse 가 기본값을 줘요.
예시 구현
// com/instagram/javabasic/modern/solution/day28/AuthorGrade.java
public static String topAuthorGrade(Post post) {
return Optional.ofNullable(post.getAuthor())
.filter(author -> author.getFollowers() >= 1000)
.map(Member::grade)
.orElse("등급 없음");
}
작성자를 상자에 담고 → 팔로워 1000명 이상만 통과시키고 → 등급으로 변환하고 → 비면 "등급 없음" 으로 마무리했어요. 팔로워 30000명짜리 작성자라면 등급(예: "강력 추천") 이 나오고, 팔로워가 모자라거나 작성자 객체가 아예 없으면 모두 "등급 없음" 으로 흘러요. 네 줄이지만 if 가 한 개도 없죠.
채점 포인트
| 확인할 점 | 왜 중요한가 |
|---|---|
ofNullable 로 작성자를 감쌌는가 |
작성자가 null 일 수 있어서, of 를 쓰면 거기서 NPE 가 나요 |
filter 의 조건이 getFollowers() >= 1000 인가 |
"영향력 있는 작성자"만 통과시키는 게 문제의 조건이에요 |
map(Member::grade) 로 등급을 뽑았는가 |
상자를 열지 않고 안의 작성자를 등급 문자열로 바꾸는 자리예요 |
orElse("등급 없음") 으로 마무리했는가 |
작성자가 없거나 조건에 막혀 빈 상자가 됐을 때의 기본값이에요 |
흔한 실수
post.getAuthor()를Optional.of로 감싸면, 작성자가 없는 게시물에서 그 자리가 바로 터져요.null가능성이 있으니ofNullable이라야 해요.filter와map의 순서를 바꿔, 등급으로 바꾼 뒤 팔로워로 거르려 하면 막혀요. 등급(String) 에는 팔로워 정보가 없으니, 거르기를 먼저 하고 변환을 나중에 해요.- 단계마다
isPresent()로 확인하고get()으로 꺼내며 풀려다 코드가 길어져요.ofNullable → filter → map → orElse한 흐름이 훨씬 짧고 안전해요.
과제 3 예시답안 — 흐름 + 상자 종합, 등록된 이메일 도메인만 모으기
핵심 접근
지난 시간 흐름으로 데이터를 모았다면, 오늘은 그 흐름에 상자를 얹어요. 회원마다 이메일을 뽑는데, 이메일이 null 인 회원이 섞여 있어요. 이 null 을 흐름에서 자연스럽게 걸러내는 게 Optional.stream() 이에요. 값이 있으면 원소 1개, 비면 0개짜리 흐름이라, flatMap 으로 펴면 이메일 없는 회원이 저절로 빠져요. 그다음 @ 가 있는 것만 남기고, 도메인을 잘라내고, 중복을 없애 모으면 돼요.
예시 구현
// com/instagram/javabasic/modern/solution/day28/DomainCollector.java
public static List<String> registeredDomains(List<Member> members) {
return members.stream()
.map(Member::getEmail)
.flatMap(email -> Optional.ofNullable(email).stream())
.filter(email -> email.contains("@"))
.map(email -> email.substring(email.indexOf("@") + 1))
.distinct()
.toList();
}
회원에서 이메일을 뽑고(map) → 이메일이 null 인 회원은 Optional.ofNullable(...).stream() 을 flatMap 으로 펴서 걸러내고 → @ 가 있는 것만 남기고(filter) → 도메인을 잘라내고(map) → 중복을 없애(distinct) 모았어요. jaehoon@example.com·이메일 없는 회원·dana@gmail.com·@ 없는 값이 섞여 있으면, 결과는 [example.com, gmail.com] 이 나와요.
채점 포인트
| 확인할 점 | 왜 중요한가 |
|---|---|
flatMap(email -> Optional.ofNullable(email).stream()) 로 null 을 걸렀는가 |
빈 상자는 원소 0개라, flatMap 으로 펴면 이메일 없는 회원이 저절로 빠져요 |
@ 가 있는 것만 filter 했는가 |
@ 가 없으면 도메인을 자를 때 엉뚱한 결과가 나와서, 미리 걸러요 |
도메인을 substring(email.indexOf("@") + 1) 로 잘랐는가 |
@ 다음 칸부터 끝까지가 도메인이에요 |
distinct() 로 중복을 없앴는가 |
같은 도메인을 쓰는 회원이 여럿이면 도메인이 중복되니, 한 번씩만 남겨요 |
흔한 실수
filter(email -> email != null)로null을 거르려다, 흐름 안에서null을 다루는 게 어색해져요.Optional.ofNullable(...).stream()+flatMap조합이 오늘 배운 더 깔끔한 방법이에요.@거르기를 빼먹으면,@없는 값에서indexOf("@")가-1을 돌려줘 엉뚱한 자리부터 잘려요. 도메인 자르기 전에@확인이 필요해요.distinct()를 빼먹으면 같은 도메인이 여러 번 들어가요. "도메인만 중복 없이"가 문제의 조건이에요.
생각해볼 주제 예시답안
생각해볼 주제 1 예시답안 — orElse 와 orElseGet, 결과가 같은데 왜 둘 다 있을까?
[문제 상황 요약]
둘 다 "비어 있으면 기본값"이라 최종 결과는 똑같아요. 그런데도 자바가 둘을 나눠놓은 이유가 있어요. 기본값을 만드는 일이 무거울 때, 상자에 값이 이미 있으면 두 방식이 각각 어떻게 동작하는지를 따져보는 주제예요.
[튜터의 가이드 및 해설]
차이는 "기본값을 언제 만드느냐"예요. orElse(기본값) 은 기본값을 항상 먼저 만들어 둬요. 상자에 값이 멀쩡히 들어 있어도, 안 쓸 기본값을 일단 계산하죠. 반면 orElseGet(() -> 기본값) 은 상자가 비었을 때만 람다를 실행해 기본값을 만들어요. 값이 있으면 람다는 아예 안 돌아요.
기본값이 "이메일 미등록" 같은 짧은 문자열이면 둘은 사실상 차이가 없어요. 문자열 하나 미리 만드는 비용은 없는 거나 마찬가지니까요. 그래서 단순한 상수 기본값엔 orElse 가 읽기 편하고 좋아요.
문제는 기본값을 만드는 게 무거운 일일 때예요. 예를 들어 기본값을 만들려고 데이터를 새로 조회하거나 한참 계산해야 한다고 해볼게요. 이때 orElse 를 쓰면, 상자에 값이 있든 없든 그 무거운 계산을 매번 해버려요. 정작 값이 있으면 그 결과는 버려지고요. 헛수고죠. orElseGet 은 정말 비어 있을 때만 계산하니까, 값이 있는 흔한 경우엔 그 비용을 아예 안 치러요.
그래서 기준은 이래요. 기본값이 이미 있는 값(상수·짧은 문자열) 이면 orElse, 기본값을 그때 만들어야 하고 그 비용이 크면 orElseGet. 헷갈리면 orElseGet 이 손해 볼 일은 없어요. "결과는 같은데 과정이 다르다"가 성능에서 갈리는 대표적인 예예요.
🎯 면접관을 홀리는 핵심 멘트
"
orElse와orElseGet은 결과는 같지만 기본값을 만드는 시점이 달라요.orElse는 인자를 항상 먼저 평가하고,orElseGet은 비어 있을 때만Supplier를 실행해요. 기본값이 단순 상수면orElse가 읽기 좋지만, 기본값 생성이 무거운 작업(조회·계산) 이면orElse는 값이 있을 때도 그 비용을 치르고 결과를 버려서 낭비가 돼요. 그래서 생성 비용이 있는 기본값은orElseGet으로 지연 평가하는 게 맞다고 봐요."
생각해볼 주제 2 예시답안 — Optional 은 왜 필드로 쓰면 안 될까?
[문제 상황 요약]
얼핏 Member 안에 Optional<String> email 필드를 두면 "이메일이 없을 수도 있어"가 더 분명해 보여요. 그런데도 권하지 않는 이유를 객체를 만들고·비교하고·저장하는 상황에서 따져보는 주제예요.
[튜터의 가이드 및 해설]
Optional 은 원래 "메서드가 값을 돌려줄 때, 그게 비어 있을 수 있음을 알리는" 용도로 만들어졌어요. 설계자들도 "반환 타입에 쓰라"고 못 박았고요. 필드로 쓰면 그 의도와 어긋나면서 불편이 여럿 생겨요.
먼저 객체를 만들 때 번거로워져요. 필드가 Optional<String> email 이면, 이메일이 있는 회원도 Optional.of(...) 로 감싸서 넣어야 해요. 값을 그냥 넣지 못하고 매번 상자에 담는 거죠. 또 equals 나 hashCode 로 회원을 비교할 때, 상자끼리 비교하는 게 끼어들어 더 복잡해져요.
저장하거나 주고받을 때도 문제예요. 객체를 파일이나 네트워크로 내보내야 할 때, Optional 은 그런 용도로 설계되지 않아서 매끄럽지 않아요. 게다가 필드가 비어 있는 상태를 표현하는 방법이 이미 null 로 충분한데, 그 위에 Optional 을 또 씌우면 "빈 상태가 두 가지(null 인 Optional? 빈 Optional?)"가 되는 혼란까지 생길 수 있어요.
그래서 깔끔한 길은 이래요. 필드는 평범하게 String email 로 두고, 그 값을 꺼내 쓰는 메서드에서 비어 있을 수 있으면 Optional.ofNullable(email) 로 감싸 돌려줘요. "비어 있을 수 있음"을 알리는 건 값을 건네주는 그 순간, 즉 메서드 반환에서 하면 충분하거든요. 필드까지 상자로 만들 필요가 없는 거죠.
🎯 면접관을 홀리는 핵심 멘트
"
Optional은 메서드 반환 타입을 위해 설계된 도구예요. 필드로 쓰면 객체를 만들 때마다 값을 상자에 감싸야 하고,equals·hashCode비교가 복잡해지고, 직렬화 같은 상황에서도 매끄럽지 않아요. 게다가 '비어 있음'을 표현하는 수단이null과 빈Optional로 이중이 되는 혼란도 생기죠. 그래서 필드는 평범한 타입으로 두고, 비어 있을 수 있음은 그 값을 돌려주는 메서드의 반환 타입(Optional) 에서 드러내는 게 설계 의도에 맞다고 봐요."
생각해볼 주제 3 예시답안 — Optional 이 있으면 이제 null 은 안 써도 될까?
[문제 상황 요약]
Optional 로 null 의 위험을 많이 줄였어요. 그렇다면 모든 자리를 Optional 로 바꾸면 더 안전할까요? Optional 도 객체라 비용이 있고 잘못 쓰면 터지는 점까지 놓고, "어디에 어울리는 도구인지"를 정리하는 주제예요.
[튜터의 가이드 및 해설]
Optional 은 만능 해결책이 아니라, "잘 맞는 자리"가 따로 있는 도구예요. 가장 잘 맞는 곳은 "값을 못 찾을 수도 있는 조회 메서드의 반환"이에요. 저장소에서 id 로 하나 찾기, 명단에서 이름으로 한 명 찾기처럼요. 받는 쪽이 "없을 수도 있음"을 잊지 않게 해주는 게 진짜 가치죠.
반대로 어울리지 않는 자리도 분명해요. 오늘 안티패턴에서 봤듯이 필드·매개변수로는 쓰지 않고, 리스트를 돌려줄 때 결과가 없으면 Optional<List> 가 아니라 그냥 빈 리스트를 줘요. 받는 쪽이 빈 리스트면 0번 반복하고 끝나니, 상자로 감쌀 이유가 없거든요.
비용도 생각해야 해요. Optional 은 값을 감싸는 객체라, 만들 때마다 아주 약간의 비용이 들어요. 한두 번이면 무시할 수준이지만, 어마어마하게 많은 데이터를 다루는 성능이 중요한 자리에선 이 작은 비용도 쌓일 수 있어요. 또 Optional 을 쓴다고 위험이 0이 되는 것도 아니에요. get() 을 확인 없이 부르면 null 을 확인 없이 쓴 것과 똑같이 터지니까요. 도구를 바꿨다고 습관까지 저절로 바뀌는 건 아닌 거죠.
정리하면 null 을 무조건 없애는 게 목표가 아니라, "비어 있을 수 있는 단일 값을 돌려주는 자리"에 Optional 을 골라 쓰는 게 핵심이에요. 그 밖의 자리에선 평범한 값, 빈 리스트, 때로는 null 이 더 단순하고 적절할 수 있어요.
🎯 면접관을 홀리는 핵심 멘트
"
Optional은null을 전부 대체하는 도구가 아니라, '비어 있을 수 있는 단일 값을 돌려주는 메서드'에 어울리는 도구예요. 컬렉션은 빈 리스트로, 필드·매개변수는 평범한 타입으로 두는 게 더 단순하죠.Optional도 객체라 생성 비용이 있고,get()을 함부로 쓰면 똑같이 터져서 '쓰기만 하면 안전'한 것도 아니에요. 그래서 저는 '조회 결과의 부재를 반환으로 알리는 자리'에 선택적으로 적용하고, 나머지는 더 단순한 표현을 택하는 기준으로 씁니다."
