[Java] 자바 8 - 올바른 Optional 사용법
올바른 Optional 사용법
실무에서 Optional을 사용하면서 정말 좋은 유틸 클래스라고 생각했습니다. Optional로 반환하면서 값이 있을 때와 값이 없을 때의 분기처리를 해줘야 하는 것을 알 수 있기 때문에 NullPointerException
과 같은 예외를 방지할 수 있게 됩니다. 실제로 실무에서 Optional을 많이 사용하였는데 가끔 '여기서 굳이 Optional을 사용해야 하나?'라는 상황들이 생겼고, 오히려 코드의 가독성이 저하되는 문제들을 발견하게 되어 불필요한 Optional 사용을 줄이고, 올바른 Optional을 사용하기 위해 포스팅하게 되었습니다.
참고 - 26 Reasons Why Using Optional Correctly Is Not Optional - DZone
1. Optional 객체에 null을 할당하지 말아라
반환 값을 'null'로 반환할 경우 서비스 로직에서 NullPointerException
이 발생할 수 있기 때문에 Optional을 사용합니다. 즉, 서비스 로직에서 Optional을 반환받으면 Optional이 감싸고 있는 값이 'null'일 경우에 대한 로직을 추가하여 NullPointerException
을 방지할 수 있게 됩니다.
이러한 Optional 객체에 'null'을 할당하게 되면 Optional을 사용하는 의도가 없어지게 됩니다. 서비스 로직에서 반환받은 Optional이 'null'일 때 사용하게 되면 'null'을 참조하게 되니 결국엔 NullPointerException
이 발생하게 됩니다. 따라서 Optional 객체에 'null'을 할당하지 않고 빈(empty) Optional 객체를 반환하는 Optional.empty()
를 사용해야 합니다.
Avoid :
@Test
void optional_test() {
Long memberId = 10L;
Optional<Member> memberOp = findById(memberId);
if (memberOp.isPresent()) { // ❗ NullPointerException 발생
...
}
}
private Optional<Member> findById(Long memberId) {
Member member = ...; // find Member from DB
if (member == null) {
return null; // ❗ null 반환
}
...
}
Prefer
@Test
void optional_null_test() {
Long memberId = 10L;
Optional<Member> memberOp = findByIdPreferMethod(memberId);
if (memberOp.isPresent()) { // ✨ NullPointerException이 발생하지 않는다.
...
}
}
private Optional<Member> findByIdPreferMethod(Long memberId) {
Member member = null; // find Member from DB
if (member == null) {
return Optional.empty(); // ✨ null이 아닌 값이 없는 Optional 객체 반환
}
return Optional.ofNullable(member);
}
2. Optional.get() 호출하기 이전에 Optional 객체가 가지고 있는 값 존재유무를 확인해라
반환된 Optional은 값의 존재유무를 확인해야 합니다. 만약 Optional 객체가 가지고 있는 값의 존재유무를 확인하지 않고 Optional.get()
메서드를 호출하게 된다면 Optional 객체의 값이 없을 경우 NoSuchElementException
이 발생하게 됩니다.
Avoid :
@DisplayName("Optional 객체의 값 존재유무를 확인해라")
@Test
void optional_get_test() {
Optional<Member> memberOp = memberRepository.findById(100L);
String name = memberOp.get().getName(); // ❗ NoSuchElementException 발생
}
Prefer :
@DisplayName("Optional 객체의 값 존재유무를 확인해라 - prefer 2")
@Test
void optional_get_test3() throws NotFoundException {
// ✨ Case1 - 해당 Member를 찾지 못했을 경우 예외처리
Member member = memberRepository.findById(100L).orElseThrow(NotFoundException::new);
// ✨ Case2 - Member가 있을 경우와 Member가 없을 경우 분기처리
Optional<Member> memberOp = memberRepository.findById(100L);
// Optional 객체의 Member가 존재하는 경우
if (memberOp.isPresent()) {
}
// Optional 객체의 Member가 존재하지 않는 경우
if (memberOp.isEmpty()) {
}
}
여기서 올바른 사용법으로 알아둬야 하는 것은 Optional API에서 제공하는 메서드를 이용하여 Case1과 같이 사용하는 것이 좋습니다. Case1과 같이 사용할 수 있음에도 Case2와 같이 사용하게 된다면 코드의 가독성만 해치게 되는 원인이 됩니다. 단, Optional 객체의 값이 있는 경우와 없는 경우에 따른 분기처리가 필요한 경우에는 Case2번과 같이 사용하는 것을 권장합니다.
3. Optional.orElse()을 사용하는 경우 이미 생성된 기본 값(객체)을 제공해라
Optional 객체에 값이 없을 경우 항상 같은 기본 값(객체)을 반환해야 한다면 Optional.orElse()
를 통해 미리 만들어둔 기본 값(객체)을 사용하는 것이 좋습니다. 그 이유는 Optional.orElse()
메서드는 Optional 객체의 값이 존재 유무에 상관없이 항상 실행되기 때문에 Optional.orElse()
메서드 내부에서 객체를 생성하게 된다면 매번 객체가 생성되는 불필요한 자원이 낭비될 수 있기 때문입니다.
Avoid :
@DisplayName("Optional.orElse()를 사용하는 경우 이미 생성된 기본 값(객체)를 제공해라 - avoid")
@Test
void optional_orElse_test1() {
// ❗ memberId = 100L 인 Member가 존재해도 new Member("Default Member")가 생성이 된다.
Member member = memberRepository.findById(100L).orElse(new Member("Default Member"));
}
Prefer :
@SpringBootTest
@ActiveProfiles("local")
public class OptionalTest {
// ✨ 미리 만들어 놓은 Default Member
private final Member DEFAULT_MEMBER = new Member("Default Member");
@DisplayName("Optional.orElse()를 사용하는 경우 이미 생성된 기본 값(객체)를 제공해라 - prefer")
@Test
void optional_orElse_test2() {
// ✨ Optional.orElse 메서드는 값의 존재 유무와 상관없이 항상실행되므로
// Optional에 값이 없을 경우 항상 같은 객체를 반환해야한다면 미리 만들어 사용해야 한다.
Member member = memberRepository.findById(100L).orElse(DEFAULT_MEMBER);
}
}
여기서 주의 깊게 봐야 하는 점은 Optional 객체의 값이 없을 경우 항상 같은 객체를 반환하는 경우입니다. 만약 매번 새로운 객체를 생성해야 하는 경우에는 아래 4번 항목과 같이 사용해야 합니다.
4. Optional 객체의 값이 없을 때 매번 새로운 객체를 생성해야 한다면 Optional.orElseGet()을 사용해라
3번 항목과는 달리 Optional 객체의 값이 없을 때 매번 새로운 객체를 생성해야 한다면 Optional.orElseGet()
메서드를 사용할 수 있습니다. Optional.orElse()
메서드는 값의 존재 유무와는 상관없이 항상 실행되는 것과는 달리 Optional.orElseGet()
메서드는 Optional 객체의 값이 없을 경우에만 실행이 되어 불필요한 자원 낭비를 줄일 수 있습니다.
Prefer :
@DisplayName("Optional에 없을 경우 매번 새로운 객체를 생성해야 한다면 Optional.orElseGet()을 사용해라")
@Test
void optional_orElseGet_test1() {
// ✨ Optional.orElseGet() 메서드는 Optional 객체의 값이 없을 경우에만 실행이 된다.
Member member = memberRepository.findById(100L).orElseGet(() -> new Member("Default Member"));
}
5. Optional 객체의 값이 있을 경우 특정 동작을 해야 한다면 Optional.ifPresent()를 사용해라
Optional 객체의 값이 있을 경우에만 특정 동작이 실행되고 없는 경우에는 실행되지 않는 로직이 필요하다면 Optional.ifPresent()
를 이용할 수 있습니다.
Prefer :
@DisplayName("Optional 객체에 값이 있는 경우에 특정 로직 실행")
@Test
void optional_ifPresent_test() {
memberRepository.findById(10L)
.ifPresent(member -> System.out.println("member.getName() = " + member.getName()));
}
6. ifPresent - get 은 orElse 나 orElseXX로 대체해라
Optional 객체의 값 존재유무에 따른 로직은 Optional.orElse()
, Optional.orElseXX
를 사용하는 것을 추천합니다. Optional에서 제공하는 다양한 API들을 사용하게 되면 if문 등을 사용하지 않기 때문에 코드의 가독성이 증가하게 됩니다. 따라서 값이 존재여부에 따른 분기처리를 하지 않는 경우라면 Optional에서 제공하는 API를 사용하는 것이 좋습니다.
Avoid :
@DisplayName("ifPresent - get 패턴은 orElse나 orElseXXX를 사용해라 - avoid")
@Test
void optional_api_test1() throws NotFoundException {
Optional<Member> memberOp = memberRepository.findById(10L);
if (memberOp.isPresent()) {
Member member = memberOp.get();
System.out.println("member.getName() = " + member.getName());
} else {
throw new NotFoundException();
}
}
Prefer :
@DisplayName("ifPresent - get 패턴은 orElse나 orElseXXX를 사용해라 - prefer")
@Test
void optional_api_test2() throws NotFoundException {
Member member = memberRepository.findById(10L).orElseThrow(NotFoundException::new);
System.out.println("member.getName() = " + member.getName());
}
7. Optional을 필드에서 사용하지 말아라
Optional은 반환 타입을 위해 설계된 Wrapper 클래스로 직렬화를 지원하지 않기 때문에 필드 변수 타입으로 사용하는 것을 권장하지 않습니다.
Avoid :
Class Member {
private Optional<String> name; // ❗ Optional 도입 의도에 반하는 패턴
// ❗ Optional 도입 의도에 반하는 패턴
public Optional<String> getName() {
return this.name;
}
}
8. Collection의 경우 Optional이 아닌 빈 Collection을 사용해라
Collection에서 제공하는 다양한 API가 있습니다. 따라서 비용이 큰 Optional을 사용하는 것보단 Collection에서 제공하는 API를 사용하는 것이 더 깔끔하고, 처리가 가벼워집니다.
Avoid :
public Optional<List<Member>> getMemberList() {
List<Member> memberList = ...; // find memberList from DB
return Optional.ofNullable(memberList);
}
Prefer :
public List<Member> getMemberList() {
List<Member> memberList = ...; // find memberList from DB
return memberList == null
? Collections.emptyList()
: memberList;
}
9. Primitive Type을 값으로 갖는 Optional은 OptionalInt, OptionalLong, OptionalDouble을 고려해라
원시 타입(Primitive Type)을 값으로 갖는 Optional은 박싱/언박싱에 따른 오버헤드가 생기게 됩니다. 따라서 원시 타입을 값으로 갖는 Optional을 사용할 때에는 OptionalInt
, OptionalLong
, OptionalDouble
을 사용하여 오버헤드를 줄일 수 있습니다.
Avoid :
// ❗ 박싱/언박싱 오버헤드 증가
Optional<Integer> intOp = ...;
Optional<Long> longOp = ...;
Optional<Double> doubleOp = ...;
Prefer :
// ✨ 박싱/언박싱 최소화
OptionalInt intOp = ...;
OptionalLong longOp = ...;
OptionalDouble doubleOp = ...;
10. Optional 객체의 값 비교에는 Optional.equals()를 사용해라
Optional 객체가 가지고 있는 값을 비교하기 위해 Optional.eqauls()
를 사용할 수 있습니다. 즉, 굳이 Optional 객체가 가지고 있는 값을 꺼내서 비교하지 않아도 된다는 장점이 있습니다.
Prefer :
@DisplayName("Optional 객체의 값 비교에는 Optional.equals()를 사용해라")
@Test
void optional_equals_test() {
Optional<Member> memberOp1 = Optional.of(new Member(1L, "test"));
Optional<Member> memberOp2 = Optional.of(new Member(1L, "test"));
boolean equals = memberOp1.equals(memberOp2);
System.out.println("equals = " + equals);
}
11. 변환이 필요한 경우 Optional.map(), Optional.flatMap 사용을 고려해라
Optional에서 변환 메서드인 Optional.map()
, Optional.flatMap()
을 제공합니다. 값이 존재하는 경우에는 새로운 Optional 객체를 반환하고 값이 없는 경우 Optional.empty()
를 반환합니다. 변환된 반환값이 Optional인 경우에 Optional.map()
을 사용하게 되면 반환 값은 Optional<Optional<Object>> 가 됩니다. 이러한 경우 Optional.flatMap()
을 사용하게 되면 Optional<Object>를 반환할 수 있습니다.
Prefer :
Optional<String> optionalName = Optional.of("John");
Optional<Integer> optionalLength = optionalName.map(name -> name.length());
optionalLength.ifPresent(length -> System.out.println("Name Length: " + length));
12. Optional 객체의 값이 존재하고 필터링이 필요한 경우 Optional.filter() 사용을 고려해라
Optional 객체의 값이 존재할 때 필터링이 필요한 경우 Optional.filter()
메서드를 사용하면 가독성이 좋은 코드를 구현할 수 있습니다. Optional.filter()
메서드는 Optional 객체의 값이 존재할 경우 Predicate을 만족하는 새로운 Optional을 반환합니다.
Optional<Integer> optionalNumber = Optional.of(10);
// ✨ Optional.filter() 메서드 사용
Optional<Integer> filteredNumber = optionalNumber.filter(number -> number > 5);
filteredNumber.ifPresent(value -> System.out.println("Filtered Value: " + value));
Optional<Integer> optionalNumber2 = Optional.of(3);
// ❗ Predicate에 만족하지 않으므로 Optional.empty()를 반환한다.
Optional<Integer> filteredNumber2 = optionalNumber2.filter(number -> number > 5);
// ❗ Optional에 값이 없으므로 실행되지 않는다.
filteredNumber2.ifPresent(value -> System.out.println("Filtered Value: " + value));
관련 포스팅