diff --git "a/\352\270\260\354\210\240\354\204\270\353\257\270\353\202\230/_posts/2024-08-16-Optional.md" "b/\352\270\260\354\210\240\354\204\270\353\257\270\353\202\230/_posts/2024-08-16-Optional.md" index 4119719..0e04523 100644 --- "a/\352\270\260\354\210\240\354\204\270\353\257\270\353\202\230/_posts/2024-08-16-Optional.md" +++ "b/\352\270\260\354\210\240\354\204\270\353\257\270\353\202\230/_posts/2024-08-16-Optional.md" @@ -1,5 +1,5 @@ --- -layout: post +layout: post title: "Optional" author: "송해덕" categories: "기술세미나" @@ -118,258 +118,263 @@ Brian Goetz는 Stack Overflow에서 Optional에 대한 질문에 이러한 답
1. Optional을 필드로 사용하지 말아라 - ```java - // 안 좋음 - class Person { - private String name; - private Integer age; - private Optional car; - } - - // 좋음 - class Person { - private String name; - private Integer age; - private String car; - } - ``` - 예시 코드를 보면 필드에 Optional이 사용된 필드가 보입니다. - 사람이 차를 가지고 있을수도 있고 아닐 수도 있으니 이렇게 사용하는 것이 Optional을 잘 사용하는 것 처럼 보이지만 이는 잘못 된 사용 방법입니다. - - 앞서 설명드린 것 처럼 **Optional은 반환 타입을 위해 설계된 타입**입니다. - - 그런데 이처럼 필드로 선언하게 된다면 Optional은 **serializable 인터페이스**를 구현하지 않기에 **직렬화 문제**를 가지게 됩니다. +```java +// 안 좋음 +class Person { + private String name; + private Integer age; + private Optional car; +} + +// 좋음 +class Person { + private String name; + private Integer age; + private String car; +} +``` +예시 코드를 보면 필드에 Optional이 사용된 필드가 보입니다. + +사람이 차를 가지고 있을수도 있고 아닐 수도 있으니 이렇게 사용하는 것이 Optional을 잘 사용하는 것 처럼 보이지만 이는 잘못 된 사용 방법입니다. + +앞서 설명드린 것 처럼 **Optional은 반환 타입을 위해 설계된 타입**입니다. + +그런데 이처럼 필드로 선언하게 된다면 Optional은 **serializable 인터페이스**를 구현하지 않기에 **직렬화 문제**를 가지게 됩니다.
2. Optional에 null을 할당하지 마라 - ```java - // 안 좋음 - Optional findById(Long id) { - // find person from db - if (result == 0) { - return null; - } + +```java +// 안 좋음 +Optional findById(Long id) { + // find person from db + if (result == 0) { + return null; } - - // 좋음 - Optional findById(Long id) { - // find person from db - if (result == 0) { - return Optional.empty(); - } +} + +// 좋음 +Optional findById(Long id) { + // find person from db + if (result == 0) { + return Optional.empty(); } - ``` - 코드를 보면 Person 객체를 Optional로 감싸서 반환하는 메서드가 있습니다. - - 그리고 결과 값이 0일 경우 null을 return하게 됩니다. - - 하지만 이는 Optional의 도입 의도와 맞지 않습니다. - - Optional은 null을 안전하고 일관성있게 사용하기 위함인데 Optional이 Optional 객체가 아닌 null을 참조한다는 것은 Optional을 사용함에 있어 아무런 의미가 없게 됩니다. - - 그렇기에 이런 경우 null이 아닌 `Optional.empty()`를 사용하는 것이 올바른 방법입니다. +} +``` +코드를 보면 Person 객체를 Optional로 감싸서 반환하는 메서드가 있습니다. + +그리고 결과 값이 0일 경우 null을 return하게 됩니다. + +하지만 이는 Optional의 도입 의도와 맞지 않습니다. + +Optional은 null을 안전하고 일관성있게 사용하기 위함인데 Optional이 Optional 객체가 아닌 null을 참조한다는 것은 Optional을 사용함에 있어 아무런 의미가 없게 됩니다. + +그렇기에 이런 경우 null이 아닌 `Optional.empty()`를 사용하는 것이 올바른 방법입니다.
3. Optional 대신 빈 객체를 사용해라 - ```java - // 안 좋음 - public interface PersonRepository extends JpaRepository { - Optional> findAllByNameContaining(String keyword); - } - - // 좋음 - public interface PersonRepository extends JpaRepository { - List findAllByNameContaining(String keyword); - // null이 반환되지 않으므로 Optional 불필요 - } - ``` - 사실 빈 컬렉션이나 빈 배열을 반환하는 메서드가 “값이 없음”을 보여주기 위해서 가장 옳은 방법은 그냥 **빈 객체 그 자체를 넘겨주는 것**입니다. - - 이미 빈 객체를 할당받은 변수의 경우 null을 참조하고 있는것이 아니니 Optional의 등장 배경과는 조금 동떨어진 것이 되겠죠? + +```java +// 안 좋음 +public interface PersonRepository extends JpaRepository { + Optional> findAllByNameContaining(String keyword); +} + +// 좋음 +public interface PersonRepository extends JpaRepository { + List findAllByNameContaining(String keyword); + // null이 반환되지 않으므로 Optional 불필요 +} +``` +사실 빈 컬렉션이나 빈 배열을 반환하는 메서드가 “값이 없음”을 보여주기 위해서 가장 옳은 방법은 그냥 **빈 객체 그 자체를 넘겨주는 것**입니다. + +이미 빈 객체를 할당받은 변수의 경우 null을 참조하고 있는것이 아니니 Optional의 등장 배경과는 조금 동떨어진 것이 되겠죠?
4. `Optional.get()` 전에는 값을 가지고 있는지 확인해야한다 - ```java - // 안 좋음 - Optional optionalPerson = personRepository.findById(regNo); - String name = optionalPerson.get().getName(); - - // 좋음(?) - Optional optionalPerson = personRepository.findById(regNo); +```java +// 안 좋음 +Optional optionalPerson = personRepository.findById(regNo); - if (optionalPerson.isPresent()) { - return optionalPerson.get().getName(); - } - - return "" - - ``` - `Optional.get()`을 하기 전에는 Optional 내부에 값이 있는지 없는지 확인이 필요합니다. - - 그렇지 않고 위와 같이 바로 .get()을 하게 될 경우 `NoSuchElementException`이 발생하게 됩니다.
- `NullPointerException`을 피하겠다고 Optional을 사용했는데 또 다른 에러를 마주하게 되는 것이죠 - - 그렇기에 `Optional.get()`을 사용하기 위해서는 이전에 `isPresent()`와 같은 확인 과정이 필요합니다. - - 하지만, 이 방법이 좋다…? 라고는 이야기 할 수 없습니다.
- 그 이유는 뒤에 이어지는 내용에서 설명해드리겠습니다. +String name = optionalPerson.get().getName(); + +// 좋음(?) +Optional optionalPerson = personRepository.findById(regNo); + +if (optionalPerson.isPresent()) { + return optionalPerson.get().getName(); +} + +return "" + +``` +`Optional.get()`을 하기 전에는 Optional 내부에 값이 있는지 없는지 확인이 필요합니다. + +그렇지 않고 위와 같이 바로 .get()을 하게 될 경우 `NoSuchElementException`이 발생하게 됩니다.
+`NullPointerException`을 피하겠다고 Optional을 사용했는데 또 다른 에러를 마주하게 되는 것이죠 + +그렇기에 `Optional.get()`을 사용하기 위해서는 이전에 `isPresent()`와 같은 확인 과정이 필요합니다. + +하지만, 이 방법이 좋다…? 라고는 이야기 할 수 없습니다.
+그 이유는 뒤에 이어지는 내용에서 설명해드리겠습니다. 5. `isPresent() - get()` 은 `orElse()`나 `orElseXXX`으로 처리해라 - ```java - Optional optionalPerson = personRepository.findById(regNo); - - if (optionalPerson.isPresent()) { - Person person = optionalPerson.get(); - } else { - throw new PersonNotFoundException(“Person Not Found"); - } - ``` - 화면에 있는 코드처럼 `isPresent()`를 통해 Optional 내부를 확인하고 값이 없는 경우 Exception을 발생시킨다고 가정해봅시다. - - 겉으로 보기에는 코드의 내용이 명확하고 깔끔해 보입니다. - - 만약, 이 코드에서 else 문을 사용하지 않고 리팩토링을 해야한다면 어떻게 하실건가요? - - if문의 조건에서 맨 앞에 !를 붙이고 해당하는 경우에 exception을 throw하게 하고 optionalPerson.get()메서드 부분을 밖으로 빼실 건가요? - - ```java - // 안 좋음 - Optional optionalPerson = personRepository.findById(regNo); - - if (optionalPerson.isPresent()) { - Person person = optionalPerson.get(); - } else { - throw new PersonNotFoundException("Person Not Found"); - } - - // 좋음 - Optional optionalPerson = personRepository.findById(regNo); - - Person person = personRepository.findById(regNo) - .orElseThrow(() -> new PersonNotFoundException(“Person Not Found")); - } - ``` - - Optional은 이런 복잡한 방법이 아닌 간단한 방법을 제공하고 있습니다.
- 바로 `orElseThrow()` 메서드입니다. - - 위의 코드는 아래와 같이 `orElseThrow()`를 통해 간단하게 변경할 수 있습니다. - - 이런 예외 처리가 아니라 비어 있는 경우 다른 액션을 취하고 싶다면 `orElse()`나 `orElseGet()` 같은 메서드들이 제공되고 있죠 - - 그렇기에 `isPresent() - get()`을 사용하기보다는 아래와 같은 orElse 계열의 메서드를 사용하는 것이 좋습니다. - - 물론, 매번 이 방법이 좋다고만은 할 수 없지만 Optional에서 지향하고 있는 방법이라는 것만 알아두시면 좋을 것 같습니다. - - ### 번외 (orElse()와 orElseGet()의 차이) - - 여러분은 `orElse()`와 `orElseGet()`의 차이가 있다는 것을 알고 계시나요? - - Optional 클래스에서 orElse()와 orElseGet()을 살펴보겠습니다. - - ![](https://github.com/Kernel360/blog-image/blob/main/2024/0816/orElse_orElseGet.png?raw=true) - - 살펴보니 orElseGet()에서 매개변수로 Supplier 객체가 보이는 차이점이 있고 메서드 내부에는 삼항연산자를 동일하게 사용하는 것 처럼 보이네요.
- 별반 차이가 없어보입니다. - - 이떤 차이가 있는지 잘 모르시겠다면 다음 예시를 보시면 이해하실 수 있을 겁니다. - - ```java - // orElse()를 사용 - @Test - @DisplayName("orElse 테스트") - void orElseTestMethod() { - String str = Optional.of("not empty") - .orElse(emptyReturn()); - - System.out.println(str); - } - - // orElseGet()을 사용 - @Test - @DisplayName("orElse 테스트") - void orElseTestMethod() { - String str = Optional.of("not empty") - .orElseGet(() -> emptyReturn()); - - System.out.println(str); - } - ``` - - 저는 테스트 코드를 두개 작성해봤습니다.
- 두개의 차이는 `orElse()`를 사용하냐 `orElseGet()`을 사용하냐의 차이밖에 없습니다.
- 두 코드 모두 **Optional 객체 내부가 null이면 emptyReturn이라는 메서드를 호출**하게 됩니다. - - 하지만, 저는 `Optional.of()`를 통해 **“not empty”**라는 string 문자열을 넣어줬으니 두 코드는 모두 **emptyReturn() 메서드를 실행하지 않고 “not empty”만 콘솔에 출력**하게 되겠네요. - - 과연 두 코드 모두 결과가 동일할까요? - - ```java - // orElse()를 사용한 결과 - empty object - not empty - - // orElseGet()을 사용한 결과 - not empty - ``` - - 결과를 확인해보면 두 코드는 **다른 결과**를 보여주고 있습니다. - - `orElse()`를 사용한 경우 **Optional 내부가 null이 아님에도 emptyReturn 메서드가 실행**되고 있는 모습이네요. - - 왜그럴까요? - - 다시 위에 있는 Optional 클래스로 돌아가서 두 메서드를 확인해봅시다. - - 아까 전에 말씀드린 매개변수쪽을 다시 봐보면 `orElse()`의 경우 **메서드가 아닌 값을 인수로** 받고 있습니다. - - 하지만 이와 다르게 `orElseGet()`의 경우 `Supplier`라는 함수형 인터페이스를 인수로 받고 있네요. - - 따라서 `orElse()`의 경우 메서드 인수를 할당하기 위해 그 안에 있는 메서드를 실행하게 됩니다.
- 반면에 `orElseGet()`의 경우 Optional의 값이 null일 때만 supplier.get()이 실행되게 됩니다. - -
- - 다시 앞의 코드로 돌아가보죠. - - ```java - // orElse()를 사용 - @Test - @DisplayName("orElse 테스트") - void orElseTestMethod() { - String str = Optional.of("not empty") - .orElse(emptyReturn()); - - System.out.println(str); - } - - // orElseGet()을 사용 - @Test - @DisplayName("orElse 테스트") - void orElseTestMethod() { - String str = Optional.of("not empty") - .orElseGet(() -> emptyReturn()); - - System.out.println(str); - } - ``` - `orElse()`는 **인수를 할당받기 위해 Optional의 값이 null이던 null이 아니던 emptyReturn()이 호출**되게 됩니다.
- 그렇기에 콘솔에 empty object와 not empty가 출력이 되는 것이죠. - - 반대로 `orElseGet()`의 경우 Optional이 null이 아니기 때문에 **emptyReturn이 호출되지 않는 것**이죠. - - # 결론 - 지금은 Optional을 올바르게 사용하는 방법들 중 일부만을 설명해드렸지만 이외에도 여러가지 다양한 내용들이 있습니다. - - Optional에 대한 올바른 사용방법이 궁금하신 분들은 [26가지 안티패턴](https://dzone.com/articles/using-optional-correctly-is-not-optional)을 보시면 도움이 될 것 같습니다. - - 이번 게시글에 작성된 내용처럼 우리가 어떠한 기능을 사용할 때 올바른 방법을 알고 사용하는것은 정말 중요하다고 생각됩니다. - - 그렇기에 좋다는 이야기만 듣고 혹은 눈에 보인다고 아무렇게나 사용하는 것이 아닌, 어떤 것인지를 정확하게 알아보고 올바른 사용법을 파악하는 것은 개발자에게 정말 중요한 소양이라고 생각하게 되었습니다. - - 이상으로 마치며 긴 포스트를 읽어주셔서 감사합니다. + +```java +Optional optionalPerson = personRepository.findById(regNo); + +if (optionalPerson.isPresent()) { + Person person = optionalPerson.get(); +} else { + throw new PersonNotFoundException(“Person Not Found"); +} +``` +화면에 있는 코드처럼 `isPresent()`를 통해 Optional 내부를 확인하고 값이 없는 경우 Exception을 발생시킨다고 가정해봅시다. + +겉으로 보기에는 코드의 내용이 명확하고 깔끔해 보입니다. + +만약, 이 코드에서 else 문을 사용하지 않고 리팩토링을 해야한다면 어떻게 하실건가요? + +if문의 조건에서 맨 앞에 !를 붙이고 해당하는 경우에 exception을 throw하게 하고 optionalPerson.get()메서드 부분을 밖으로 빼실 건가요? + +```java +// 안 좋음 +Optional optionalPerson = personRepository.findById(regNo); + +if (optionalPerson.isPresent()) { + Person person = optionalPerson.get(); +} else { + throw new PersonNotFoundException("Person Not Found"); +} + +// 좋음 +Optional optionalPerson = personRepository.findById(regNo); + +Person person = personRepository.findById(regNo) + .orElseThrow(() -> new PersonNotFoundException(“Person Not Found")); +} +``` + +Optional은 이런 복잡한 방법이 아닌 간단한 방법을 제공하고 있습니다.
+바로 `orElseThrow()` 메서드입니다. + +위의 코드는 아래와 같이 `orElseThrow()`를 통해 간단하게 변경할 수 있습니다. + +이런 예외 처리가 아니라 비어 있는 경우 다른 액션을 취하고 싶다면 `orElse()`나 `orElseGet()` 같은 메서드들이 제공되고 있죠 + +그렇기에 `isPresent() - get()`을 사용하기보다는 아래와 같은 orElse 계열의 메서드를 사용하는 것이 좋습니다. + +물론, 매번 이 방법이 좋다고만은 할 수 없지만 Optional에서 지향하고 있는 방법이라는 것만 알아두시면 좋을 것 같습니다. + +### 번외 (orElse()와 orElseGet()의 차이) + +여러분은 `orElse()`와 `orElseGet()`의 차이가 있다는 것을 알고 계시나요? + +Optional 클래스에서 orElse()와 orElseGet()을 살펴보겠습니다. + +![](https://github.com/Kernel360/blog-image/blob/main/2024/0816/orElse_orElseGet.png?raw=true) + +살펴보니 orElseGet()에서 매개변수로 Supplier 객체가 보이는 차이점이 있고 메서드 내부에는 삼항연산자를 동일하게 사용하는 것 처럼 보이네요.
+별반 차이가 없어보입니다. + +이떤 차이가 있는지 잘 모르시겠다면 다음 예시를 보시면 이해하실 수 있을 겁니다. + +```java +// orElse()를 사용 +@Test +@DisplayName("orElse 테스트") +void orElseTestMethod() { + String str = Optional.of("not empty") + .orElse(emptyReturn()); + + System.out.println(str); +} + +// orElseGet()을 사용 +@Test +@DisplayName("orElse 테스트") +void orElseTestMethod() { + String str = Optional.of("not empty") + .orElseGet(() -> emptyReturn()); + + System.out.println(str); +} +``` + +저는 테스트 코드를 두개 작성해봤습니다.
+두개의 차이는 `orElse()`를 사용하냐 `orElseGet()`을 사용하냐의 차이밖에 없습니다.
+두 코드 모두 **Optional 객체 내부가 null이면 emptyReturn이라는 메서드를 호출**하게 됩니다. + +하지만, 저는 `Optional.of()`를 통해 **“not empty”**라는 string 문자열을 넣어줬으니 두 코드는 모두 **emptyReturn() 메서드를 실행하지 않고 “not empty”만 콘솔에 출력**하게 되겠네요. + +과연 두 코드 모두 결과가 동일할까요? + +```java +// orElse()를 사용한 결과 +empty object +not empty + +// orElseGet()을 사용한 결과 +not empty +``` + +결과를 확인해보면 두 코드는 **다른 결과**를 보여주고 있습니다. + +`orElse()`를 사용한 경우 **Optional 내부가 null이 아님에도 emptyReturn 메서드가 실행**되고 있는 모습이네요. + +왜그럴까요? + +다시 위에 있는 Optional 클래스로 돌아가서 두 메서드를 확인해봅시다. + +아까 전에 말씀드린 매개변수쪽을 다시 봐보면 `orElse()`의 경우 **메서드가 아닌 값을 인수로** 받고 있습니다. + +하지만 이와 다르게 `orElseGet()`의 경우 `Supplier`라는 함수형 인터페이스를 인수로 받고 있네요. + +따라서 `orElse()`의 경우 메서드 인수를 할당하기 위해 그 안에 있는 메서드를 실행하게 됩니다.
+반면에 `orElseGet()`의 경우 Optional의 값이 null일 때만 supplier.get()이 실행되게 됩니다. + +
+ +다시 앞의 코드로 돌아가보죠. + +```java +// orElse()를 사용 +@Test +@DisplayName("orElse 테스트") +void orElseTestMethod() { + String str = Optional.of("not empty") + .orElse(emptyReturn()); + + System.out.println(str); +} + +// orElseGet()을 사용 +@Test +@DisplayName("orElse 테스트") +void orElseTestMethod() { + String str = Optional.of("not empty") + .orElseGet(() -> emptyReturn()); + + System.out.println(str); +} +``` +`orElse()`는 **인수를 할당받기 위해 Optional의 값이 null이던 null이 아니던 emptyReturn()이 호출**되게 됩니다.
+그렇기에 콘솔에 empty object와 not empty가 출력이 되는 것이죠. + +반대로 `orElseGet()`의 경우 Optional이 null이 아니기 때문에 **emptyReturn이 호출되지 않는 것**이죠. + +# 결론 +지금은 Optional을 올바르게 사용하는 방법들 중 일부만을 설명해드렸지만 이외에도 여러가지 다양한 내용들이 있습니다. + +Optional에 대한 올바른 사용방법이 궁금하신 분들은 [26가지 안티패턴](https://dzone.com/articles/using-optional-correctly-is-not-optional)을 보시면 도움이 될 것 같습니다. + +이번 게시글에 작성된 내용처럼 우리가 어떠한 기능을 사용할 때 올바른 방법을 알고 사용하는것은 정말 중요하다고 생각됩니다. + +그렇기에 좋다는 이야기만 듣고 혹은 눈에 보인다고 아무렇게나 사용하는 것이 아닌, 어떤 것인지를 정확하게 알아보고 올바른 사용법을 파악하는 것은 개발자에게 정말 중요한 소양이라고 생각하게 되었습니다. + +이상으로 마치며 긴 포스트를 읽어주셔서 감사합니다.