Backend/Java

Optional 클래스로 NullPointerException(NPE) 방어하기

미니임 2026. 3. 3. 23:57

 

현대 자바 개발에서 NullPointerException(NPE)은 개발자의 숙명과도 같은 존재였습니다. 런타임에 갑자기 터져 나오는 이 예외는 서비스의 안정성을 해칠 뿐만 아니라, 코드를 if (obj != null) 같은 방어 코드로 도배하게 만들어 가독성을 현저히 떨어뜨리곤 하죠.

이러한 고통을 해결하기 위해 자바 8에서 등장한 **Optional<T>**는 단순히 null을 체크하는 도구가 아닙니다. 이는 '값이 없을 수도 있음'을 타입 시스템을 통해 명시적으로 드러내는 하나의 컨테이너이자, 함수형 프로그래밍 스타일로 안전한 코드를 작성하게 돕는 강력한 도구입니다.


💡 핵심 개념: Optional은 '비어있을 수 있는 상자'입니다 (Deep Dive)

Optional을 이해하는 가장 쉬운 비유는 **'선물 상자'**입니다.

  • 상자 안의 물건: 우리가 실제로 다루고 싶은 객체입니다.
  • 상자 그 자체: Optional 객체입니다.

우리는 선물이 들어있는지 확인하기 위해 상자를 열어보지 않고도, 상자 외부의 인터페이스(메서드)를 통해 "선물이 있으면 포장지를 뜯고, 없으면 다른 선물을 가져와"라는 명령을 미리 전달할 수 있습니다. 이것이 바로 Optional이 제공하는 선언적 프로그래밍의 핵심입니다.

작동 원리를 수식으로 표현하자면, 특정 타입 $T$에 대한 Optional은 다음과 같은 상태 집합으로 정의될 수 있습니다.

$$Optional(T) \in \{ \{t \mid t \in T\}, \emptyset \}$$

즉, $T$ 타입의 요소가 하나 포함된 집합이거나, 공집합($\emptyset$)인 상태 중 하나임을 보장하는 것이죠.


🚀 실전 예제: 이커머스 주문 시스템에서의 NPE 방어

흔히 쓰이는 user.getAddr().getCity() 같은 연속적인 호출은 NPE의 온상입니다. 이를 Optional을 활용해 실제 비즈니스 로직에 녹여보겠습니다.

상황: 사용자 주문 정보에서 배송지 도시 이름을 추출하기

Java
 
public class OrderService {

    /**
     * 주문 ID를 통해 배송지 도시 이름을 안전하게 추출합니다.
     * @param orderId 주문 고유 번호
     * @return 도시 이름 (없을 경우 "Unknown")
     */
    public String getDeliveryCityName(Long orderId) {
        // 1. DB에서 주문을 조회 (결과가 없을 수 있으므로 Optional로 감쌈)
        return findOrderById(orderId)
            // 2. 주문이 존재한다면 배송 정보(Delivery)를 추출 (map 활용)
            .map(Order::getDelivery)
            // 3. 배송 정보가 있다면 주소(Address)를 추출
            .map(Delivery::getAddress)
            // 4. 주소가 있다면 도시(City) 이름을 추출
            .map(Address::getCity)
            // 5. 앞선 과정 중 하나라도 null이 발생했다면 기본값 반환
            .orElse("Unknown");
    }

    private Optional<Order> findOrderById(Long id) {
        // 실제로는 DB 조회 로직이 들어갑니다.
        return Optional.ofNullable(repository.findById(id));
    }
}

🛠 트러블슈팅: Optional 사용 시 주의할 점

  • 성능 이슈: Optional은 객체입니다. 반복문 내부에서 수백만 번씩 생성하면 가비지 컬렉션(GC) 부하가 발생할 수 있습니다. 성능이 극도로 중요한 루프 내부에서는 기본형(Primitive) 체크를 고려하세요.
  • Optional.get() 남용: isPresent()로 체크하고 .get()으로 꺼내는 방식은 기존의 null 체크 방식과 다를 바 없습니다. 가급적 orElse, ifPresent, map 등을 활용하세요.

⚖️ 장단점 및 고려사항 (Trade-offs)

Optional이 만능 열쇠는 아닙니다. 도입 시 다음과 같은 트레이드오프를 반드시 고려해야 합니다.

장점 단점 및 주의사항
명시성: API 설계 시 값이 없을 수 있음을 타입으로 강제함. 직렬화 불가: Optional은 직렬화(Serializable)를 지원하지 않아 DTO의 필드로 부적절함.
가독성: 메서드 체이닝을 통해 로직의 흐름을 한눈에 파악 가능. 오버헤드: 추가적인 객체 생성 비용이 발생함.
안전성: NPE 발생 가능성을 컴파일 타임 혹은 설계 단계에서 인지. 파라미터 사용 금지: 메서드 인자로 Optional을 넘기는 것은 코드 복잡도만 높이는 안 좋은 관습임.

특히, 컬렉션(List, Set 등)을 반환할 때는 Optional<List<T>>를 사용하지 마세요. 비어있는 컬렉션(Collections.emptyList())을 반환하는 것이 클라이언트 입장에서 훨씬 다루기 쉽습니다.


🏁 결론: 도구가 아닌 '약속'입니다

Optional의 진정한 가치는 코드를 짧게 만드는 것이 아니라, 동료 개발자(혹은 미래의 나)에게 **"이 데이터는 없을 수도 있으니 주의해서 다뤄줘"**라고 명확하게 의사를 전달하는 데 있습니다. 무분별한 사용은 오히려 코드를 복잡하게 만들 수 있지만, 적절한 위치에 배치된 Optional은 시스템의 견고함을 지탱하는 든든한 버팀목이 됩니다.

현재 진행 중인 프로젝트의 서비스 계층에서 반환 타입들을 살펴보세요. 혹시 null을 반환하며 클라이언트에게 책임을 전가하고 있지는 않나요?

반응형