JAVA

[Java] Optional로 Null 처리하기

Stark97 2023. 12. 28. 23:57
반응형
 
 

자바8부터 지원하기 시작한 Optional을 사용하여 Null을 처리하는 방법을 예시를 통해 이해해 보자

 

📌 서론

개발을 하다보면 null을 종종 보게 되는데 'null' 값의 관리는 굉장히 중요한 문제 중 하나다. 'null' 값은 종종 예기치 못한 NullPointerException을 일으키며, 이는 애플리케이션의 신뢰성과 안정성을 저하시키기 때문이다. 자바 8에서는 이런 문제를 해결하기 위해 Optional이라는 기능이 등장한다. 이번 포스트에서는 Optional을 사용하여 어떻게 안전하게 'null' 값을 처리할 수 있는지 예제를 통해 알아보자

 

1.  Optional로 조건에 따른 필터링

이 예시는 Optional.filter를 사용하여 특정 조건을 만족하는 값에만 접근하는 방법이다.

public class OptionalIntermediate1 {

    public static void main(String[] args) {
        Optional<String> optionalString = Optional.of("옵셔널 생성");

        // 문자열의 길이가 5보다 큰 경우에만 값을 가져옴
        Optional<String> longString = optionalString.filter(s -> s.length() > 5);

        longString.ifPresent(System.out::println);  // "옵셔널 생성" 출력
    }
}

코드를 단계별로 살펴보자

1. 문자열 "옵셔널 생성"을 포함하는 Optional 객체를 생성한다.

  • Optional.of 메서드를 통해 "옵셔널 생성"이라는 String 값을 Optional 객체로 감싸준다.
Optional<String> optionalString = Optional.of("옵셔널 생성");

2. filter 메소드를 사용하여 optionalString의 값이 특정 조건을 만족하는지 검사한다.

  • 여기서는 문자열의 길이가 5보다 큰지 확인한다. "옵셔널 생성" 문자열의 길이는 5보다 크므로, longString은 값이 포함된 Optional 객체가 된다.
Optional<String> longString = optionalString.filter(s -> s.length() > 5);

3. ifPresent 메소드를 사용하여 longString이 값을 포함하고 있는 경우 (filter에 의해 조건을 만족하는 경우), 그 값을 출력한다.

  • 출력 결과는 "옵셔널 생성"이다.
longString.ifPresent(System.out::println);

 

2.  Optional로 값 변환하기

이 예시는 Optional 안에 또 다른 Optional을 넣은 후 이를 평탄화하는 과정을 보여준다.

public class OptionalIntermediate2 {

    public static void main(String[] args) {
        Optional<String> optionalString = Optional.of("Hello");

        // 문자열을 대문자로 변환
        Optional<String> upperString = optionalString.map(String::toUpperCase);

        upperString.ifPresent(System.out::println);  // "HELLO" 출력
    }
}

코드를 단계별로 살펴보자

1. 이 줄은 "Hello"라는 문자열을 포함하는 Optional 객체 optionalString을 생성한다.

  • Optional.of 메소드는 null이 아닌 값을 Optional 객체로 감싼다.
Optional<String> optionalString = Optional.of("Hello");

2. optionalString 객체에 map 메소드를 적용하여 각 문자열 값을 대문자로 변환한다.

  • String::toUpperCase는 각 문자열에 대한 대문자 변환 메소드 참조이다. 이 map 연산의 결과는 변환된 문자열을 포함하는 새로운 Optional 객체 upperString이다.
Optional<String> upperString = optionalString.map(String::toUpperCase);

📌 map을 사용하면 stream처럼 h,e,l,l,o를 반복해서 처리하는가?

Optional은 하나의 값만을 포함할 수 있으므로, Optional.map은 그 단일 값을 변환하는 데 사용된다. 예를 들어, Optional<String> optionalString = Optional.of("Hello");에서 optionalString은 "Hello"라는 하나의 문자열 값을 포함한다. 이때 optionalString.map(String::toUpperCase);를 호출하면, "Hello"라는 단일 문자열 값이 "HELLO"로 변환된다. 이 과정에서 "Hello" 문자열의 각 문자('H', 'e', 'l', 'l', 'o')가 개별적으로 처리되는 것이 아니라 전체 문자열이 한 번에 대문자로 변환된다.

3. ifPresent 메소드를 사용하여 upperString이 값을 포함하고 있는 경우 (Optional 객체가 비어 있지 않은 경우) 그 값을 출력한다.

  • 이 예제에서는 변환된 문자열 "HELLO"가 출력된다.
upperString.ifPresent(System.out::println);

 

3.  Optional의 flatMap 메서드를 사용한 연쇄 처리

Optional.flatMap을 사용하여 복잡한 구조의 Optional을 단순화하고 연쇄적으로 처리하는 방법이다.

public class OptionalIntermediate3 {

    public static void main(String[] args) {
        // Optional 객체 생성
        Optional<String> optionalString = Optional.of("Example");

        // 여기서 map은 Optional<String>을 Optional<Optional<String>>으로 만든다.
        Optional<Optional<String>> wrapped = optionalString.map(Optional::of);

        // 여기서 flatMap은 Optional<Optional<String>>을 Optional<String>으로 만든다.
        Optional<String> flattened = wrapped.flatMap(o -> o);

        // 결과 출력
        flattened.ifPresent(System.out::println);  // "Example" 출력
    }
}

코드를 단계별로 살펴보자

1. 문자열 "Example"을 포함하는 Optional 객체를 생성한다.

  • optionalString: Optional 객체에 문자열 "Optional"이 담겨 있다. -> Optional["Example"]
Optional<String> optionalString = Optional.of("Example");

2.optionalString에 map 함수를 적용한다.

  • optionalString의 값인 문자열 "Example"을 Optional::of 함수로 다시 감싸 Optional 객체로 만든다. 이제 wrapped는 문자열 "Example"을 포함하는 Optional 객체를 다시 포함하는 Optional 객체다. -> Optional[Optional["Example"]]
Optional<Optional<String>> wrapped = optionalString.map(Optional::of);

3. wrapped는 Optional 객체 안에 또 다른 Optional 객체를 포함하고 있다. flatMap 메소드를 사용하여 이 중첩된 구조를 평탄화한다.

  • wrapped의 각 Optional 요소에 o -> o 함수를 적용하게 된다. 이 함수는 단순히 Optional을 그대로 반환한다. flatMap은 이를 단일 Optional 수준으로 평탄화한다. 결과적으로, flattened는 원래 문자열 "Example"을 포함하는 Optional 객체가 된다. ->  Optional["Example"]
Optional<String> flattened = wrapped.flatMap(o -> o);

📌  잠깐! flatMap은 어떻게 동작하는가?

flatMap은 중첩된 Optional을 제거하고 단일 Optional을 반환하는 역할을 한다. 즉, 여기서는 Optional<Optional<T>>를 Optional<T>로 변환한다. Optional<Optional<String>>에서 flatMap을 사용하면 결과는 Optional<String>이 된다.

4. ifPresent 메소드를 사용하여 flattened가 값이 있을 경우, 즉 Optional이 비어 있지 않을 경우 그 값을 출력한다.

  • 이 코드에서는 "Example"이 출력된다.
flattened.ifPresent(System.out::println);

 

4.  여러 Optional 객체 조합하기

여러 Optional 객체를 조합하여 복잡한 로직을 처리하는 방법이다.

public class OptionalIntermediate4 {

    public static void main(String[] args) {
        Optional<String> firstName = Optional.of("Jin");
        Optional<String> lastName = Optional.empty();

        // 이름과 성을 조합하여 전체 이름 만들기
        String fullName = firstName.flatMap(fName -> lastName.map(lName -> fName + " " + lName))
                .orElse(firstName.orElse("Unknown"));

        System.out.println(fullName);  // "Jin" 출력
    }
}

코드를 단계별로 살펴보자

1. firstName 생성하기

  • "Jin"이라는 문자열을 포함하는 Optional 객체 firstName를 생성한다.
Optional<String> firstName = Optional.of("Jin");

2. lastName 생성하기

  • 값을 포함하지 않는, 즉 비어 있는 Optional 객체 lastName를 생성한다.
Optional<String> lastName = Optional.empty();

3. flatMap을 사용하여 firstName의 값이 있는 경우에만 lastName을 사용하여 전체 이름을 조합한다.

  • lastName이 비어 있기 때문에 fName + " " + lName 이 실행되지 않고, flatMap의 결과는 비어 있는 Optional이 된다. 그 후, orElse 메소드는 비어 있는 Optional에 대해 호출되며, 이 경우 firstName.orElse("Unknown")을 평가한다.  지금 firstName에는 Jin이라는 값이 존재하기(비어있지 않기) 때문에, "Jin"을 반환한다.
String fullName = firstName.flatMap(fName -> lastName.map(lName -> fName + " " + lName)).orElse(firstName.orElse("Unknown"));

4. 따라서 최종 출력 결과는 "Jin"이 된다.

  • 이 예제는 flatMap과 map을 사용하여 Optional 객체들을 조합하는 방법을 보여주며, 특히 한쪽이 비어 있을 때 다른 쪽의 값만 사용하는 경우를 다룬다. 하지만 lastName이 비어 있기 때문에 "Jin " (공백 포함)이 아니라 단순히 "Jin"만 출력된다.

 

만약 firstName이 Optional.empty()면 어떻게 될까?

  • 이 코드의 동작 방식을 살펴보면, flatMap과 map 메소드를 사용한 조건부 조합이 잘못 사용된 것을 확인할 수 있다. firstName이 Optional.empty()이기 때문에, flatMap 메소드 내부의 람다 표현식은 실행되지 않는다. 따라서 orElse 메소드는 firstName의 대체값인 "Unknown"을 반환한다.
public class OptionalIntermediate4 {

    public static void main(String[] args) {
    
        Optional<String> firstName = Optional.empty();
        Optional<String> lastName = Optional.of("Jin");

        // 이름과 성을 조합하여 전체 이름 만들기
        String fullName = firstName.flatMap(fName -> lastName.map(lName -> fName + " " + lName))
                .orElse(firstName.orElse("Unknown"));

        System.out.println(fullName);  // "Unknown" 출력
    }
}

코드를 분석해 보자

1. firstName은 비어 있는 Optional이다.

Optional<String> firstName = Optional.empty();

2. lastName은 "Jin"을 값으로 갖는 Optional이다.

Optional<String> lastName = Optional.of("Jin");

3. firstName이 비어 있으므로, flatMap 내부의 람다 함수는 실행되지 않는다.

  • 따라서 이 표현식의 결과는 여전히 비어 있는 Optional이다.
firstName.flatMap(fName -> lastName.map(lName -> fName + " " + lName))

4. 첫 번째 orElse는 flatMap의 결과가 비어 있을 경우 대체값을 제공한다.

  • 여기서 대체값은 firstName.orElse("Unknown")의 결과값이다. firstName도 비어 있으므로, firstName.orElse("Unknown")은 "Unknown"을 반환한다.
.orElse(firstName.orElse("Unknown"))

5. 결과적으로, 최종 출력 결과는 "Unknown"이 된다.

  • firstName이 비어 있기 때문에 flatMap을 통한 이름 조합은 실행되지 않고, orElse 부분에서 "Unknown"이 사용되어 이 값이 출력된다.

 

 

반응형