자바 백엔드 입문 8편. NullPointerException을 막는 모던 자바 표준 자바 Optional의 표준 사용법을 봉투 비유로 풀어쓴 학습 노트.
이 글은 자바 백엔드 입문 시리즈 59편 중 8편이에요. 이번 8편은 자바 8(2014)에 도입돼 모던 자바의 표준이 된 자바 Optional — null 안전 처리.
NullPointerException — 자바의 영원한 적
자바 개발자가 가장 자주 만나는 에러 = NullPointerException(NPE). 1965년 "십억 달러 실수" 라고 불리는 null 참조 — 영국 컴퓨터 과학자 Tony Hoare 가 ALGOL W에 null 도입한 걸 평생 후회한다고 말한 일화는 유명.
User user = userRepository.findByEmail("alice@example.com");
String name = user.getName(); // user가 null이면 → NullPointerException 폭발
이 시리즈 4편 어노테이션 까지 다룬 자바 코드 어디서나 null 검사를 안 하면 NPE 위험. 자바 8 Optional 도입의 동기.
Optional 비유 — 봉투 같은 객체
Optional = "값이 들어 있을 수도 있고 비어 있을 수도 있는 봉투". 봉투를 받은 사람은 — 봉투를 열기 전에 "안에 뭔가 있나?" 확인 후 꺼내야 함.
Optional<User> userOpt = userRepository.findByEmail("alice@example.com");
if (userOpt.isPresent()) {
User user = userOpt.get(); // 안전하게 꺼냄
System.out.println(user.getName());
} else {
System.out.println("사용자 없음");
}
null 가능성을 타입 시스템에 명시 — Optional<User> 타입이 "이 값은 없을 수도 있다" 를 말함. 호출자가 null 검사를 "잊을 수 없게" 강제.
Optional 생성
Optional<String> empty = Optional.empty(); // 비어 있음
Optional<String> nonEmpty = Optional.of("Hello"); // 비어 있을 수 없음
Optional<String> nullable = Optional.ofNullable(maybeNull); // null 허용
Optional.of(null) 은 NullPointerException 던짐 — "null이 들어올 수 있는 곳" 엔 무조건 ofNullable. 헷갈리기 쉬운 함정.
값 꺼내기 — 5가지 표준 패턴
(1) isPresent() + get() — 옛 스타일 (지양)
if (userOpt.isPresent()) {
User user = userOpt.get();
process(user);
}
작동은 하지만 "if-else" 가 도로 살아남. Optional 도입 의도 무색. 모던 자바는 다음 패턴들 사용.
(2) ifPresent(Consumer) — 값 있으면 실행
userOpt.ifPresent(user -> process(user));
userOpt.ifPresent(this::process);
값이 있을 때만 실행. 람다 (9편 람다·Stream 에서 깊이).
(3) orElse(default) — 없으면 기본값
User user = userOpt.orElse(User.GUEST);
String name = userRepository.findByEmail(email)
.map(User::getName)
.orElse("Anonymous");
값이 없으면 매개변수의 기본값 반환. 가장 자주 쓰는 패턴.
(4) orElseGet(Supplier) — 없으면 함수 실행
User user = userOpt.orElseGet(() -> createDefaultUser());
orElse 와 비슷한데 — 기본값이 "비싼 객체 생성" 일 때 orElseGet 사용 (값 있으면 생성 안 함, 지연 평가).
(5) orElseThrow(Supplier) — 없으면 예외
User user = userOpt.orElseThrow(() -> new UserNotFoundException(email));
비즈니스 로직에서 "없으면 에러" 가 명확할 때. Spring 백엔드 거의 표준.
map·filter — 변환·필터
Optional은 "한 개 짜리 Stream" 처럼 다룰 수 있어요.
map — 변환
String userName = userRepository.findByEmail(email)
.map(User::getName) // User → String
.orElse("Anonymous");
값이 있으면 함수 적용 → 새 Optional. 없으면 빈 Optional 그대로 통과.
filter — 조건 필터
Optional<User> active = userRepository.findByEmail(email)
.filter(User::isActive); // active만 통과
조건 안 맞으면 빈 Optional.
flatMap — 중첩 Optional 평탄화
Optional<Order> latestOrder = userRepository.findByEmail(email)
.flatMap(User::findLatestOrder); // User → Optional<Order>
map 결과가 또 Optional이면 — flatMap 으로 평탄화. Optional<Optional<Order>> 같은 중첩 회피.
체이닝 — 모던 자바의 진짜 매력
여러 패턴을 한 줄로 묶으면 — 함수형 표현이 완성.
String greeting = userRepository.findByEmail(email) // Optional<User>
.filter(User::isActive) // active만
.map(User::getName) // → String
.map(name -> "안녕하세요, " + name) // → String
.orElse("게스트님 환영합니다"); // 없으면 기본값
7~8줄 if-else가 한 표현식으로. 한국 회사 백엔드 코드의 표준 풍경.
Optional 안티패턴 — 절대 하지 말 것
(1) 필드·매개변수에 Optional 사용
public class User {
private Optional<String> nickname; // ❌ 절대 X
}
public void process(Optional<User> user) { ... } // ❌ 매개변수도 X
Optional은 메서드 반환값 전용. 필드·매개변수에 박는 건 자바 설계자(Brian Goetz) 가 명시적으로 "의도와 다르다" 라고 말함.
(2) isPresent() + get() 박았는데 변환 없을 때
// ❌ 안티패턴
if (userOpt.isPresent()) {
User user = userOpt.get();
process(user);
}
// ✅ 모던 스타일
userOpt.ifPresent(this::process);
(3) Optional.of(null) 호출 가능 코드
// ❌ NullPointerException 폭발
return Optional.of(maybeNullValue);
// ✅ 안전
return Optional.ofNullable(maybeNullValue);
(4) orElse 에 비싼 객체 생성
// ❌ 매번 비싼 객체 생성 (값 있어도 생성됨)
User user = userOpt.orElse(loadDefaultUserFromDB());
// ✅ 지연 평가 — 필요할 때만 생성
User user = userOpt.orElseGet(() -> loadDefaultUserFromDB());
(5) 컬렉션을 Optional로
// ❌ 빈 리스트 vs 비어 있음을 둘 다 표현해 혼란
public Optional<List<Order>> findOrders(Long userId) { ... }
// ✅ 빈 리스트 반환이 표준
public List<Order> findOrders(Long userId) { ... } // 없으면 emptyList
컬렉션은 Collections.emptyList() 가 충분히 "비어 있음" 을 표현해줘서 — Optional로 감쌀 필요 X.
Spring·JPA에서 자주
이 시리즈 45편 Repository 에서 깊이 — JPA가 표준으로 Optional 반환.
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
// 서비스
public User getActiveUser(String email) {
return userRepository.findByEmail(email)
.filter(User::isActive)
.orElseThrow(() -> new UserNotFoundException(email));
}
JPA Repository 메서드가 Optional<T> 반환하는 게 한국 회사 표준. null 반환 메서드는 옛 스타일.
메서드 반환 타입이 Optional<T> 면 — .map(...).orElse(...) 또는 .orElseThrow(...) 한 줄로 처리. isPresent + get 옛 스타일은 손에서 나오는 순간 다시 생각.
한 줄 정리 — 자바 Optional = NPE 방어 봉투. 메서드 반환값 전용. map·filter·orElse·orElseThrow 체이닝이 모던 자바 표준. 필드·매개변수·컬렉션엔 박지 말 것.
시험 직전 한 번 더 — 자바 Optional 입문자가 매번 헷갈리는 것
- Optional = 자바 8+ null 안전 봉투 객체
- 동기 = NullPointerException 회피, 타입 시스템에 null 가능성 명시
- 생성 =
Optional.empty()·Optional.of(value)·Optional.ofNullable(maybeNull) Optional.of(null)= NPE 던짐 (가장 자주 틀리는 함정)- 값 있는지 =
isPresent()·isEmpty()(자바 11+) ifPresent(c)= 값 있으면 실행 (옛 스타일isPresent + get대체)orElse(default)= 없으면 기본값 (즉시 평가)orElseGet(supplier)= 없으면 함수 실행 (지연 평가)orElseThrow(supplier)= 없으면 예외 (한국 회사 백엔드 표준)map(fn)= 값 있으면 변환filter(pred)= 조건 안 맞으면 빈 OptionalflatMap(fn)= 중첩 Optional 평탄화- 안티패턴 1 = 필드·매개변수에 Optional 사용 (절대 X)
- 안티패턴 2 =
isPresent + get패턴 (ifPresent로 대체) - 안티패턴 3 =
Optional.of(null) - 안티패턴 4 =
orElse(비싼객체())(대신orElseGet) - 안티패턴 5 = 컬렉션을 Optional로 감싸기 (빈 리스트로 충분)
- JPA Repository =
Optional<T> findById(id)표준 null반환 메서드 = 옛 스타일, Optional 반환이 모던- 자바 백엔드 = 매일 Optional 다룸
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
다음 글: