자바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"이 사용되어 이 값이 출력된다.
'JAVA' 카테고리의 다른 글
[Java] HTTP 서버 만들기: GET, POST, PUT 요청별 처리 (28) | 2023.12.30 |
---|---|
[Java] HTTP 서버 구현: postman과 자바 HttpClient를 사용한 요청 (23) | 2023.12.30 |
[Java] 자바 리플렉션(Reflection) 실습하기 (1) | 2023.11.18 |
[Java] 자바 리플렉션(reflection)이란? (1) | 2023.11.15 |
[Java] 추상화란 무엇인가? (1) | 2023.11.02 |