200===Dev Language/Java

Java Optional과 flatMap - null 안전하게 다루기 🎁

블로글러 2025. 4. 14. 13:47

안녕하세요! 여러분은 자바 프로그래밍 중 NullPointerException(NPE) 때문에 골치 아팠던 경험, 있으신가요? 😫 마치 선물을 열었는데 텅 비어있는 상자를 받은 기분이랄까요? Java 8에서 등장한 Optional은 이런 NPE 지옥에서 우리를 구원해 줄 멋진 친구랍니다. 오늘은 Optional이 무엇이고, 특히 flatMap은 언제 어떻게 사용하는지 쉽고 재미있게 알아볼게요!

등장 배경

예전에는 메서드가 값을 반환하지 못하는 경우 null을 리턴하는 경우가 많았어요. 개발자는 이 null을 제대로 처리하지 않으면 예기치 않은 NPE를 만나 프로그램이 중단되는 상황을 겪어야 했죠. 💥

// 예전 방식: 매번 null 체크가 필요
String userName = getUserNameById(123);
if (userName != null) {
System.out.println(userName.toUpperCase()); // userName이 null이면 여기서 NPE 발생!
} else {
System.out.println("사용자를 찾을 수 없습니다.");
}

// 수많은 null 체크 코드가 반복되고, 실수하기 쉬웠죠.

이런 문제를 해결하기 위해 Java 8에서는 "값이 없을 수도 있음"을 명시적으로 표현하는 Optional 클래스를 도입했습니다. Optional은 객체를 감싸는 '포장지' 같은 역할을 해요. 포장지를 열어보기 전까지는 안에 진짜 값이 있는지, 아니면 비어있는지 알 수 없지만, 적어도 NPE 걱정 없이 안전하게 내용물을 확인할 방법을 제공하죠. 🛡️

Optional과 flatMap: 왜 필요할까? 🤔

Optional은 다음과 같은 문제를 해결하고 코드를 더 명확하게 만들어줍니다.

  1. Null Pointer Exception 방지: Optional 객체를 사용하면 값이 없는 경우를 위한 로직을 명시적으로 작성하게 되어 NPE 발생 가능성을 크게 줄여줍니다.
  2. 가독성 향상: 메서드 시그니처에 Optional<T>를 반환 타입으로 사용하면, 이 메서드가 값을 반환하지 않을 수도 있다는 사실을 API 사용자에게 명확하게 알려줍니다.
  3. 명시적인 처리 강제: Optional 객체에서 값을 얻으려면 .get(), .orElse(), .orElseThrow() 등 명시적인 메서드를 사용해야 하므로, 값이 없는 경우를 어떻게 처리할지 고민하게 만듭니다.

핵심 원리: Optional과 map vs flatMap

Optional은 내부에 값이 있을 수도 있고(Present), 없을 수도 있는(Empty) 상태를 가지는 컨테이너 객체입니다.

+-----------------+      +-----------------+
| Optional        |      | Optional        |
|                 |      |                 |
|  Value: "Hello" |  or  |  Value: null    |
| (Present)       |      | (Empty)         |
+-----------------+      +-----------------+

Optional 객체에 담긴 값에 어떤 연산을 적용하고 싶을 때 map()이나 flatMap()을 사용하는데, 이 둘의 차이가 중요합니다!

  • map(Function<? super T, ? extends U> mapper): Optional에 값이 있다면, 주어진 함수(mapper)를 값에 적용하고 그 결과를 다시 Optional로 감싸서 반환합니다. 만약 원래 Optional이 비어있다면, 그냥 비어있는 Optional을 반환해요.

    • 핵심: map의 결과는 항상 Optional로 한 번 더 감싸집니다. 만약 mapper 함수 자체가 Optional을 반환하면, 결과는 Optional<Optional<T>> 형태가 됩니다. (이중 포장!)
  • flatMap(Function<? super T, Optional<? extends U>> mapper): map과 비슷하지만, mapper 함수가 반드시 Optional을 반환해야 합니다. flatMap은 mapper가 반환한 Optional을 그대로 반환합니다. 즉, map과 달리 결과가 이중으로 감싸지지 않아요. (포장지를 한 겹 벗겨줌!)

언제 map을 쓰고, 언제 flatMap을 쓸까요? 🤔

- 변환하려는 함수가 **일반 객체 (`T`)** 를 반환하면 `map`을 사용하세요.
- 변환하려는 함수가 **Optional<T>** 를 반환하면 `flatMap`을 사용하세요. `flatMap`이 불필요한 `Optional` 중첩(Optional;Optional<T>)을 막아줍니다.

코드 예시로 살펴보기:

import java.util.Optional;

class User {
    private String name;
    private Optional<Address> address; // 주소는 Optional일 수 있음

    public User(String name, Optional<Address> address) {
        this.name = name;
        this.address = address;
    }

    public String getName() { return name; }
    public Optional<Address> getAddress() { return address; } // Optional<Address> 반환
    public Optional<String> getNameOptional() { return Optional.ofNullable(name); } // 그냥 String 말고 Optional<String> 반환
}

class Address {
    private String street;

    public Address(String street) { this.street = street; }
    public String getStreet() { return street; } // 그냥 String 반환
    public Optional<String> getStreetOptional() { return Optional.ofNullable(street); } // Optional<String> 반환
}

public class OptionalExample {
    public static void main(String[] args) {
        User userWithAddress = new User("Alice", Optional.of(new Address("123 Main St")));
        User userWithoutAddress = new User("Bob", Optional.empty());
        Optional<User> optUserWithAddress = Optional.of(userWithAddress);
        Optional<User> optUserWithoutAddress = Optional.of(userWithoutAddress);
        Optional<User> optEmptyUser = Optional.empty();

        // --- map 사용 예시 ---
        // User -> String (getName은 String 반환) -> map 사용
        Optional<String> userName = optUserWithAddress.map(User::getName); // 결과: Optional["Alice"]
        System.out.println("User name (map): " + userName);

        // --- flatMap 사용 예시 ---
        // User -> Optional<Address> (getAddress는 Optional<Address> 반환) -> flatMap 사용
        Optional<Address> userAddress = optUserWithAddress.flatMap(User::getAddress); // 결과: Optional[Address[street=123 Main St]]
        System.out.println("User address (flatMap): " + userAddress);

        // 만약 여기서 map을 쓰면? Optional<Optional<Address>>가 됨 (이중 포장!)
        Optional<Optional<Address>> nestedOptional = optUserWithAddress.map(User::getAddress);
        System.out.println("User address (map - nested): " + nestedOptional);

        // --- 연쇄 호출 (Chaining) ---
        // User -> Optional<Address> -> Optional<String> (getStreetOptional 사용)
        Optional<String> street = optUserWithAddress
                                    .flatMap(User::getAddress) // Optional<Address>
                                    .flatMap(Address::getStreetOptional); // Optional<String>
        System.out.println("User street (flatMap chain): " + street); // 결과: Optional["123 Main St"]

        // User -> Optional<Address> -> String (getStreet 사용 - 일반 객체 반환)
        // 이 경우 마지막은 map을 사용
        Optional<String> streetUsingMap = optUserWithAddress
                                          .flatMap(User::getAddress) // Optional<Address>
                                          .map(Address::getStreet);    // Optional<String> (map이 String을 Optional로 감쌈)
        System.out.println("User street (flatMap + map): " + streetUsingMap); // 결과: Optional["123 Main St"]


        // 주소 없는 유저의 경우
        Optional<String> noStreet = optUserWithoutAddress
                                       .flatMap(User::getAddress) // Optional.empty() 반환
                                       .flatMap(Address::getStreetOptional); // 실행되지 않음
        System.out.println("No street (flatMap chain): " + noStreet); // 결과: Optional.empty

        // 비어있는 Optional<User>의 경우
         Optional<String> emptyUserStreet = optEmptyUser
                                       .flatMap(User::getAddress) // Optional.empty() 반환
                                       .flatMap(Address::getStreetOptional); // 실행되지 않음
        System.out.println("Empty user street (flatMap chain): " + emptyUserStreet); // 결과: Optional.empty
    }
}

map vs flatMap 요약:

메서드 매핑 함수(mapper) 반환 타입 최종 결과 타입 (입력이 Optional) 중첩 Optional 주요 용도
map U (일반 객체) Optional<U> X Optional 내부 값 변환 (결과가 일반 객체)
map Optional<U> Optional<Optional<U>> O (발생) (거의 사용 안 함)
flatMap Optional<U> Optional<U> X (해결) Optional 내부 값 변환 (결과가 Optional)
# map: 상자 안의 내용물을 바꾸고 다시 포장
Optional<User> [👤] -- map(getName) --> Optional<String> ["Alice"]

# flatMap: 상자 안의 내용물을 '다른 상자'로 바꾸고, 기존 상자는 버림
Optional<User> [👤] -- flatMap(getAddressOpt) --> Optional<Address> [🏠]
# 만약 map을 쓰면: Optional<Optional<Address>> [[🏠]] <- 상자 속 상자!

# 연쇄 호출 (flatMap -> map)
Optional<User> [👤] -- flatMap(getAddressOpt) --> Optional<Address> [🏠] -- map(getStreet) --> Optional<String> ["123 Main St"]

주의사항 및 팁 💡

⚠️ 이것만은 주의하세요!

  1. Optional 필드 사용 자제: 클래스의 필드 타입으로 Optional을 사용하는 것은 일반적으로 권장되지 않습니다. 직렬화 문제 등이 발생할 수 있고, 필드 자체가 없을 수 있다는 의미인지, 필드의 이 없을 수 있다는 의미인지 모호해집니다. 대신 필드 값 자체를 null로 두고, getter 메서드에서 Optional.ofNullable()을 반환하는 것이 좋습니다.
  2. 메서드 파라미터로 Optional 사용 자제: 메서드 파라미터로 Optional을 받는 것은 코드를 더 복잡하게 만들 수 있습니다. 그냥 null을 허용하는 일반 타입으로 받고, 메서드 내부에서 Optional.ofNullable()로 감싸거나, 메서드 오버로딩을 사용하는 것이 나을 수 있습니다.
  3. Optional.get() 직접 사용 주의: get()Optional이 비어있을 때 NoSuchElementException을 발생시킵니다. isPresent()로 확인하거나, orElse(), orElseGet(), orElseThrow() 등 안전한 메서드를 사용하는 것이 좋습니다.
  4. orElse(null) 사용 금지: Optional을 쓰는 의미가 없어집니다. 값이 없을 때 null을 반환해야 한다면 Optional을 사용하지 않는 것이 나을 수 있습니다.

💡 꿀팁

  • 값이 없을 때 기본값 제공: .orElse(defaultValue)를 사용하세요. Optional이 비어있으면 defaultValue를 반환합니다.
  • 값이 없을 때 동적으로 기본값 생성: .orElseGet(() -> createDefaultValue())를 사용하세요. Optional이 비어있을 때만 람다식이 실행되어 값을 생성하므로, 기본값 생성이 복잡하거나 비용이 클 때 유용합니다.
  • 값이 없을 때 예외 발생: .orElseThrow(() -> new CustomException())를 사용하세요. Optional이 비어있으면 지정된 예외를 발생시킵니다.
  • Stream API와 함께 사용: Optional은 Stream과 잘 어울립니다. 예를 들어, Optional을 Stream으로 변환(optional.stream())하거나, Stream의 최종 연산 결과(findFirst(), findAny())는 Optional을 반환합니다.

마치며

지금까지 Java의 OptionalflatMap에 대해 알아보았습니다. 처음에는 mapflatMap의 차이가 조금 헷갈릴 수 있지만, '결과를 다시 Optional로 감싸는가 (map)', '이미 Optional인 결과를 그대로 사용하는가 (flatMap)'로 기억하면 구분이 쉬울 거예요! 😉 Optional을 올바르게 사용하면 NPE 걱정 없는 안전하고 읽기 좋은 코드를 작성하는 데 큰 도움이 될 것입니다.

혹시 Optional을 사용하면서 궁금했던 점이나 더 알고 싶은 부분이 있다면 언제든지 질문해주세요! 🙋‍♀️

참고 자료 🔖


#Java #Optional #flatMap #NPE #NullPointerException #Java8

728x90
반응형