문서 읽는 데 58분 · day28

Day 28 — Optional: null을 안전하게 다루기

목차 27
전체 33강 중 28강 · 자바 기초
난이도 · 입문

지난 시간, 우리는 흐름에서 딱 하나를 집어오는 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 1null 이 일으키는 사고: 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 을 모르고 그냥 쓸 때 생겨요.

Java
// 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 인지 직접 확인하는 거죠.

Java
// 방어한 방식 — 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 의 핵심은 "이 값은 비어 있을 수도 있어"라는 경고를 타입 안에 박아두는 거예요. 그래서 받는 사람이 빈 경우를 처리하는 걸 깜빡하기 어려워져요. 지난 시간 maxOptional 을 돌려준 것도 같은 이유였어요. "흐름이 비면 줄 값이 없으니, 그 사실을 상자로 정직하게 알려준" 거죠.

이제 이 상자를 어떻게 만들고, 열고, 다루는지 하나씩 배워볼게요.


Step 3: 상자 만들기: of·ofNullable·empty

상자를 만드는 방법은 세 가지예요. 언제 무엇을 쓰는지가 중요해요.

Java
// 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()        처음부터 빈 상자가 필요할 때

여기서 가장 헷갈리는 게 ofofNullable 의 차이예요. 둘 다 값을 상자에 담는데, 차이는 "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

상자를 만들었으면, 이제 "값이 들어 있나?"를 확인하고 싶을 거예요. 가장 직관적인 방법부터 볼게요.

Java
// 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 가 깔끔해요.

Java
// 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() 이라는 메서드예요.

Java
// 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() 의 위험을 봤으니, 이제 진짜 제대로 된 꺼내기를 배워요. 핵심은 "비어 있을 때 어떻게 할지"를 함께 정하는 거예요. 세 가지가 있어요.

Java
// 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 을 여기서 다시 써요. "이 값이 없으면 더 진행할 수 없다"는 자리에 딱이에요.

orElseorElseGet 은 뭐가 다를까?

둘 다 "비어 있으면 기본값"인데 왜 나뉘어 있을까요? 차이는 기본값을 언제 만드느냐예요.

텍스트
 orElse("기본값")          기본값을 항상 먼저 만들어 둠
                           → 상자에 값이 있어도 기본값 계산은 이미 끝난 상태

 orElseGet(() -> "기본값")  기본값은 상자가 비었을 때만 만듦
                           → 상자에 값이 있으면 람다는 아예 실행 안 함

orElse 는 기본값을 항상 미리 준비해요. 상자에 값이 멀쩡히 들어 있어도, 안 쓸 기본값을 일단 만들어 둬요. 기본값이 그냥 "이메일 미등록" 같은 짧은 문자열이면 아무 문제 없어요.

그런데 기본값을 만드는 데 비용이 크다면? 예를 들어 기본값을 만들려고 데이터를 한참 조회하거나 계산해야 한다면, orElse 는 상자에 값이 있어도 그 계산을 매번 해버려서 낭비예요. 이럴 때 orElseGet 을 쓰면, 정말 비어 있을 때만 람다가 돌아서 헛수고를 안 해요.

💡 기억하기 쉽게: 기본값이 이미 있는 값(상수·짧은 문자열) 이면 orElse, 기본값을 그때 만들어야 하면 orElseGet. 헷갈리면 orElseGet 이 항상 안전한 선택이에요.

orElseThrow 는 지난 며칠 배운 예외와 다시 만나요. 저장소에서 회원을 찾았는데 없으면, 빈 상자 대신 MemberNotFoundException 을 던져서 "그 회원 없어요"를 분명히 알리는 거죠. 이 패턴은 Step 8 에서 실전으로 다시 만나요.


Step 6: 상자 안에서 변환하기: map

지금까지는 상자에서 값을 꺼내는 데 집중했어요. 그런데 Optional 의 진짜 매력은 "상자를 열지 않고도 안의 값을 다룰 수 있다"는 거예요. 그 첫 번째 도구가 map(변환) 이에요.

Java
// 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 은 여러 원소를 하나씩 변환했죠. Optionalmap 은 원소가 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 을 쓰는 큰 이유 중 하나예요.

변환한 다음 꺼내기까지 한 흐름으로 이어볼 수 있어요.

Java
// 이메일 도메인 — @ 뒤를 잘라내요. 비면 기본값으로 마무리해요.
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 일 수 있고요. "있을 수도 없을 수도"가 두 번 겹치는 거예요.

Java
// 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(평평하게 펴서 변환) 이에요.

Java
// 작성자의 이메일 — 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 없이 빈 상자로 흘러요.

Java
public static String authorEmailOrDefault(Post post) {
    return authorEmail(post).orElse("(작성자 이메일 없음)");
}

작성자 객체 자체가 null 이어도, 작성자는 있는데 이메일이 null 이어도, 모두 "(작성자 이메일 없음)" 으로 안전하게 마무리돼요. if 두 번 중첩할 일을 한 흐름으로 푼 거예요.

조건으로 거르기 — filter

상자 안의 값이 조건을 만족할 때만 남기고 싶을 때는 filter(거르기) 를 써요.

Java
// 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 은 이 역할을 깔끔하게 나눠줘요. 저장소는 "찾았다 / 못 찾았다"만 정직하게 보고하고, "없을 때 무엇을 할지"는 호출하는 쪽이 정하는 거예요.

Java
// 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 하나로 서로 다른 결정을 내릴 수 있어요.

Java
// 호출자 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 를 한꺼번에 찾아서, 존재하는 회원만 모으고 싶다고 해볼게요.

Java
// 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() 와 같이 쓰는 거죠.

Java
// 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 을 쓰는 의미가 없어진 거예요. 같은 일을 이렇게 하면 한 줄로 끝나요.

Java
// 깔끔한 방식 — 같은 결과를 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 로 정직하게 표현해봐요.

🎯 해결 미션:

  1. findByUsername(List<Member> members, String username) 메서드를 만들어, 찾으면 값이 든 상자를, 없으면 빈 상자를 돌려주세요. (흐름의 filter + findFirst 를 떠올려보세요. findFirstOptional 을 돌려준다는 걸 지난 시간에 봤죠.)
  2. getByUsernameOrThrow(List<Member> members, String username) 메서드를 추가해, 회원이 없으면 MemberNotFoundException 을 던지세요. (orElseThrow 를 써요. "그 회원이 꼭 있어야 다음을 진행할 수 있는" 경우예요.)

이렇게 만들면 "찾기"는 한 곳에 두고, "없을 때 어떻게 할지"는 호출하는 쪽이 고르게 돼요.

과제 2: 작성자 등급 안전하게 뽑기 — ofNullablefiltermaporElse

상황 배경: 게시물(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. orElseorElseGet, 결과가 같은데 왜 둘 다 있을까?

Step 5 에서 둘 다 "비어 있으면 기본값"이라고 배웠어요. 상자에 값이 있을 때도, 없을 때도 최종 결과는 똑같이 나와요. 그런데 자바는 왜 굳이 둘을 나눠놨을까요? 기본값을 만드는 데 시간이 오래 걸리는 작업(예: 한참 계산하거나 데이터를 새로 조회하는 일) 이라면, 상자에 값이 이미 있을 때 두 방식이 각각 어떻게 동작할지 떠올려보세요. "결과는 같은데 과정이 다르다"가 왜 중요한지 생각해보는 주제예요.

2. Optional 은 왜 필드로 쓰면 안 될까?

Step 9 에서 "Optional 은 메서드 반환 타입에 쓰고, 필드로는 쓰지 마라"고 했어요. 그런데 얼핏 보면 Member 안에 Optional<String> email 필드를 두면 "이 회원은 이메일이 없을 수도 있어"가 더 분명해 보이기도 해요. 그런데도 권하지 않는 이유가 뭘까요? 객체를 만들고, 비교하고, 저장하는 상황을 떠올리면서, 필드를 Optional 로 두면 어떤 불편이 생길지 생각해보세요.

3. Optional 이 있으면 이제 null 은 안 써도 될까?

오늘 Optionalnull 의 위험을 많이 줄였어요. 그렇다면 앞으로 모든 자리에서 null 을 없애고 전부 Optional 로 바꾸면 더 안전할까요? 그런데 Optional 도 결국 객체라서 만들 때마다 약간의 비용이 들고, get() 처럼 잘못 쓰면 똑같이 터지는 메서드도 있어요. "어디엔 Optional 이 어울리고, 어디엔 평범한 값이나 빈 리스트가 더 나은지"를 구분하는 기준을 한번 정리해보세요.

✅ 예시 답안정답 보기

오늘 과제는 전부 "비어 있을 수 있는 값을 Optional 로 정직하게 다루기" 한 박자예요. modern 패키지의 오늘 예제(OptionalExtractDemo·OptionalFlatMapDemo·OptionalRepositoryDemo)를 옆에 두고 비교하면서 보면 편해요. 특히 과제 3 은 지난 시간 배운 흐름(Stream) 위에 오늘 배운 상자(Optional) 를 얹는 거라, 두 시간이 이어지는 재미가 있어요.

과제 예시답안

과제 1 예시답안 — 회원 명단에서 한 명 찾기, 없으면 빈 상자

핵심 접근

"명단에서 한 명 찾기"는 흐름으로 풀면 깔끔해요. filter 로 username 이 같은 회원만 남기고, findFirst 로 맨 앞 하나를 집어요. 그런데 지난 시간 배웠듯이 findFirst 는 값을 그냥 주지 않고 Optional 에 담아 줘요. 그래서 "찾으면 든 상자, 없으면 빈 상자"가 자연스럽게 나와요. 우리는 그 상자를 그대로 돌려주기만 하면 돼요. "없을 때 무엇을 할지"는 이 메서드가 정하지 않고, 부르는 쪽에 맡기는 거죠.

예시 구현

Java
// 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 라야 정확해요
getByUsernameOrThroworElseThrow 를 썼는가 "없으면 진행 불가"인 자리라, 빈 상자를 예외로 바꿔야 해요
던지는 예외가 MemberNotFoundException 인가 Day 21~24 에서 만든 우리 예외를 재사용해, 이름만 봐도 상황을 알 수 있어요

흔한 실수

  • findFirst().get() 으로 바로 꺼내려다, 없는 이름에서 프로그램이 멈춰요. get() 은 빈 상자에서 터지니, Optional 을 그대로 돌려주거나 orElseThrow 로 안전하게 처리해요.
  • findByUsername 안에서 null 을 돌려주려다 Optional 의 의미가 사라져요. "없음"을 null 이 아니라 빈 상자로 표현하는 게 오늘의 핵심이에요.
  • equals 대신 == 로 이름을 비교하면, 내용이 같아도 다른 객체라 false 가 나올 수 있어요. 문자열 비교는 equals 예요.

과제 2 예시답안 — 작성자 등급 안전하게 뽑기

핵심 접근

"작성자가 있고, 팔로워가 충분할 때만 등급"은 단계가 이어지는 일이에요. 각 단계가 비어 있을 수 있으니, Optional 한 흐름으로 이으면 if 중첩 없이 풀려요. 작성자를 ofNullable 로 감싸고(없을 수 있으니까), filter 로 팔로워 조건을 걸고, map 으로 등급으로 바꾸고, orElse 로 마무리하는 거예요. 어느 단계가 비어도 그 뒤는 자동으로 건너뛰어 빈 상자가 되고, 마지막 orElse 가 기본값을 줘요.

예시 구현

Java
// 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 이라야 해요.
  • filtermap 의 순서를 바꿔, 등급으로 바꾼 뒤 팔로워로 거르려 하면 막혀요. 등급(String) 에는 팔로워 정보가 없으니, 거르기를 먼저 하고 변환을 나중에 해요.
  • 단계마다 isPresent() 로 확인하고 get() 으로 꺼내며 풀려다 코드가 길어져요. ofNullable → filter → map → orElse 한 흐름이 훨씬 짧고 안전해요.

과제 3 예시답안 — 흐름 + 상자 종합, 등록된 이메일 도메인만 모으기

핵심 접근

지난 시간 흐름으로 데이터를 모았다면, 오늘은 그 흐름에 상자를 얹어요. 회원마다 이메일을 뽑는데, 이메일이 null 인 회원이 섞여 있어요. 이 null 을 흐름에서 자연스럽게 걸러내는 게 Optional.stream() 이에요. 값이 있으면 원소 1개, 비면 0개짜리 흐름이라, flatMap 으로 펴면 이메일 없는 회원이 저절로 빠져요. 그다음 @ 가 있는 것만 남기고, 도메인을 잘라내고, 중복을 없애 모으면 돼요.

예시 구현

Java
// 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 예시답안 — orElseorElseGet, 결과가 같은데 왜 둘 다 있을까?

[문제 상황 요약]

둘 다 "비어 있으면 기본값"이라 최종 결과는 똑같아요. 그런데도 자바가 둘을 나눠놓은 이유가 있어요. 기본값을 만드는 일이 무거울 때, 상자에 값이 이미 있으면 두 방식이 각각 어떻게 동작하는지를 따져보는 주제예요.

[튜터의 가이드 및 해설]

차이는 "기본값을 언제 만드느냐"예요. orElse(기본값) 은 기본값을 항상 먼저 만들어 둬요. 상자에 값이 멀쩡히 들어 있어도, 안 쓸 기본값을 일단 계산하죠. 반면 orElseGet(() -> 기본값) 은 상자가 비었을 때만 람다를 실행해 기본값을 만들어요. 값이 있으면 람다는 아예 안 돌아요.

기본값이 "이메일 미등록" 같은 짧은 문자열이면 둘은 사실상 차이가 없어요. 문자열 하나 미리 만드는 비용은 없는 거나 마찬가지니까요. 그래서 단순한 상수 기본값엔 orElse 가 읽기 편하고 좋아요.

문제는 기본값을 만드는 게 무거운 일일 때예요. 예를 들어 기본값을 만들려고 데이터를 새로 조회하거나 한참 계산해야 한다고 해볼게요. 이때 orElse 를 쓰면, 상자에 값이 있든 없든 그 무거운 계산을 매번 해버려요. 정작 값이 있으면 그 결과는 버려지고요. 헛수고죠. orElseGet 은 정말 비어 있을 때만 계산하니까, 값이 있는 흔한 경우엔 그 비용을 아예 안 치러요.

그래서 기준은 이래요. 기본값이 이미 있는 값(상수·짧은 문자열) 이면 orElse, 기본값을 그때 만들어야 하고 그 비용이 크면 orElseGet. 헷갈리면 orElseGet 이 손해 볼 일은 없어요. "결과는 같은데 과정이 다르다"가 성능에서 갈리는 대표적인 예예요.

🎯 면접관을 홀리는 핵심 멘트

"orElseorElseGet 은 결과는 같지만 기본값을 만드는 시점이 달라요. orElse 는 인자를 항상 먼저 평가하고, orElseGet 은 비어 있을 때만 Supplier 를 실행해요. 기본값이 단순 상수면 orElse 가 읽기 좋지만, 기본값 생성이 무거운 작업(조회·계산) 이면 orElse 는 값이 있을 때도 그 비용을 치르고 결과를 버려서 낭비가 돼요. 그래서 생성 비용이 있는 기본값은 orElseGet 으로 지연 평가하는 게 맞다고 봐요."


생각해볼 주제 2 예시답안 — Optional 은 왜 필드로 쓰면 안 될까?

[문제 상황 요약]

얼핏 Member 안에 Optional<String> email 필드를 두면 "이메일이 없을 수도 있어"가 더 분명해 보여요. 그런데도 권하지 않는 이유를 객체를 만들고·비교하고·저장하는 상황에서 따져보는 주제예요.

[튜터의 가이드 및 해설]

Optional 은 원래 "메서드가 값을 돌려줄 때, 그게 비어 있을 수 있음을 알리는" 용도로 만들어졌어요. 설계자들도 "반환 타입에 쓰라"고 못 박았고요. 필드로 쓰면 그 의도와 어긋나면서 불편이 여럿 생겨요.

먼저 객체를 만들 때 번거로워져요. 필드가 Optional<String> email 이면, 이메일이 있는 회원도 Optional.of(...) 로 감싸서 넣어야 해요. 값을 그냥 넣지 못하고 매번 상자에 담는 거죠. 또 equalshashCode 로 회원을 비교할 때, 상자끼리 비교하는 게 끼어들어 더 복잡해져요.

저장하거나 주고받을 때도 문제예요. 객체를 파일이나 네트워크로 내보내야 할 때, Optional 은 그런 용도로 설계되지 않아서 매끄럽지 않아요. 게다가 필드가 비어 있는 상태를 표현하는 방법이 이미 null 로 충분한데, 그 위에 Optional 을 또 씌우면 "빈 상태가 두 가지(nullOptional? 빈 Optional?)"가 되는 혼란까지 생길 수 있어요.

그래서 깔끔한 길은 이래요. 필드는 평범하게 String email 로 두고, 그 값을 꺼내 쓰는 메서드에서 비어 있을 수 있으면 Optional.ofNullable(email) 로 감싸 돌려줘요. "비어 있을 수 있음"을 알리는 건 값을 건네주는 그 순간, 즉 메서드 반환에서 하면 충분하거든요. 필드까지 상자로 만들 필요가 없는 거죠.

🎯 면접관을 홀리는 핵심 멘트

"Optional 은 메서드 반환 타입을 위해 설계된 도구예요. 필드로 쓰면 객체를 만들 때마다 값을 상자에 감싸야 하고, equals·hashCode 비교가 복잡해지고, 직렬화 같은 상황에서도 매끄럽지 않아요. 게다가 '비어 있음'을 표현하는 수단이 null 과 빈 Optional 로 이중이 되는 혼란도 생기죠. 그래서 필드는 평범한 타입으로 두고, 비어 있을 수 있음은 그 값을 돌려주는 메서드의 반환 타입(Optional) 에서 드러내는 게 설계 의도에 맞다고 봐요."


생각해볼 주제 3 예시답안 — Optional 이 있으면 이제 null 은 안 써도 될까?

[문제 상황 요약]

Optionalnull 의 위험을 많이 줄였어요. 그렇다면 모든 자리를 Optional 로 바꾸면 더 안전할까요? Optional 도 객체라 비용이 있고 잘못 쓰면 터지는 점까지 놓고, "어디에 어울리는 도구인지"를 정리하는 주제예요.

[튜터의 가이드 및 해설]

Optional 은 만능 해결책이 아니라, "잘 맞는 자리"가 따로 있는 도구예요. 가장 잘 맞는 곳은 "값을 못 찾을 수도 있는 조회 메서드의 반환"이에요. 저장소에서 id 로 하나 찾기, 명단에서 이름으로 한 명 찾기처럼요. 받는 쪽이 "없을 수도 있음"을 잊지 않게 해주는 게 진짜 가치죠.

반대로 어울리지 않는 자리도 분명해요. 오늘 안티패턴에서 봤듯이 필드·매개변수로는 쓰지 않고, 리스트를 돌려줄 때 결과가 없으면 Optional<List> 가 아니라 그냥 빈 리스트를 줘요. 받는 쪽이 빈 리스트면 0번 반복하고 끝나니, 상자로 감쌀 이유가 없거든요.

비용도 생각해야 해요. Optional 은 값을 감싸는 객체라, 만들 때마다 아주 약간의 비용이 들어요. 한두 번이면 무시할 수준이지만, 어마어마하게 많은 데이터를 다루는 성능이 중요한 자리에선 이 작은 비용도 쌓일 수 있어요. 또 Optional 을 쓴다고 위험이 0이 되는 것도 아니에요. get() 을 확인 없이 부르면 null 을 확인 없이 쓴 것과 똑같이 터지니까요. 도구를 바꿨다고 습관까지 저절로 바뀌는 건 아닌 거죠.

정리하면 null 을 무조건 없애는 게 목표가 아니라, "비어 있을 수 있는 단일 값을 돌려주는 자리"에 Optional 을 골라 쓰는 게 핵심이에요. 그 밖의 자리에선 평범한 값, 빈 리스트, 때로는 null 이 더 단순하고 적절할 수 있어요.

🎯 면접관을 홀리는 핵심 멘트

"Optionalnull 을 전부 대체하는 도구가 아니라, '비어 있을 수 있는 단일 값을 돌려주는 메서드'에 어울리는 도구예요. 컬렉션은 빈 리스트로, 필드·매개변수는 평범한 타입으로 두는 게 더 단순하죠. Optional 도 객체라 생성 비용이 있고, get() 을 함부로 쓰면 똑같이 터져서 '쓰기만 하면 안전'한 것도 아니에요. 그래서 저는 '조회 결과의 부재를 반환으로 알리는 자리'에 선택적으로 적용하고, 나머지는 더 단순한 표현을 택하는 기준으로 씁니다."

전체 목록 자바 기초

더 배우려면

실무 프로젝트까지 가고 싶다면

팀스파르타 백엔드 부트캠프에서 인스타그램 클론을 풀스택으로 완성합니다.