공공부하자개발 · 영어 학습 노트
자바
고급모던 자바와 성능0/10 완료
  • 01제네릭과 와일드카드
  • 02멀티스레드와 동기화
  • 03람다식과 함수형 인터페이스
  • 04Stream API와 병렬 처리
  • 05Optional로 NPE 방지
  • 06메서드 활용 패턴 (고급)
  • 07Java 21 모던 문법
  • 08어노테이션·리플렉션·동적 프록시
  • 09CompletableFuture 심화와 가상 스레드 실전
  • 10JVM 메모리·GC·OOM 진단
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 고급 › 05 / 10

Optional로 NPE 방지

섹션 7진행 0 / 10
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

3. 코드 예제

예제 1: orElse vs orElseGet 평가 시점 증명

orElse 의 인자는 값이 있어도 실행된다는 것을 카운터로 확인합니다.

java
import java.util.Optional;

public class OrElseTiming {
    static int calls = 0;

    static String loadDefault(String tag) {
        calls++;
        System.out.println("  loadDefault(" + tag + ") 실행됨");
        return "default-" + tag;
    }

    public static void main(String[] args) {
        Optional<String> present = Optional.of("kim");
        Optional<String> absent = Optional.empty();

        System.out.println("값 있음 + orElse:");
        System.out.println("  → " + present.orElse(loadDefault("A")));      // 값이 있어도 실행
        System.out.println("값 있음 + orElseGet:");
        System.out.println("  → " + present.orElseGet(() -> loadDefault("B")));   // 실행 안 됨
        System.out.println("값 없음 + orElseGet:");
        System.out.println("  → " + absent.orElseGet(() -> loadDefault("C")));    // 실행됨
        System.out.println("총 loadDefault 호출 = " + calls);

        try {
            absent.orElseThrow(() -> new IllegalStateException("사용자 없음"));
        } catch (IllegalStateException e) {
            System.out.println("orElseThrow: " + e.getMessage());
        }
    }
}
// 출력:
// 값 있음 + orElse:
//   loadDefault(A) 실행됨
//   → kim
// 값 있음 + orElseGet:
//   → kim
// 값 없음 + orElseGet:
//   loadDefault(C) 실행됨
//   → default-C
// 총 loadDefault 호출 = 2
// orElseThrow: 사용자 없음

예제 2: Repository findById 체인 — 서비스 계층 패턴

Optional<User> findById 를 받아 map / filter / orElseThrow 로 비즈니스 로직을 체이닝합니다. if-null 이 하나도 없습니다.

java
import java.util.*;

public class RepositoryChain {
    record User(long id, String name, String email, boolean active) {}

    static class UserRepository {
        private final Map<Long, User> db = Map.of(
            1L, new User(1, "kim", "kim@x.com", true),
            2L, new User(2, "lee", null, true),
            3L, new User(3, "park", "park@x.com", false));
        Optional<User> findById(long id) { return Optional.ofNullable(db.get(id)); }
    }

    static class UserService {
        private final UserRepository repo = new UserRepository();

        // 활성 사용자의 이메일 도메인. 없거나 비활성이거나 이메일 없으면 "unknown"
        String emailDomain(long id) {
            return repo.findById(id)
                .filter(User::active)
                .map(User::email)                       // email 이 null 이면 여기서 empty
                .map(e -> e.substring(e.indexOf('@') + 1))
                .orElse("unknown");
        }

        User requireActive(long id) {
            return repo.findById(id)
                .filter(User::active)
                .orElseThrow(() -> new NoSuchElementException("활성 사용자 없음: id=" + id));
        }

        void greet(long id) {
            repo.findById(id).ifPresentOrElse(
                u -> System.out.println("안녕하세요, " + u.name()),
                () -> System.out.println("id=" + id + " 사용자를 찾을 수 없습니다"));
        }
    }

    public static void main(String[] args) {
        UserService svc = new UserService();
        for (long id : new long[]{1, 2, 3, 9}) System.out.println(id + " → " + svc.emailDomain(id));
        svc.greet(1);
        svc.greet(9);
        try { svc.requireActive(3); } catch (NoSuchElementException e) { System.out.println(e.getMessage()); }
    }
}
// 출력:
// 1 → x.com
// 2 → unknown
// 3 → unknown
// 9 → unknown
// 안녕하세요, kim
// id=9 사용자를 찾을 수 없습니다
// 활성 사용자 없음: id=3

예제 3: 설정 값 fallback 체인 — or() 와 Objects.requireNonNullElse

"시스템 프로퍼티 → 환경 변수 → 설정 파일 → 기본값" 순서로 값을 찾는 전형적인 설정 로더입니다.

java
import java.util.*;

public class ConfigFallback {
    static final Map<String, String> FILE_CONFIG = Map.of("db.host", "file-host", "db.port", "5432");

    static Optional<String> fromSystemProperty(String key) { return Optional.ofNullable(System.getProperty(key)); }
    static Optional<String> fromEnv(String key) { return Optional.ofNullable(System.getenv(key.toUpperCase().replace('.', '_'))); }
    static Optional<String> fromFile(String key) { return Optional.ofNullable(FILE_CONFIG.get(key)); }

    static String resolve(String key, String defaultValue) {
        return fromSystemProperty(key)
            .or(() -> fromEnv(key))          // 비어 있을 때만 다음 소스 시도 (지연 평가)
            .or(() -> fromFile(key))
            .orElse(defaultValue);
    }

    static int resolveInt(String key, int defaultValue) {
        return fromSystemProperty(key).or(() -> fromFile(key))
            .map(String::strip)
            .filter(s -> s.matches("\\d+"))  // 숫자가 아니면 empty → 기본값
            .map(Integer::parseInt)
            .orElse(defaultValue);
    }

    public static void main(String[] args) {
        System.setProperty("db.host", "prop-host");
        System.out.println("db.host = " + resolve("db.host", "localhost"));      // 프로퍼티 우선
        System.out.println("db.port = " + resolveInt("db.port", 3306));          // 파일
        System.out.println("db.user = " + resolve("db.user", "app"));            // 기본값
        System.setProperty("db.port", "abc");
        System.out.println("db.port(bad) = " + resolveInt("db.port", 3306));     // 검증 실패 → 기본값

        // Optional 이 과한 곳: 단순 null 기본값은 Objects 로
        String nick = null;
        System.out.println(Objects.requireNonNullElse(nick, "anonymous"));
    }
}
// 출력:
// db.host = prop-host
// db.port = 5432
// db.user = app
// db.port(bad) = 3306
// anonymous

예제 4: 중첩 DTO null 안전 접근 + 스트림과 결합

외부 API 응답처럼 어느 단계든 null 일 수 있는 중첩 구조를 map 체인으로 안전하게 파고들고, Optional::stream 으로 여러 결과에서 빈 값을 걸러냅니다.

java
import java.util.*;
import java.util.stream.*;

public class NestedDto {
    record Geo(Double lat, Double lng) {}
    record Address(String city, Geo geo) {}
    record Company(String name, Address address) {}
    record Employee(String name, Company company) {}

    static String companyCity(Employee e) {
        return Optional.ofNullable(e)
            .map(Employee::company)
            .map(Company::address)
            .map(Address::city)          // 어느 단계든 null 이면 여기까지 empty 로 흘러감
            .orElse("(no city)");
    }

    static OptionalDouble latitude(Employee e) {
        return Optional.ofNullable(e)
            .map(Employee::company).map(Company::address).map(Address::geo).map(Geo::lat)
            .map(OptionalDouble::of).orElse(OptionalDouble.empty());
    }

    public static void main(String[] args) {
        List<Employee> staff = Arrays.asList(
            new Employee("kim", new Company("ACME", new Address("Seoul", new Geo(37.5, 127.0)))),
            new Employee("lee", new Company("Beta", null)),
            new Employee("park", null),
            null,
            new Employee("choi", new Company("Gamma", new Address("Busan", null))));

        for (Employee e : staff) System.out.println((e == null ? "null" : e.name()) + " → " + companyCity(e));

        // Optional::stream 으로 있는 값만 모으기
        List<String> cities = staff.stream()
            .map(e -> Optional.ofNullable(e).map(Employee::company).map(Company::address).map(Address::city))
            .flatMap(Optional::stream)
            .toList();
        System.out.println("cities = " + cities);

        // 스트림 최종 연산의 Optional
        OptionalDouble avgLat = staff.stream().map(NestedDto::latitude)
            .filter(OptionalDouble::isPresent).mapToDouble(OptionalDouble::getAsDouble).average();
        System.out.println("avgLat = " + avgLat.orElse(Double.NaN));
    }
}
// 출력:
// kim → Seoul
// lee → (no city)
// park → (no city)
// null → (no city)
// choi → Busan
// cities = [Seoul, Busan]
// avgLat = 37.5

예제 5: JDK 21 대안 — sealed 결과 타입과 패턴 매칭

"없음"의 이유가 여러 가지일 때 Optional 대신 sealed 타입을 쓰고, switch 완전성 검사와 case null 을 활용합니다. record 생성자의 requireNonNull 도 함께 봅니다.

java
import java.util.*;

public class SealedLookup {
    record Account(String id, long balance) {
        Account { Objects.requireNonNull(id, "id 는 필수"); }     // 생성 시점에 null 차단
    }

    sealed interface Lookup permits Found, NotFound, Frozen {}
    record Found(Account account) implements Lookup {}
    record NotFound(String id) implements Lookup {}
    record Frozen(String id, String reason) implements Lookup {}

    static final Map<String, Account> DB = Map.of("A1", new Account("A1", 500), "A2", new Account("A2", 0));
    static final Set<String> FROZEN = Set.of("A2");

    static Lookup find(String id) {
        if (FROZEN.contains(id)) return new Frozen(id, "의심 거래");
        Account a = DB.get(id);
        return a == null ? new NotFound(id) : new Found(a);
    }

    static String describe(Object o) {                              // nullable 값 분기: case null
        return switch (o) {
            case null -> "null 입력";
            case Found f when f.account().balance() > 0 -> "잔액 " + f.account().balance();
            case Found f -> "잔액 없음";
            case NotFound n -> "계좌 없음: " + n.id();
            case Frozen z -> "동결(" + z.reason() + "): " + z.id();
            default -> "알 수 없음: " + o;
        };
    }

    public static void main(String[] args) {
        for (String id : new String[]{"A1", "A2", "A9"}) System.out.println(describe(find(id)));
        System.out.println(describe(null));
        try { new Account(null, 1); } catch (NullPointerException e) { System.out.println("NPE: " + e.getMessage()); }
    }
}
// 출력:
// 잔액 500
// 동결(의심 거래): A2
// 계좌 없음: A9
// null 입력
// NPE: id 는 필수

예제 직접 실행

아래 폴더를 JDK 21 로 컴파일하고 실행합니다.

cd java-src\advanced\05_optional
javac -encoding UTF-8 *.java && java Main
코드 예제
  • 예제 1: orElse vs orElseGet 평가 시점 증명
  • 예제 2: Repository findById 체인 — 서비스 계층 패턴
  • 예제 3: 설정 값 fallback 체인 — or() 와 Objects.requireNonNullElse
  • 예제 4: 중첩 DTO null 안전 접근 + 스트림과 결합
  • 예제 5: JDK 21 대안 — sealed 결과 타입과 패턴 매칭
이전 섹션2 핵심 원리3 / 7다음 섹션4 응용 변형 예제