Optional
public final class Optional<T> {
// If non-null, the value; if null, indicates no value is present
private final T value;
// 생략
}
Java8에서는 Optional<T> 클래스를 사용해 NPE를 방지할 수 있도록 도와줍니다. 즉, Optional<T>는 null이 올 수 있는 값을 감싸는 Wrapper 클래스로, 참조하더라도 NPE가 발생하지 않도록 도와줍니다. Optional 클래스는 아래와 같은 value에 값을 저장하기 때문에 값이 null이더라도 바로 NPE가 발생하지 않으며, 클래스이기 때문에 각종 메서드를 제공합니다.
🏷️ NPE(NullPointerException)
// names가 null이라면 NPE가 발생할 수 있습니다.
List<String> names = getNames();
names.sort();
List<String> names = getNames();
// 따라서 NPE를 방지하기 위해 null 검사를 해야합니다.
if(names != null){
names.sort();
}
개발을 할 때 가장 많이 발생하는 예외 중 하나가 바로 NPE(NullPointerException)입니다. NPE를 피하려면 null 여부를 검사해야 하는데, null 검사를 해야하는 변수가 많은 경우 코드가 복잡해지고 번거로워집니다. 그래서 null 대신 초기값을 사용하길 권장하기도 합니다.
🏷️ 활용
✅ Optional 생성
1. Optional.empty()
Optional<String> optional = Optional.empty();
System.out.println(optional); // Optional.empty
System.out.println(optional.isPresent()); // false
값이 null인 경우로 Optional은 Wrapper 클래스이기 때문에 값이 없을 수도 있는데, 이때는 Optional.empty()로 생성할 수 있습니다.
public final class Optional<T> {
private static final Optional<?> EMPTY = new Optional<>();
private final T value;
private Optional() {
this.value = null;
}
// 생략
}
Optional 클래스는 내부에서 static 변수로 EMPTY 객체를 미리 생성해서 가지고 있습니다. 이러한 이유로 빈 객체를 여러 번 생성해줘야 하는 경우에도 1개의 EMPTY 객체를 공유함으로써 메모리를 절약하고 있습니다.
2. Optional.of()
// Optional의 value는 절대 null이 아닙니다.
Optional<String> optional = Optional.of("MyName");
값이 null이 아닌 경우로 만약 어떤 데이터가 절대 null이 아니라면 Optional.of()로 생성할 수 있습니다. 만약 Optional.of()로 Null을 저장하려고 하면 NullPointerException이 발생하게 됩니다.
3. Optional.ofNullbale()
// Optional의 value는 값이 있을 수도 있고 null 일 수도 있습니다.
Optional<String> optional = Optional.ofNullable(getName());
String name = optional.orElse("anonymous"); // 값이 없다면 "anonymous" 를 리턴합니다
값이 Null일수도, 아닐수도 있는 경우로 만약 어떤 데이터가 null이 올 수도 있고 아닐 수도 있는 경우에는 Optional.ofNullbale로 생성할 수 있습니다. 그리고 이후에 orElse 또는 orElseGet 메소드를 이용해서 값이 없는 경우라도 안전하게 값을 가져올 수 있습니다.
✔️ 예시코드
기존에는 아래와 같이 null 검사를 한 후에 null일 경우에는 새로운 객체를 생성해주어야 했다. 이러한 과정을 코드로 나타내면 다소 번잡해보이는데, Optional<T>와 Lambda를 이용하면 해당 과정을 보다 간단하게 표현할 수 있다.
// Java8 이전
List<String> names = getNames();
List<String> tempNames = list != null
? list
: new ArrayList<>();
// Java8 이후
List<String> nameList = Optional.ofNullable(getNames())
.orElseGet(() -> new ArrayList<>());
두번째 예시로는 아래와 같은 우편번호를 꺼내는 null 검사 코드가 있다고 하면 이 코드는 null 검사 때문에 상당히 복잡합니다.
// Optional 없이 표현할 경우
public String findPostCode() {
UserVO userVO = getUser();
if (userVO != null) {
Address address = user.getAddress();
if (address != null) {
String postCode = address.getPostCode();
if (postCode != null) {
return postCode;
}
}
}
return "우편번호 없음";
}
// Optional로 표현할 경우
public String findPostCode() {
// 위의 코드를 Optional로 펼쳐놓으면 아래와 같습니다.
Optional<UserVO> userVO = Optional.ofNullable(getUser());
Optional<Address> address = userVO.map(UserVO::getAddress);
Optional<String> postCode = address.map(Address::getPostCode);
String result = postCode.orElse("우편번호 없음");
// 그리고 위의 코드를 다음과 같이 축약해서 쓸 수 있습니다.
String result = user.map(UserVO::getAddress)
.map(Address::getPostCode)
.orElse("우편번호 없음");
}
세번째 예시로는 이름을 대문자로 변경하는 코드에서 NPE 처리를 해준는 코드입니다.
// Optional을 사용하지 않는 경우
String name = getName();
String result = "";
try {
result = name.toUpperCase();
} catch (NullPointerException e) {
throw new CustomUpperCaseException();
}
// Optional을 사용한 경우
Optional<String> nameOpt = Optional.ofNullable(getName());
String result = nameOpt.orElseThrow(CustomUpperCaseExcpetion::new)
.toUpperCase();
이와 같이 가독성이 좋도록 변경이 가능합니다.
🏷️ 정리
Optional은 null 또는 값을 감싸서 NPE로부터 부담을 줄이기 위해 등장한 Wrapper 클래스입니다. Optional은 값을 Wrapping하고 다시 풀고, null 일 경우에는 대체하는 함수를 호출하는 등의 오버헤드가 있으므로 잘못 사용하면 시스템 성능이 저하되게 됩니다. 그렇기 때문에 메소드의 반환 값이 절대 null이 아니라면 Optional을 사용하지 않는 것이 좋습니다. 즉, Optional은 메소드의 결과가 null이 될 수 있으며, null에 의해 오류가 발생할 가능성이 매우 높을 때 반환값으로만 사용되어야 합니다. 또한 Optional은 파라미터로 넘어가는 등이 아니라 반환 타입으로써 제한적으로 사용되도록 설계되어있습니다.
🏷️ orElse와 orElseGet 차이
Optional API의 단말 연산에는 orElse와 orElseGet 함수가 있고, 이 비슷해 보이는 두 함수는 엄청난 차이가 있습니다.
▪️ orElse: 파라미터로 값을 받습니다.
▪️ orElseGet: 파라미터로 함수형 인터페이스(함수)를 받습니다.
실제로 Optional 코드를 보면 다음과 orElse와 orElseGet이 각각 구현되어 있음을 확인할 수 있습니다.
public final class Optional<T> {
// 생략
public T orElse(T other) {
return value != null ? value : other;
}
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
}
orElse로는 값이, orElseGet로는 함수가 넘어간다는 것은 상당히 큰 차이가 있습니다. 이로 인해 호출 결과가 달라질 수 있기 때문입니다.
✔️ 예시코드
예를 들어 다음과 같은 예시 코드와 같이 첫 번째 함수는 값이 비어있을 때 orElse를 호출하도록 되어있고, 두 번째 함수는 orElseGet을 호출하도록 되어있습니다.
public void findUserEmailOrElse() {
String userEmail = "Empty";
String result = Optional.ofNullable(userEmail)
.orElse(getUserEmail());
System.out.println(result);
}
public void findUserEmailOrElseGet() {
String userEmail = "Empty";
String result = Optional.ofNullable(userEmail)
.orElseGet(this::getUserEmail);
System.out.println(result);
}
private String getUserEmail() {
System.out.println("getUserEmail() Called");
return "mrk0607@tistory.com";
}
출력
1. orElse인 경우
getUserEmail() Called
Empty
2. orElseGet인 경우
Empty
먼저 OrElse인 경우에는 다음과 같은 순서로 처리가 됩니다.
1. Optional.ofNullable로 "EMPTY"를 갖는 Optional 객체 생성합니다.
2. getUserEmail()가 실행되어 반환값을 orElse 파라미터로 전달합니다.
3. orElse가 호출됨, "EMPTY"가 Null이 아니므로 "EMPTY"를 그대로 가집니다.
위와 같이 동작하는 이유는 Optional.orElse()가 값을 파라미터로 받고, orElse 파라미터로 값을 넘겨주기 위해 getUserEmail()이 호출되었기 때문입니다.
하지만 함수형 인터페이스(함수)를 파라미터로 받는 orElseGet에서는 동작이 달라집니다.
1. Optional.ofNullable로 "EMPTY"를 갖는 Optional 객체 생성합니다.
2. getUserEmail() 함수 자체를 orElseGet 파라미터로 전달합니다.
3. orElseGet이 호출됨, "EMPTY"가 Null이 아니므로 "EMPTY"를 그대로 가지며 getUserEmail()이 호출되지 않습니다.
orElseGet에서는 파라미터로 넘어간 값인 getUserEmail 함수가 Null이 아니므로 .get에 의해 함수가 호출되지 않습니다. 만약 다음과 같이 Optional의 값으로 null이 있다면, 다음과 같은 흐름에 의해 orElseGet의 파라미터로 넘어온 getUserEmail()이 실행됩다.
public void findUserEmailOrElseGet() {
String result = Optional.ofNullable(null)
.orElseGet(this::getUserEmail);
System.out.println(result);
}
private String getUserEmail() {
System.out.println("getUserEmail() Called");
return "mrk0607@tistory.com";
}
1. Optional.ofNullable로 null를 갖는 Optional 객체를 생성합니다.
2. getUserEmail() 자체를 orElseGet 파라미터로 전달합니다.
3. orElseGet이 호출됨, 값이 Null이므로 other.get()이 호출되어 getUserEmail()가 호출됩니다.
❗orElse에 의한 발생가능한 장애 예시
위에서 살펴보았듯 orElse와 orElseGet은 명확하고 중요한 차이가 있는데, 이를 정확히 인식하지 못하면 장애가 발생할 수 있습니다. 예를 들어 userEmail을 Unique한 값으로 갖는 시스템에서 아래와 같은 코드를 작성했다고 가정합니다.
public void findByUserEmail(String userEmail) {
// orElse에 의해 userEmail이 이미 존재해도 유저 생성 함수가 호출되어 에러가 발생합니다.
return userRepository.findByUserEmail(userEmail)
.orElse(createUserWithEmail(userEmail));
}
private String createUserWithEmail(String userEmail) {
User newUser = new User(userEmail);
return userRepository.save(newUser);
}
위의 예제는 Optional의 단말 연산으로 orElse를 사용하고 있습니다. 따라서 조회 결과와 무관하게 사용된 함수인 createUserWithEmail가 반드시 실행됩니다. 하지만 Database에서는 userEmail이 Unique로 설정되어 있기 때문에 오류가 발생할 것입니다. 그렇기 때문에 위와 같은 경우에는 다음과 같이 해당 코드를 orElseGet으로 수정해야 합니다. 이렇게 코드를 수정하였다면 파라미터로 createUserWithEmail 함수 자체가 넘어가므로, 조회 결과가 없을 경우에만 사용자를 생성하는 로직이 호출 될 것입니다.
// 위 예제를 맞는 방법으로 변경
public void findByUserEmail(String userEmail) {
// orElseGet에 의해 파라미터로 함수를 넘겨주므로
// Null이 아니면 유저 생성 함수가 호출되지 않습니
return userRepository.findByUserEmail(userEmail)
.orElseGet(this::createUserWithEmail(userEmail));
}
private String createUserWithEmail(String userEmail) {
User newUser = new User(userEmail);
return userRepository.save(newUser);
}
실제 서비스에서 위와 같은 오류를 범한다면 큰 시스템 장애로 돌아오게 된다. 설령 문제가 없다고 하더라도 orElse는 값을 생성하여 orElseGet보다 비용이 크므로 최대한 사용을 피해야 합니다. 그러므로 orElse와 orElseGet의 차이점을 정확히 이해하고 사용하도록 해야합니다.
✔️ orElse와 orElseGet의 차이점 및 사용법 정리
orElse
▪️ 파라미터로 값을 필요로합니다.
▪️ 값이 미리 존재하는 경우에 사용합니다.
orElseGet
▪️ 파라미터로 함수(함수형 인터페이스)를 필요로 합니다.
▪️ 값이 미리 존재하지 않는 거의 대부분의 경우에 orElseGet을 사용하면 됩다.
📖 Reference
'코딩 공부 > web & Java' 카테고리의 다른 글
[JPA] 영속성 컨텍스트 (0) | 2024.03.02 |
---|---|
[JPA] JPA Auditing (0) | 2024.02.11 |
[Java] 함수형 인터페이스(Functional Interface) (1) | 2024.01.21 |
[Java] Stream (0) | 2024.01.20 |
[Docker] Docker Container와 Docker Image (1) | 2024.01.13 |