Dev/Java

Optional의 안티 패턴을 피하는 방법 😎

아콩2 2022. 7. 22. 18:38
반응형

비는 태양을 피하는 방법을 노래하고 저는 Optional의 안티 패턴을 피하는 방법을 포스팅 하겠습니다.
??? : 짤쓰려고 글 쓰시는건가요 ?
나 : Yes -!

 

이전 포스팅 에서는 Optional이 무엇이고 어떤 메서드가 내장 되어있는지를 확인하였습니다.

Optional은 왜 JAVA 8에서 도입이 되었을까요?

stack overflow에서는 Optional을 만든 의도 에 대해 다음과 같이 설명하였습니다.

... Our intention was to provide a limited mechanism for library method return types where there needed to be a clear way to represent "no result" ...

우리의 의도는 "no result"를 표현할 수 있는 명확한 방법이 필요한 라이브러리 메서드 반환 형식에 대한 제한된 메커니즘을 제공하는 것이었다.

답변에 의하면 Optional의 주 목적은 '반환값이 없음을 나타내는 것이 주목적'이며 개발자들이 기대하는 것과는 다르게 만들어졌다고 합니다.
그럼에도 불구하고 개발자들은 기대했던 대로 사용해버려서 Optional과 관련된 주의사항이 26개라고 합니다.

개인적인 생각으로는 주의사항이 26개가 될 정도면 왜 의도와 다르게 사용하는걸 허용했을지 의문이 듭니다 🤨

참고로 [Optional API 공식 문서]의 API note에는 아래의 내용이 포함 되어 있습니다.

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인 변수 자체는 null이 아니어야 하며 항상 Optional 인스턴스를 가리켜야 합니다.

주의사항이 무려 26개 이기 때문에 무심결에 안티패턴을 사용하고 있었을 수도 있습니다.
이번 포스팅에서는 무심결에 잘못 사용하고 있었던 안티패턴과 Optional의 올바른 사용법을 알아보겠습니다.

1. isPresent()-get() 대신 orElse()/orElseGet()/orElseThrow()

Optional은 비쌉니다. 기왕 비싼 Optional를 사용하기로 했으니 코드 라인수라도 줄여야 이득이겠죠? 코드로 확인해보겠습니다.

// bad code ❌
Optional<User> user = findUserById(); // return Optional<User> 

if(user.isPresent(){
     return user.get();
} else {
     return null;
}

// 좋은 코드는 아니지만 위의 코드보단 나은 코드 ⛔️
Optional<User> user = findUserById();
return user.orElse(null); 

// good code ✅
Optional<User> user = findUserById():
return member.orElseThrow(()-> new AnyException());

2. orElse(new A) 대신 orElseGet(()-> new ...)

orElse(A)에서 A는 Optional에 값이 있든 없든 무조건 실행됩니다. 따라서 A가 새로운 객체를 생성하거나 새로운 연산을 수행하는 경우에는 orElse()대신 orElseGet()을 사용해야 합니다.

methodB(methodA()) 메서드가 실행이 되면 methodA()는 methodB()보다 먼저 실행 & 언제나 실행 됩니다.
따라서 orElse(new A) 에서도 new A가 무조건 실행이 되겠죵?

Optional에 값이 있으면 먼저 실행이 되든 언제나 실행이 되든 orElse()의 인자로서 실행된 값이 반환되므로 실행한 의미가 있지만 Optional에 값이 없으면 어떻게 될까요? Optional에 값이 있다면 orElse()의 인자로서 실행된 값이 무시되고 버려집니다.

따라서 orElse(A)는 A가 새 객체 생성이나 새로운 연산을 유발하지 않고 이미 생성되었거나 이미 계산된 값일 때만 사용해야 합니다💡

orElseGet(Supplier)에서 Supplier는 Optional에 값이 없을 때만 실행됩니다. 따라서 Optional에 값이 없을때만 새 객체를 생성하거나 새 연산을 수행하므로 불필요한 오버헤드가 발생하지 않습니다.

// bad code ❌
Optional<User> user = findById();
return user.orElse(new User());  // user에 값이 있든 없든 new User()는 무조건 실행 

//good code ✅
Optional<User> user = findById();
return user.getElseGet(User :: new); // user에 값이 없을 때만 new User() 실행 

//good code ✅
User EMPTY_USER = new User();
Optional<User> user = findById();
return user.orElse(EMPTY_USER); // 이미 생성됐거나 계산된 값은 orElse()를 사용해도 무방 

orElse의 메소드 명만 봤을땐 optional에 값이 없으면 실행 될 것 같은 착각을 유발할 수 있습니다.
하지만 이제 두 메서드의 차이점을 알았으니 더이상 착각하고 사용 할 일이 없어졌습니다. 👏🏼👏🏼👏🏼👏🏼

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

Optional은 비쌉니다. 따라서 단순히 값 또는 null을 얻을 목적이라면 Optional 대신 null 비교를 써야합니다.

//bad code ❌
return Optional.ofNullable(status).orElse(READY);

// good code ✅
return status != null ? status : REDAY;

Optional 대신 비어있는 컬렉션 반환

컬렉션은 null이 아니라 비어있는 컬렉션을 반환하는 것이 좋을 때가 많습니다. 따라서 컬렉션은 Optional로 감싸서 반환하지말고 비어있는 컬렉션을 반환합시다!

//bad code ❌
List<User> users = findUsersByAge();
return Optional.ofNullable(users); 

//good code ✅
List<User> users = findUsersByAge();
return users != null ? users : Collection.emptyList();

참고로 Collection.emptyList()는 호출될 때마다 비어있는 리스트를 반환하는 것이아니라 이미 생성된 static 변수인 EMPTY_LIS를 반환합니다. 마찬가지로 Spring Data JPA Repository 메서드 선언 시 컬렉션을 Optional로 감싸서 반환하는 것은 좋지 않은 코드입니다.
컬렉션을 반환하는 Spring Data JPA Repository 메서드는 null을 반환하지 않고 비어있는 컬렉션으로 반환해주기 때문에 굳이 Optional로 감싸줄필요가 없습니다!

// bad code ❌
public interface UserRepository extends JpaRepository<User,Long> {
    Optional<List<User>> findAllByUserAge(int age);
}

// good code ✅
public interface UserRepository extends JpaRepository<User,Long> {
    List<User> findAllByUserAge(int age);
}

Optional을 필드에서 사용 ❌

옵셔널은 필드에 사용할 목적으로 만들어지지 않았습니다.

//bad code ❌
public class User {
   private long id;
   private int age;
   private Optional<String> name;
}

// good code ✅
public class User {
   private long id;
   private int age;
   private String name;
}

Optional을 생성자나 메서드 인자로 사용 ❌

Optional을 생성자나 메서드 인자로 사용하면, 호출할 때마다 Optional을 생성해서 인자로 전달해줘야한다. 하지만 호출되는 쪽, 즉 API나 라이브러리 메서드에서는 인자가 Optional이든 아니든 null 체크를 하는 것이 안전하다. 따라서 굳이 비싼 Optional을 사용하지 말고 호출되는 쪽에 null 체크 책임을 넘기는 것이 좋습니다 !

Optional을 컬렉션의 원소로 사용 ❌

컬렉션에는 많은 원소가 들어갈 수 있습니다. 비싼 Optional을 원소로 사용하지 말고 원소를 꺼낼 때나 사용할 때 null 체크를 하는 것이 좋다. Map은 null 체크가 포함된 메서드를 제공하기 때문에(ex getOrDefault(),putIfAbsent() ...) Map의 원소로 Optional을 사용하지말고 제공하는 메서드를 활용하는 것이 좋습니다.

of(), ofNullalbe() 혼동 주의

of(X)는 X가 절대 null 이 아닌 경우에만 사용하고 Null일 가능성이 1%라도 있으면 ofNullable() 메서드를 사용해야 합니다.

마치며

Optional을 제대로 쓰는 방법은 위에서 언급한 내용을 제외하고도 꽤 많은 내용이 있었습니다. 그중에서 제가 모르고 사용할법한 내용들을 추려서 정리해보았습니다.

저는 실무에서 Optional을 자주 사용하는 편입니다. (JPA findById() << 이거땜에)
인텔리제이에서 alt + tab을 남발한 결과 Optional의 개념과 사용법을 숙지하지 못하고 사용하고 있었네요 ^^;
이제부터는 Optional을 제대로 사용하는 방법의 일부를 알게 되었으니 조금 더 효율적인 코드를 작성 할 수 있겠죠?

항상 공부해야 하는 개발자의 삶이지만 하나하나 알아가는 재미가 있네요.

이번 포스팅은 여기서 마치고 저는 다음 포스팅 주제를 찾으러 가겠습니다. 끝까지 읽어주셔서 감사합니다.💛

반응형