# Java: Null 안정성에 좋은 Optional 클래스, 이제 이렇게 써야 깔끔합니다.

> Optional은 무엇인가요?
> 
> Optional 때문에 코드가 난잡하게 된다면 어떻게 해야 할까요?

# 글의 발단

무분별한 옵셔널 사용으로 코드가 깔끔하지 않다고 느낄 때가 있다는 사연을 예전에 들은 적이 있습니다.

그래서 저는 어떤 이유로 어떻게 관리하고 있는지 설명을 했고, 이를 결국 글로도 남기기로 결정했습니다.

# Java Optional이란?

> 자바의 NULL 안정성을 위한 표준 API 클래스입니다.
> 
> null일 수도 있는 값을 감싸서(박싱 해서) 사용한다는 점에서 마치 래퍼 클래스(wrapper class)와 닮았습니다. 래퍼 클래스보다는 박싱된 클래스라고 표현하는 것이 더 알맞습니다. 래퍼 클래스는 이미 기본 타입에 대응하는 참조 타입을 가리키는 표현으로 사용되기 때문입니다.

다른 언어들은 젊다 보니, null 안정성을 보장하기 위한 여러 신택스가 있습니다. 연산자 수준에서 이미 지원하고 있다는 말이죠.

```javascript
// javascript

// 1) 객체가 nullish인 경우
const result = someObject?.field;

// 2) 갖고 오려는 값이 nullish일 때 대체값
const result = someNullableData ?? 'NOT_FOUND';

// 3) 객체 또는 필드 중 하나라도 nullish일 때 대체값
const result = someObject?.nullableField ?? 'NOT_FOUND';
```

코틀린도 null-safety 연산자들을 지원합니다.

```kotlin
// kotlin
val result = someNullableData ?: 'NOT_FOUND'

someObject?.field = 'abc'

item?.let { println(it) }
```

그런데 자바는 버전을 꾸준히 올리고 있음에도 불구하고, 정말 중요한 기능들은 어느 주기로 특정 버전에 담는 식으로 발전의 여지를 남겨 가며 업그레이드되고 있습니다.

그나마 null 처리를 편하게 수행할 수 있도록, 데이터를 박싱해서 null 여부에 관해 관리해 주는 옵셔널 클래스가 존재합니다. (JDK 1.8 이상)

## Optional 인스턴스 생성

Optional 인스턴스는 다른 값을 '감싸며' 생성합니다. 박싱이라고 표현합니다.

래퍼 클래스의 주요 개념인 박싱/언박싱과 거의 같은 관점입니다.

### 생성(박싱) 방식 분류

우리가 직접 생성하는 경우도 있을 수 있습니다. 이 경우 다음 세 경우에 따른 방식이 기본입니다.

* null이 아니라는 것을 알 때
    
* null이라는 것을 알 때
    
* null인지 아닌지 긴가민가할 때
    

```java
// null이 아닌 것으로 보장된 객체를 박싱
Optional<Item> optionalItem = Optional.of(nonNullItem);

// null이 분명한 경우, null을 넣지 말고 Optional.empty() 사용
Optional<Item> optionalItem = Optional.empty();

// null일 수도 있고 아닐 수도 있는 경우
Optional<Item> optionalItem = Optional.ofNullable(item);
```

### 언박싱

누구나 설레는 언박싱 시간입니다. 평범하게 `get()`도 있지만, 그런 식으로 뻔한 언박싱보다는 대체값, 예외 등 `null`일 때의 조치를 포함하는 언박싱 함수들이 더 좋습니다.

```java
// Optional<Item> optionalItem에 대하여

// non-null임을 분명하게 알 때
Item item = optionalItem.get();

// nullable일 때, null 대체 값
Item item = optionalItem.orElse(대체값);

// nullable일 때, null 대체 값 생성
Item item = optionalItem.orElseGet(대체값을 반환하는 함수);

// nullable일 때, null인 경우의 예외 발생
Item item = optionalItem.orElseThrow(예외를 반환하는 함수 또는 그 생성자);
```

이중에서 저는 `.orElseThrow`를 사용하는 일이 많았습니다. 특히 에러 코드 관리에서 상성이 좋게 되어 있다면 이렇게 활용할 수 있습니다.

```java
Item item = optionalItem
        .orElseThrow(MyErrorCode.EXAMPLE::defaultException);
```

* 제 경우는 `ErrorCode`를 인터페이스로 만듭니다.
    
* 그리고 리소스나 피처 단위에서 세부적인 `enumErrorCode`를 만들어 인터페이스를 구현합니다.
    
* `ErrorCode` 인터페이스에는 `defaultException` 메서드가 있으며, `RuntimeException` 또는 그 하위 예외 클래스를 반환합니다.
    

# 일반 아키텍처에서 깔끔하게 관리하기

다시 글의 배경으로 돌아와서, 무분별한 옵셔널 사용으로 코드가 깔끔하지 않다고 느낀다는 사연에 대해 어떤 솔루션을 제시할 수 있을까요?

거창하게 솔루션이라고 할 것도 아닙니다만, 간단히 우리의 여러 아키텍처를 생각해 봅시다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1716475511171/582aa80c-5bb7-4ebd-97ef-60836e9abe38.png align="center")

제가 제안했던 것은,

1. 서비스 레이어는 기능의 구체적인 부분을 구현하는 영역이기 때문에, Exception 등 가용한 수단이 많이 있다는 전제를 두고 이야기를 했습니다.
    
2. **레포지터리는 우리가 구현하려는 구체적인 기능에 결정권을 가지면 안 되기 때문에, 옵셔널 객체로 반환하여 선택권을 상위 레이어에 넘기지만, 서비스에서부터는 선택권을 갖는다는 것이죠.**
    
3. **그래서 레포지터리에서는 반환에 옵셔널을 사용하되, 서비스에서 반환 시 옵셔널을 언박싱하는 것이 일반적인 코드 작성 규칙으로 편해 보인다는 것이었습니다.**
    

특히 저희는 ErrorCode를 인터페이스로 두고, 구체적인 ErrorCode enum을 적절한 리소스나 피처 단위로 따로 관리하기 때문에 앞서 소개한 ErrorCode를 통한 `.orElseThrow(...)` 구현이 매우 쉬운 상태이기도 합니다.

그리고 GlobalExceptionHandler 등에서 이 예외를 캐치하여 반환할 거니까, 정상 응답 로직과 예외 응답 로직을 적절한 수준에서 구분해 둘 수 있었습니다.
