본문 바로가기
[ JAVA ]/JAVA

[ Java ] Optional 개념 및 올바른 사용법 알아가기

by 환이s 2024. 6. 10.


Intro

 

개발을 하다 보면 가장 많이 발생하는 에러 중 하나인 NullPointException을 자주 만나게 되는데,

해당 에러를 피하기 위해 null을 체크하는 로직을 추가합니다.

 

//mkey = Id 값 입니다. 해당하는 데이터가 없으면 null 입니다.
MemberVO findUser = memberRepository.findById(mKey);

//만약 findUser 결과 값이 null이면 NullPointException이 발생합니다.
findUser.getUserNm();
//--------------------------------------------------------------------------------
if(findUser != null){
//NullPointException을 방지하기 위해 null이 아닌 경우에만
//유저 이름을 가져올 수 있게 로직을 구성합니다.
	findUser.getUserNm(); 
	
}

 

하지만 null 체크를 해야 될 부분이 많다면

코드가 복잡해지고 가독성이 현저히 떨어지는 단점이 있습니다.

 

개발자 혼자서 작업하는 토이 프로젝트 같은 경우에는 크게 관심을 안 갖고 위 코드처럼 작성해도 괜찮지만

실무에서 가독성은 정말 중요합니다.

 

상상해 보자

내가 사용자 입장일 때 옷을 사려고 해당 상품을 구매하기 버튼을 클릭했는데

처리하는데 10초 정도가 걸렸다.

 

10초? 사실상 유저 입장에서는 5초만 걸려도 불편함을 느낄 수 있습니다.

 

이러한 고충 때문에 회사마다 자사 설루션의 가독성을 높이려고 하는데

Optional 클래스는 그 과정 중 하나라고 생각해서 포스팅을 진행해 보겠습니다.


Optional - 소개

 

Optional은 Java 8부터 사용할 수 있는데 Optional <T> 클래스를 사용해서 NullPointException(NPE)를 방지할 수 있도록 도와줍니다.

 

Optional <T>Null이 올 수 있는 값을 감싸는 Wrapper 클래스이며

참조하더라도 NPE가 발생하지 않도록 해줍니다.

 

해당 클래스를 클릭해서 확인해 보면

public final class Optional<T> {

  // If non-null, the value; if null, indicates no value is present
  private final T value;
   
  ...
}

 

위와 같은 value에 값을 저장하기 때문에 값이 Null이더라도 바로 NPE가 발생하지 않으며,

클래스이기 때문에 각종 메서드를 제공해 줍니다.

 


Optional - 생성

 

Optional 객체를 생성하기 위해서는 다음 메서드들을 사용해야 합니다.

Optional<String> optional = Optional.of(value);

 

이 경우 value 변수의 값이 null인 경우 NPE 예외가 발생합니다.

반드시 값이 있어야 하는 경우에 of() 메서드를 사용합니다.


Optional<String> optional = Optional.ofNullable(value);

 

value 변수의 값이 null일 수도 있다는 상황입니다. 변수가 null인 경우 Optional.empty()가 리턴됩니다.


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

 

비어있는 Optional 객체를 생성합니다. Optional 객체 자체는 있지만 내부에서 가리키는 참조가 없는 경우를 빈 객체라고 합니다. 

 

Optional.empty() 객체는 미리 생성되어 있는 싱글턴 인스턴스입니다.


Optional - 중간 처리(Method)

 

Optional 객체를 가져와서 변수 처리를 하고 리턴해서 Optional 객체를 반환하는 메서드들이 있습니다.

 

해당 메서드들은 람다식으로 사용할 수 있는데

중간 처리 메서드들을 연이어 이어 붙여 원화는 로직을 반복해서 사용할 수 있습니다.


- filter()

 

filter() 메서드의 인자인 람다식이 true이면 Optional 객체를 그대로 통과시키고,

false이면 Optional.empty()를 리턴하여 추가로 처리되지 않도록 합니다.

String OpTest1 = Optional.of("True").filter((val) -> val.contains("True")).orElse("Result False");
System.out.println(OpTest1); //True

String OpTest2 = Optional.of("True").filter((val) -> val.contains("False")).orElse("Result False");
System.out.println(OpTest2) // Result False

- map()

 

입력받은 값을 다른 값으로 변환하는 메서드입니다.

String OpTest3 = Optional.of("uppercase").map(String::toUpperCase).orElse("Result False");
System.out.println(OpTest3); //UPPERCASE -> 대문자 변환

Optional - Value Return Method

 

지금까지 중간 처리하는 메서드들을 알아봤습니다.

해당 메서드들은 Optional 객체를 리턴해서 메서드 체인으로 사용할 수 있는 반면, 값을 리턴해서 메서드 체인을 끝내는 메서드들도 있습니다.


- isPresent()

 

isPresent() 메서드는 Optional 객체의 값이 null? 즉, 여부를 판단해서 값이 존재하는지 체크합니다.

Optional.of("UserA").isPresent(); // true
Optional.of("UserA").filter(user -> "UserB".equals(user)).isPresent(); // false

 


- ifPresent()

 

ifPresent() 메서드는 람다식을 인자로 받아서 값이 존재하면 람다식에 적용해 줍니다.

만약 Optional 객체에 값이 없다면 해당 로직은 실행되지 않습니다.

// UserA 출력
Optional.of("UserA").ifPresent(System.out::println);

// 해당 로직은 실행되지 않습니다.
Optional.ofNullable(null).ifPresent(System.out::println);

 


- get()

 

Optional 객체가 가지고 있는 value 값을 가져옵니다.

만약 Optional 객체에 값이 없다면 NoSuchElementException 에러가 발생합니다.

 

Optional.of("UserA").get(); // UserA 값 가져오기 
Optional.ofNullable(null).get(); // NoSuchElementException 에러 발생

 


- orElse()

 

변수 처리를 거치면서 혹은 원래 Optional 객체가 비어있었다면,

orElse() 메서드에 지정된 값이 기본값으로 리턴됩니다.

// Ressult False return
Optional.of("UserA").filter(user -> user.startsWith("UserB")).orElse("Result False");

 


- orElseGet()

 

Optional 객체가 비어 있다면 기본값으로 제공할 supplier를 지정합니다.

orElse()의 경우 값이 null이든 아니던 호출되며, orElseGet()null일 때만 호출됩니다.

 

// Result False Return
Optional.of("UserB").filter(user -> user.startsWith("UserA")).orElseGet(() -> "Result False");

- orElseThrow()

 

연산을 끝낸 후에도 Optional 객체가 비어 있다면 예외 공급자 함수를 통해 예외를 발생시킵니다.

Optional.of("UserB").filter(user -> user.startsWith("UserA")).orElseThrow(NoSuchElementException::new);

 

 

orElseThrow() 메서드는 Java 10부터는 인수 없이도 사용할 수 있습니다.

Optional.ofNullable(value).orElseThrow(NoSuchElementException::new);
Optional.ofNullable(value).orElseThrow();

 


[ Java 9부터 추가된 메서드들 ]

 

- or()

 

or() 메서드는 orElse(), orElseGet() 메서드와 비슷합니다.

하지만 or() 메서드는 Optional 객체를 리턴합니다. 메서드 체인 중간에서 Optional.empty()가 되었을 때,

Optional.empty() 대신 다른 Optional 객체를 만들어서 뒤쪽으로 넘겨주고 싶을 때 사용합니다.

 

Optional.ofNullable(value)
        .map(value.getValue())
        .or(() -> Optional.ofNullable(user.getFavorite()))
        .orElse("No favorite");

 

valuenull이거나 valuegetValue() 메서드가 null을 리턴했을 때, 

Optional.ofNullable(user.getFavorite())를 실행해서 만들어진 Optional 객체를 넘겨줍니다.

 

만약 user.getFavorite() 메서드가 null을 리턴한다면 "No favorite" 문자열을 리턴할 것이고 

아니라면 getFavorite() 메서드로 얻어진 값이 리턴됩니다.

 


- ifPresentOrElse()

 

최종적으로 값을 반환하는 메서드입니다.

ifPresent() 메서드와 유사하지만 인자를 하나 더 받습니다.

첫 번째 인자로 받은 람다식은 Optional 객체에 값이 존재하는 경우 실행되고, 두 번째 인자로 받은 람다식은 Optional 객체가 비어있을 때 실행됩니다.

void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)

//'action' : 값이 존재할 경우 수행할 작업을 정의하는 'Consumer'

//'emptyAction' : 값이 존재하지 않을 경우 수행할 작업을 정의하는 'Runnable'

 

 

예제 코드로 알아봅시다.

Optional<String> optionalWithValue = Optional.of("Optional Test");
        Optional<String> emptyOptional = Optional.empty();

        // ifPresentOrElse() 사용 예제
        optionalWithValue.ifPresentOrElse(
            value -> System.out.println("Optional contains: " + value),
            () -> System.out.println("Optional is empty")
        );

        emptyOptional.ifPresentOrElse(
            value -> System.out.println("Optional contains: " + value),
            () -> System.out.println("Optional is empty")
        );

 

"optionalWithValue"는 값이 "Optional Test"로 설정된 Optional 객체입니다.

"ifPresentOrElse" 메서드가 호출되면 첫 번째 Consumer가 실행되어 값이 출력됩니다.

 

반대로 "emptyOptional"은 값이 없는 Optional 객체입니다.

"ifPresentOrElse" 메서드가 호출되면 두 번째 Runnable이 실행되어 빈 상태 메시지가 출력됩니다.

 

// 출력 결과
Optional contains: Optional Test
Optional is empty

 

이 메서드는 코드의 가독성을 높이고 Optional 객체의 상태에 따라 명확하게 다른 동작을 정의할 수 있게 해 줍니다.


- stream()

 

stream() 메서드는 중간 처리 연산자입니다.

Java 8에서 Optional 객체가 바로 stream 객체로 전환되지 않아 불편했던 부분을 해소하기 위한 메서드입니다.

Optional<String> optionalWithValue = Optional.of("Optional Test");
        Optional<String> emptyOptional = Optional.empty();

        // Optional 객체를 Stream으로 변환
        Stream<String> streamWithValue = optionalWithValue.stream();
        Stream<String> emptyStream = emptyOptional.stream();

        // Stream 처리 예제
        streamWithValue.forEach(System.out::println); // 출력: Optional Test
        emptyStream.forEach(System.out::println); // 출력 없음

        // Optional 객체들을 Stream으로 변환 후 하나의 Stream으로 병합
        Stream<Optional<String>> optionals = Stream.of(optionalWithValue, emptyOptional);
        Stream<String> values = optionals.flatMap(Optional::stream);

        values.forEach(System.out::println); // 출력: Optional Test

 

이 메서드는 Optional 객체를 스트림 파이프라인의 일부로 처리하고자 할 때 매우 유용합니다.

 

예를 들어,

여러 Optional 객체를 병합하여 하나의 stream으로 처리하거나, 스트림 파이프라인 내에서 Optional 값을 처리하는 경우에 활용할 수 있습니다.


Optional - 올바른 사용법 알아가기

 

Optional 타입은 최근까지도 자주 사용하고 있는데, 어느 부분에서 사용해야 하는 것이 적절한 지, 그렇지 않은지를 명확하게 구분하기가 어렵습니다.

 

너무 남용하는 것은 오히려 악효과를 낼 수 있기 때문에

Optional을 올바르게 사용하기 위한 방법을 제공하는 글이 있어서 일부를 번역해서 정리해 보겠습니다.

참고 포스팅 1

참고 포스팅 2

 

 

1. Optional 변수에 절대로 null 을 할당 하지 말 것

 

나쁜 예 : 

Optional<Person> findById(Long id) {
    // find person from db
    if (result == 0) {
        return null;
    }
}

 

좋은 예:

Optional<Person> findById(Long id) {
    // find person from db
    if (result == 0) {
        return Optional.empty();
    }
}

 

반환 값으로 null 을 사용하는 것이 위험하기 때문에 등장한 것이 Optional 입니다.

당연히 Optional 대신 null 을 반환하는 것은 Optional 의 도입 의도와 맞지 않습니다.

 

Optional 은 내부 값을 null 로 초기화한 싱글 톤 객체를 Optional.empty() 메서드를 통해 제공하고 있습니다.

위에서 인용한 답변과 같이 "결과 없음"을 표현해야 하는 경우라면 null을 메서드들을 통해 적절하게 처리를 이어갈 수 있습니다.


2. Optional.get() 호출 전에 Optional 객체가 값을 가지고 있음을 확실히 할 것

 

Optional 을 사용하면 그 안의 값은 Optional.get() 메서드를 통해 접근할 수 있는데,

만약 빈 Optional 객체에 get() 메서드를 호출한 경우 NoSuchElementException 이 발생하기 때문에 값을 가져오기 전에 반드시 값이 있는지 확인해야 합니다.

 

나쁜 예 :

Optional<Person> maybePerson = findById(4);
String name = maybePerson.get().getName();

 

 

피해야 하는 예:

Optional<Person> maybePerson = findById(4);
if (myabePerson.ifPresent()) {
    return maybeperson.get();
}
return UNKNOWN_PERSON;

 

 

좋은 예:

Person person = findById(4).orElseThrow(PersonNotFoundException::new);
String name = person.getName();

 

피해야 하는 예의 경우는 반드시 나쁘다고만은 할 수 없지만 

이후에 소개할 Optional의 API를 활용하면 동일한 로직을 더 간단하게 처리할 수 있습니다.

 

Optional 을 이해하고 있다면 가독성면에서도 더 낫기 때문에 꼭 필요한 경우가 아니라면 피하는 게 좋습니다.


3. 값이 없는 경우, Optional.orElse()를 통해 이미 생성된 기본 값(객체)을 제공할 것

 

결과가 없는 상황에 대해 null 대신 Optional 을 사용하기로 했으니, 이전에는 null 을 반환했던 "값이 없는" 상황을 처리할 방법은 크게 두 가지로 볼 수 있습니다.

 

  • 기본값 반환
  • 예외 던지기

 

Optional 객체에 값이 있는지는 Optional.isPresent() 메서드를 통해 확인할 수 있습니다.

 

Optional.orElse() 메서드는 "기본값 반환"에 해당하는 메서드입니다.

Optional 객체의 값이 없는 경우에 orElse의 인자로 명시된 값을 대신 반환합니다.

 

좋은 예:

public static final String MEMBER_STATUS = "UNKNOWN";
...
Member member = findById(1).orElse(MEMBER_STATUS); 

Member EMPTY_MEMBER = new Member();
...
Member member = findById(1).orElse(EMPTY_MEMBER);

 

주의할 점은 orElse 메서드의 인자는 Optional 객체가 존재할 때도 평가된다는 점입니다.

 

주의:

Member member = findById(1).orElse(new Member());

 

아마도 이름 때문이겠지만 orElse(new ...)를 써보면, new ...는 Optional 에 값이 없을 때만 실행될 것 같은 착각이 드는데, orElse(...)에서 ...는 Optional에 값이 있든 없든 무조건 실행됩니다.

method1(method2()) 이 실행되면 method2() 는 method1() 보다 먼저 그리고 언제나 실행됩니다.
따라서 orElse(new ...)에서도 new ...가 무조건 실행되는 것이 당연합니다.

 

값이 없으면 orElse() 의 인자로서 실행된 값이 반환되므로 실행한 의미가 있지만,

Optional 에 값이 있으면 orElse() 의 인자로서 실행된 값이 무시되고 버려집니다.

 

따라서 orElse(...)는 ...가 새 객체 생성이나 새로운 연산을 유발하지 않고

이미 생성되었거나 계산된 값일 때만 사용해야 합니다.

 

매번 새로운 객체를 생성해야 한다면 4번 항목을 참조해 봅시다.


4. 값이 없는 경우, Optional.orElseGet()을 통해 이를 나타내는 객체를 제공할 것

 

3번 항목의 경우 값이 없는 경우에 문자열처럼 동일한 객체 참조를 반환해도 괜찮은 경우에 적합합니다.

하지만 불변 객체가 아닌 경우 이 방법은 위험할 수 있습니다.

 

값이 없는 경우에 매번 새로운 객체를 반환해야 하는 경우에는 Optional.orElseGet() 을 사용할 수 있습니다.

orElse 가 기본값으로 반환할 값을 인자로 받는 것과 달리, orElseGet() 은 값이 없는 경우 이를 대신해 반환할 값을 생성하는 람다를 인자로 받습니다.

 

좋은 예:

Member member = findById(1).orElseGet(Member::new);

 

orElseGet(Supplier) 에서 Supplier는 Optional 에 값이 없을 때만 실행됩니다.

따라서 Optional 에 값이 없을 때만 새 객체를 생성하거나 새 연산을 수행하므로 불필요한 오버헤드가 없습니다.

 

물론 람다식이나 메서드 참조에 대한 오버헤드는 있겠지만 불필요한 객체 생성이나 연산을 수행하는 것에 비하면 경미합니다.


5. 값이 없는 경우, Optional.orElseThrow()를 통해 명시적으로 예외를 던질 것

 

값이 없는 경우, 기본 값을 반환하는 대신 예외를 던져야 하는 경우도 있습니다.

이 경우에는 Optional.orElseThrow()를 사용할 수 있습니다.

Member member = findById(1).orElseThrow(() -> new NoSuchElementException("Member Not Found"));

 


6. 값이 있는 경우에 이를 사용하고 없는 경우에 아무 동작도 하지 않는다면, Optional.ifPresent()를 활용할 것

 

Optional.ifPresent()Optional 객체 안에 값이 있는 경우 실행할 람다를 인자로 받습니다.

값이 있는 경우에 실행되고 값이 없는 경우에는 실행되지 않는 로직에 ifPresent를 활용할 수 있습니다.

 

좋은 예:

Optional<Member> optionalMember = findById(1);
optionalMember.ifPresent(System.out::println);

7. isPresent() - get() 은 orElse() 나 orElseXXX 등으로 대체할 것

 

Optional 객체로부터 값의 유무를 확인한 뒤 사용하는 패턴은 앞에서 소개한 다양한 API들로 대체할 수 있습니다.

 

피해야 하는 예:

Optional<Member> optionalMember = findById(1);
if(optionalMember.isPresent()) {    
	System.out.println("member : " +optionalMember.get());
    } else {
    throw new MemberNotFoundException("Member Not Found id : " + 1);
    }

 

좋은 예:

Member member = findById(1)
	.orElseThrow(() -> new MemberNotFoundException("Member not found id : " + 1));
System.out.println("member : " + member.get());

 


8. Optional을 필드의 타입으로 사용하지 말 것

 

나쁜 예:

public class Member {   
	private Optional<String> name;
}

 

 

좋은 예:

public class Member {    
	private String name;
}

 

Optional 은 반환 타입을 위해 설계된 타입입니다.

Optional 을 클래스의 필드로 선언하거나 (생성자와 세터를 포함한) 메서드의 인자로 사용하는 것은 Optional 의 도입 의도에 반하는 패턴입니다.

 


9. Optional을 생성자나 메서드 인자로 사용하지 말 것

 

Optional 을 생성자나 메서드 인자로 사용하면,

호출할 때마다 Optional 을 생성해서 인자로 전달해줘야 합니다.

 

굳이 비싼 Optional 을 인자로 사용하지 말고 호출되는 쪽에 null 체크 책임을 남겨두는 것이 좋습니다.

 

나쁜 예:

void increaseSalary(Optional<Member> member, int salary) {   
	member.ifPresent(member -> member.increaseSalary(salary));
} 

//call the method
increaseSalary(Optional.ofNullable(member), 10);

 

 

좋은 예:

void increaseSalary(Member member, int salary) {
	if(member != null) {
    	member.increaseSalary(salary);    
        }
} 

//call the method
increaseSalary(member, 10);

 


10. 단지 값을 얻을 목적이라면 Optional 대신 null 비교

 

Optional 은 비싸기 때문에 과도하게 사용하지 말아야 합니다.

단순히 값 또는 null 을 얻을 목적이라면 Optional 대신 null 비교를 사용합니다.

 

나쁜 예:

return Optional.ofNullable(member).orElse(UNKNOWN);

 

 

좋은 예:

return member != null ? member : UNKNOWN;

 


11. Optional을 빈 컬렉션이나 배열을 반환하는 데 사용하지 말 것

 

컬렉션이나 배열로 복수의 결과를 반환하는 메서드가 "결과 없음"을 가장 명확하게 나타내는 방법은 대부분의 경우 빈(empty) 컬렉션 또는 배열을 반환하는 방법입니다.

 

이러한 상황에 빈 컬렉션이나 배열 대신 Optional 을 사용해서 얻는 이점이 있는지 고민해 본다면 Optional 을 컬렉션이나 배열에 사용하는 것이 옳은지에 대한 답을 찾을 수 있을 것입니다.

 

나쁜 예:

List<Member> members = team.getMember();
return Optional.ofNullable(members);

 

 

좋은 예:

List<Member> members = team.getMembers();
return members != null ? members : Collections.emptyList();

 

 

마찬가지 이유로 Spring Data JPA Repository 메서드 선언 시 다음과 같이 컬렉션을 Optional 로 감싸서 반환하는 것은 좋지 않습니다.

 

컬렉션을 반환하는 Spring Data JPA Repository 메서드는 null 을 반환하지 않고 비어있는 컬렉션을 반환해 주므로 Optional 로 감싸서 반환할 필요가 없습니다.

 

나쁜 예:

public interface MemberRepository extends JpaRepository<Member, Long> {
	Optional<List<Member>> findAllByNameContaining(String keyword);
   }

 

 

좋은 예:

public interface MemberRepository extends JpaRepository<Member, Long> { 
	List<Member> findAllByNameContaining(String keyword);
 }

 


12. Optional을 컬렉션의 원소로 사용하지 말 것

 

컬렉션에 Optional 을 원소로 사용하지 말고 원소를 꺼낼 때나 사용할 때 null 체크하는 것이 좋습니다.

 

특히 MapgetOrDefault(), PutIfAbsent(), computeIfAbsent(), computeIfPresent() 처럼 null 체크가 포함된 메서드를 제공하므로, Map의 원소로 Optional 을 사용하지 말고 Map 이 제공하는 메서드를 활용하는 것이 좋습니다.

 

 

나쁜 예:

Map<String, Optional<String>> categorys = new HashMap<>();
categorys.put("100", Optional.of("top"));
categorys.put("200", Optional.ofNullable(someOtherCategorys)); 

String top = categorys.get("100").orElse("top");
String unknown = categorys.get("200").orElse("");

 

 

좋은 예:

Map<String, Optional<String>> categorys = new HashMap<>();
categorys.put("100","top");
categorys.put("200", null); 

String top = categorys.getOrDefault("100","top");
String unknown = categorys.computeIfAbsent("200", k -> "");

13. Optional.of()와 Optional.ofNullable()을 혼동하지 말 것

 

of(X)는 X 가 null 이 아님이 확실할 때만 사용해야 하며,

X 가 null 이면 NullPointException 이 발생합니다.

 

ofNullable(X) 은 X 가 null 일 가능성이 있을 때 사용해야 하며,

X 가 null 이 아님이 확실하면 of(X) 를 사용해야 합니다.

 

 

나쁜 예:

return Optional.of(member.getName()); // member의 name이 null 이면 NPE 발생

return Optional.ofNullable(MEMBER_STATUS);

 

 

좋은 예:

return Optional.ofNullable(member.getName());

return Optional.of(MEMBER_STATUS);

 


14. 원시 타입(Primitve Type) 의 Optional 에는 OptionalInt, OptionalLong, OptionalDouble 사용을 고려할 것

 

원시 타입(Primitve Type)을 Optional 로 사용해야 할 때는 Boxing UnBoxing 을 거치면서 오버헤드가 생기게 됩니다.

 

반드시 Optional 의 제네릭 타입에 맞춰야 하는 경우가 아니라면 int, long, double 타입에는 OptionalXXX 타입 사용을 고려하는 것이 좋습니다.

 

이들은 내부 값을 래퍼 클래스가 아닌 원시 타입으로 갖고, 값의 존재 여부를 나타내는 isPresent 필드를 함께 갖는 구현체들입니다.

 

 

나쁜 예:

Optional<Integer> cnt = Optional.of(10); // boxing 발생

for(int i = 0; i < cnt.get(); i++) { ... } // unboxing 발생

 

 

좋은 예:

OptionalInt cnt = OptionalInt.of(10); // boxing 발생 안 함

for(int i = 0; i < cnt.getAsInt(); i++) { ... } // unboxing 발생 안 함

 


15. 내부 값의 비교에는 Optional.equals 사용을 고려할 것

 

Optional.equals의 구현은 다음과 같습니다.

@Override
public boolean equals(Object obj) {
  if (this == obj) {
      return true;
  }

  if (!(obj instanceof Optional)) {
      return false;
  }

  Optional<?> other = (Optional<?>) obj;
  return Objects.equals(value, other.value);
}

 

기본적인 참조 확인과, 타입 확인 이후에 두 Optional 의 동치성은 내부 값의 equals 구현이 결정합니다.

즉, Optional 객체 maybeA maybeB 의 두 내부 객체 ab 에 대해,

a.equals(b)true 이면 maybeA.equals(maybeB)true 이며 그 역도 성립합니다.

굳이 내부 값의 비교만을 위한 값을 꺼내올 필요는 없다는 의미입니다.

 

 

나쁜 예:

boolean comparePersonById(long id1, long id2) {
  Optional<Person> maybePersonA = findById(id1);
  Optional<Person> maybePersonB = findById(id2);
  if (!maybePersonA.isPresent() && !maybePersonB.isPresent()) { return false; }
  if (maybePersonA.isPresent() && maybePersonB.isPresent()) {
      return maybePersonA.get().equals(maybePersonB.get());
  }
  return false;
}

 

 

좋은 예:

boolean comparePersonById(long id1, long id2) {
  Optional<Person> maybePersonA = findById(id1);
  Optional<Person> maybePersonB = findById(id2);
  if (!maybePersonA.isPresent() && !maybePersonB.isPresent()) { return false; }
  return findById(id1).equals(findById(id2));
}

 


16. 값에 대해 미리 정의된 규칙(제약사항)이 있는 경우에는 filter 사용을 고려할 것

 

Optional.filter 도 스트림처럼 값을 필터링하는 역할을 합니다.

인자로 전달된 predicate이 참인 경우에는 기존의 내부 값을 유지한 Optional 이 반환되고,

그렇지 않은 경우에는 비어 있는 Optional 을 반환합니다.

 

유저네임에 대한 몇 가지 제약 사항을 검증하는 기능을 아래 메서드를 활용하여 다음과 같이 구현해 볼 수 있습니다.

boolean isIncludeSpace(String str) { /* ... */ } // check if string includes white space

boolean isOverLength(String str) { /* ... */ } // check if length of string is over limit

boolean isDuplicate(String str) { /* ... */ } // check if string is duplicates with already registered

 

 

기존 방식:

boolean isValidName(String username) {
  return isIncludeSpace(username) &&
    isOverLength(username) &&
    isDuplicate(username);
}

 

 

Optional 을 활용한 방식:

boolean isValidName(String username) {
  return Optional.ofNullable(username)
    .filter(this::isIncludeSpace)
    .filter(this::isOverLength)
    .filter(this::isDuplicate)
    .isPresent();
}

 

여기에는 어느 방법이 맞다고 단정하기 어렵기 때문에 (가독성 등을 고려하여) 상황에 따라 최선이라고 생각되는 방법을 찾는 게 중요할 것 같습니다.


마치며

 

오늘은 Optional 타입의 개념 및 올바르게 사용하는 방법에 대해 알아봤습니다.

다음 포스팅에서 뵙겠습니다.

 

728x90

'[ JAVA ] > JAVA' 카테고리의 다른 글

[ Java ] java.util.stream.IntStream 주요 메서드 정리  (1) 2024.05.23
[ JAVA ] Iterator 개념 및 예제  (0) 2023.07.21
[ Java ] Lambda  (0) 2023.01.17
[ Java ] File 클래스  (0) 2023.01.16
[ Java ] Socket Programming  (0) 2023.01.13