Java프로그래밍

[JAVA] 자바 코딩시 null 체크 방법으로 Optional.ofNullable, .orElseGet(), .orElseThrow() 사용 예제 총정리

새로운 프로젝트를 하고 있는데

다른사람들이 개발한 소스코드들이 git에 올라온다.

내려받아서 다른 개발자들의 코드를 한번씩 보는데

내가 보지못했던  Optional.ofNullable 메소드가 보여서 기록해둔다.

Optional은 Java8 버전부터 새롭게 Optional API 지원한다.

참고로 Optional 클래스를 이용하면  if를 이용한 null값 체크를 대체할 수 있다.


Optional이란?


Optional는 “존재할 수도 있지만 안 할 수도 있는 객체”  즉, “null이 될 수도 있는 객체”을 감싸고 있는 일종의 래퍼 클래스이다.

import java.util.Optional;

Java 언어 설계자인 Brian Goetz는 Optional을 만든 의도를 다음과 같은 문장이 공식 API 문서에 있다.

API Note:
Optional is primarily intended for use as a method return type where there is a clear need to represent “no result,” and where using null is likely to cause errors. A variable whose type is Optional should never itself be null; it should always point to an Optional instance.

메소드가 반환할 결과 값이 ‘없음’을 명백하게 표현할 필요가 있고, null을 반환하면 에러가 발생할 가능성이 높은 상황에서 메소드의 반환 타입으로 Optional을 사용하자는 것이 Optional을 만든 주된 목적이다. Optional타입의 변수의 값은 절대 null이어서는 안 되며, 항상 Optional인스턴스를 가리켜야 한다.


Optional.ofNullable

인자값으로 null값을 허용한다. 즉 일반 객체뿐만 아니라 null값까지 입력 인자로 받을 수 있다.

Optional.of

인자값으로 null값을 허용하지않는다. 그럼으로 인자값이 null인 케이스가 오면 NullPointerException이 발생한다.

String usrName = null;
Optional<String> opt = Optional.of(usrName);

isPresent을 사용하여 null여부 체크

isPresent메서드를 사용하면 현재 Optional의 값이 null인지 아닌지를 확인할 수 있다.

Optional<String> opt = Optional.of("hello");
boolean isVal = opt.isPresent();
 
opt = Optional.ofNullable(null);
boolean isVal = opt.isPresent();
	
System.out.println(opt.isPresent());

람다식을 함께 사용

Optional을 사용할 때 람다식을 이용할 수 있는데, .ifPresent()와 람다식을 이용하여 if문을 작성할 수 있다.

String str = "orange";
Optional<String> name = Optional.ofNullable(str);

opt.ifPresent( v -> System.out.println(v.length()));
int length;
if (ext.isPresent()) {
 length = ext.get().length();
} else {
 length = 0;
}


null 체크시 if문 작성의 번거로움을 줄일 수 있는 장점이 있다.

다음은 람다식을 이용한 예제이다.

/ Java8 이전
List<String> nameList = getUserNames();
List<String> tempNames = nameList != null ? nameList : new ArrayList<>();

// Java8 이후
List<String> nameList = Optional.ofNullable(getUserNames()).orElseGet(() -> new ArrayList<>());


Optional 클래스는 담고 있는 객체를 꺼내오는 방법

.get()

.orElse() : 파라미터로 값을 받는다.

.orElseGet() : 파라미터로 함수형을 받는다.

.orElseThrow()

get() 메소드는 비어있는 Optional 객체를 대상으로 호출할 경우 오류가 발생한다.

그럼으로 다음과 같이 객재 존재 여부를 bool 타입으로 반환하는 isPresent()라는 메소드를 통해 null 체크가 필요하다.

String text = getText();
Optional<String> ext = Optional.ofNullable(text);

orElse() 메소드는 값이 null인 경우에만 parameter로 적용한  인자값을 반환한다.

String str = "banana";
Optional<String> name = Optional.ofNullable(str);
System.out.println(name.orElse("orange"));

// 예제 2
int num = Optional.ofNullable(객체).orElse(0L).intValue();

// 예제 3
List<UserVo> uList= Optional.ofNullable(서비스호출).orElse(new ArrayList<>());


orElseGet()메소드는 인자값으로 함수형을 받는다.

String userEmail = "";
    String result = Optional.ofNullable(userEmail)
    .orElseGet(this::getUserEmail);
        
    System.out.println(result);
}

private String getUserEmail() {
    return "test@test.com";
}

//예제 2
UserVo dto= Optional.ofNullable(서비스호출).orElseGet(() -> UserVo.builder().build());

위 코드를 람다식 없이 작성하면 다음과 같다.

UserVo dto= Optional.ofNullable(서비스호출).orElseGet(UserVo :: new);

orElseThrow() 메소드는 람다식을 이용하여 Exception으로 던질 수 있다.

Optional.ofNullable(null).orElseThrow(() -> new Exception(“오류야”));



Java] Optional 올바르게 사용하는 방법

Optional 올바르게 사용하기

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

나쁜 예 :

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

좋은 예 :

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

반환 값으로 null을 사용하는 것이 위험하기 때문에 등장한 것이 Optional이다. 그럼으로 Optional대신 null을 반환하는 것은 Optional의 도입 의도와 맞지 않는다.

Optional은 내부 값을 null로 초기화한 싱글톤 객체를 Optional.empty() 메소드를 통해 제공하고 있다.
위에서 말한 “결과 없음”을 표현해야 하는 경우라면 null대신 Optional.empty() 를 반환하자.


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

Optional을 사용하면 그 안의 값은 Optional.get()메소드를 통해 접근 할 수 있는데,
만약 빈 Optional객체에 get() 메소드를 호출한 경우 NoSuchElementException 이 발생하기 때문에 값을 가져오기 전에 반드시 값이 있는지 확인해야 한다.
나쁜 예 :

Optional<Member> optionalMember = findById(1);
String name = optionalMember.get().getName();

피해야 하는 예 :

Optional<Member> optionalMember = findById(1);
if (optionalMember.isPresent()) {    
    return optionalMember.get();
} else {
    throw new NoSuchElementException(); 
}

좋은 예 :

Member member = findById(1).orElseThrow(MemberNotFoundException::new);
String name = member.getName();

피해야 하는 예의 경우엔 반드시 나쁘다고만은 할 수 없으나, 이후에 소개할 Optional의 API를 활용하면 동일한 로직을 더 간단하게 처리할 수 있다.
Optional을 이해하고 있다면 가독성 면에서도 더 낫기 때문에 꼭 필요한 경우가 아니라면 피하는 것이 좋다.


3. 값이 없는 경우, 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());

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

피해야 하는 예 :

Member member = findById(1).orElse(new Member()); // 값이 있던 없던 new Member()는 무조건 실행됨

좋은 예 :

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"));

자바 10부터는 orElseThrow() 의 인수 없이도 사용할 수 있다.


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

피해야 하는 예 :

Optional<Member> optionalMember = findById(1);
if(optionalMember.isPresent()) {
    System.out.println("member : " +optionalMember.get());
}

좋은 예 :

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

Optional.ifPresent()Optional객체 안에 값이 있는 경우 실행 할 람다를 인자로 받는다.
값이 있는 경우에 실행되고 값이 없는 경우에는 실행되지 않는 로직에 ifPresent()를 활용 할 수 있다.


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 methodincreaseSalary(Optional.ofNullable(member), 10);

좋은 예 :

void increaseSalary(Member member, int salary) {
    if(member != null) {
        member.increaseSalary(salary);
    }
} //call the methodincreaseSalary(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>> sports = new HashMap<>();
sports.put("100", Optional.of("BasketBall"));
sports.put("101", Optional.ofNullable(someOtherSports));        
String basketBall = sports.get("100").orElse("BasketBall");
String unknown = sports.get("101").orElse("");

좋은 예 :

Map<String, String> sports = new HashMap<>();
sports.put("100", "BasketBall");
sports.put("101", null); 
String basketBall = sports.getOrDefault("100", "BasketBall");
String unknown = sports.computeIfAbsent("101", k -> "");

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

of(X) 는 X 가 null이 아님이 확실할 때만 사용해야 하며, X 가 null 이면 NullPointerException이 발생한다.
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. 원시 타입의 Optional 에는 OptionalInt , OptionalLong , OptionalDouble 사용을 고려할 것

원시 타입(primitive type)을 Optional로 사용하면 BoxingUnBoxing을 거치면서 오버헤드가 생기게 된다.

반드시 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객체 maybeAmaybeB의 두 내부 객체 ab에 대해 a.equals(b)true이면maybeA.equals(maybeB)true 이며 그 역도 성립한다. 굳이 내부 값의 비교만을 위해 값을 꺼낼 필요는 없다는 의미이다.

나쁜 예 :

boolean compareMemberById(long id1, long id2) {
    Optional<Member> maybeMemberA = findById(id1);
    Optional<Member> maybeMemberB = findById(id2);
    if(!maybeMemberA.isPresent() && !maybeMemberB.isPresent()) {
       return false; 
    }    
    if (maybeMemberA.isPresent() && maybeMemberB.isPresent()) {
        return maybeMemberA.get().equals(maybeMemberB.get());
    }
     return false;
}

좋은 예 :

boolean compareMemberById(long id1, long id2) {
    Optional<Member> maybeMemberA = findById(id1);
    Optional<Member> maybeMemberB = findById(id2);
    if(!maybeMemberA.isPresent() && !maybeMemberB.isPresent()) {
         return false; 
    }
    return findById(id1).equals(findById(id2));
}

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

Optional.filter도 스트림처럼 값을 필터링 하는 역할을 한다.
인자로 전달된 predicate이 참인 경우에는 기존의 내부 값을 유지한 Optional이 반환되고,
그렇지 않은 경우 비어 있는 Optional을 반환한다.

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

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();
}


error: Content is protected !!