From f19e47f685d945f8ed42394631255e9e3aebba75 Mon Sep 17 00:00:00 2001 From: Haedeok Song <86779839+ss0ngcode@users.noreply.github.com> Date: Fri, 6 Sep 2024 16:03:36 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:=2020240816=5FOptional=5F=EC=86=A1?= =?UTF-8?q?=ED=95=B4=EB=8D=95=20(#77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_posts/2024-08-16-Optional.md" | 375 ++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 "\352\270\260\354\210\240\354\204\270\353\257\270\353\202\230/_posts/2024-08-16-Optional.md" 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" new file mode 100644 index 0000000..4119719 --- /dev/null +++ "b/\352\270\260\354\210\240\354\204\270\353\257\270\353\202\230/_posts/2024-08-16-Optional.md" @@ -0,0 +1,375 @@ +--- +layout: post +title: "Optional" +author: "송해덕" +categories: "기술세미나" +banner: + image: "assets/images/post/2023-11-05.webp" + background: "#000" + height: "100vh" + min_height: "38vh" + heading_style: "font-size: 4.25em; font-weight: bold; text-decoration: underline" + tags: ["Optional", "안티패턴", "NullPointerException"] +--- + +# Optional란? + +안녕하세요. Kernel 360 백엔드 2기 크루 송해덕입니다. + +우리가 `Spring JPA`를 사용할 때 `findByID`를 하게 되면 반환값으로 항상 **Optinal**을 Return하게 됩니다. + +그런데, 이런 Optional을 여러분은 올바르게 사용하고 계신가요?
+오늘은 우리가 자주 사용하지만 올바르게 사용하지는 못하는 Optional에 대해 알아보겠습니다. + +어떤 기능을 알아보기 위해 가장 좋은 방법은 **Documentation**을 찾아보는 것입니다. + +Optional은 JDK 8에서 람다식, Stream과 함께 등장하였습니다.
+Oracle에서 제공하는 JDK DOCS를 보면 Optioanl을 이렇게 표현하고 있습니다. + +![](https://github.com/Kernel360/blog-image/blob/main/2024/0816/JDK_DOCS.png?raw=true) + +Documentation에는 이와 같이 명시되어 있지만 해당 정의만 본다면 약간 이해하기 어려울 수 있습니다.
+ +이해하기 쉽도록 하나의 간단한 예시를 보겠습니다. + +![](https://github.com/Kernel360/blog-image/blob/main/2024/0816/JPA_Optional.png?raw=true) + +저는 stationRepository라는 곳에서 findById를 통해 지하철역을 검색하려고 합니다.
+그렇기에 반환 지하철역 entity로 받으려고 했지만 에러가 발생하게 됩니다. + +왜그럴까요? 에러를 확인하기 위해 빨간줄에 마우스를 올려보았습니다.
+화면을 보면 findById를 사용할 경우 반환값으로 Optional을 요구합니다. + +이를 확인하기 위해 Optional class를 확인해봅시다.
+클래스 내부에서 findById 메서드를 확인해보니 제네릭 T를 감싼 Optional 보이네요. + +그렇다면 이를 확인했으니 아래와 같이 반환 값을 Optional로 감싸면 에러가 발생하지 않게 됩니다.
+그 아랫줄 처럼 `get()` 메서드를 사용하여 entity를 꺼낼 수 있겠네요. + +# Optional을 사용하는 이유? + +자, 그럼 우리는 이런 Optional을 왜 사용하는지 생각해봐야 합니다.
+어차피 get() 같은 메서드로 Optional 내부에 있는 객체를 꺼내 쓸꺼면 그냥 Optional로 감싸지 않고 바로 쓰면 되는걸 이런 귀찮은 작업을 대체 왜 하는걸까요? + +![](https://github.com/Kernel360/blog-image/blob/main/2024/0816/NullpointerException.png?raw=true) + +바로 이 보기도 싫은 NullPointerException을 방지하기 위함입니다. + +## NullPointerException은 무엇인가? + +자 그럼 또 다시 NullPointerException은 무엇일까요?
+우리가 개발을 처음 시작하면서부터 지금까지 질리도록 본 이 NullPointerException이 대체 왜 발생하게 되는걸까요? + +이를 설명하기 위해 우리는 JVM의 메모리 동작을 간단하게 살펴보겠습니다.
+처음 JVM의 메모리를 알아보면 일반적으로 **method영역, stack영역, heap영역** 이렇게 3가지의 영역을 두고 이야기를 많이 합니다.우리는 이 중 **stack영역**과 **heap영역**을 두고 이야기를 하겠습니다. + +![](https://github.com/Kernel360/blog-image/blob/main/2024/0816/JVM.png?raw=true) + +text라고 명명한 String에 “Hello, World!”라는 문자열을 담아 선언한다 가정해보죠.
+이렇게 String 객체를 선언할 경우 메모리 영역에서는 어떻게 동작하게 될까요? + +화면처럼 stack 영역에는 String text가 heap 영역에는 “Hello, World”가 저장되게 됩니다.
+(사실 실제로는 String의 경우 컴파일 시 문자열 상수 풀에 저장되지만 설명을 위해 이렇게 표현했습니다.) + +stack 영역에는 지역변수나 매개변수가, heap 영역에는 인스턴스 객체가 저장되기에 당연한 것이겠죠
+이 상황에서 String text는 주소를 가지고 있으며 Heap 영역의 “Hello, World” 를 가르키게 되죠 + +자, 그럼 이런 상황(null 참조 객체)이라면 어떻게 될까요? + +위와 동일하게 String empty는 stack 영역에 저장됩니다.
+하지만 반대로 heap영역에는 아무것도 저장되지 않습니다. + +왜그럴까요? 바로 empty가 참조하는 null이 그 이유입니다.
+null은 참조 변수가 어떤 객체도 가리키지 않음을 의미합니다.
+그렇기에 메모리에 저장될 것도 없는 것이죠. + +이 상황에서 empty를 호출해서 길이를 알아보려 한다면 사진과 같이 `NullpointerException`이 발생하게 됩니다. + +이와 같이 NullpointerException은 **아무것도 참조하지 않는 Null 객체 및 변수를 호출할 때** 발생되는 예외입니다. + +# Optional을 올바르게 사용하는 방법 + +그럼 다시 Optional로 돌아와서 여러분들이 이미 Optional을 사용하고 계신다면 어떻게 사용하고 계시나요? + +혹시 내가 작성한 Optional 관련 코드가 잘못되었나 의심해본적은 없으신가요? + +Optional을 올바르게 사용하기 위해서는 어떻게 해야할까요?
+바로 **개발자의 의도**를 파악하는게 중요하겠죠. 이 기능을 만든 개발자가 어떤 생각으로 어떤 뜻을 가지고 만들었는가가 가장 중요합니다. + +Oracle에서 Java Architect로 일하고 계신 Brian Goetz는 Optional을 주도해서 개발한 개발자입니다. + +Brian Goetz는 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", and using null for such was overwhelmingly likely to cause errors. + +또한, Oracle JDK 9 이후에는 Optional에 이러한 부가 설명이 달려있습니다. + +> 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. + +결국 이를 종합해보면 Optional은 **제한적으로 사용해야 한다**는 이야기가 됩니다.
+모든 메서드가 그렇겠지만 Optional 사용시에는 **제약**이 붙는다고 봐야 하겠죠. + +![](https://github.com/Kernel360/blog-image/blob/main/2024/0816/26%EA%B0%80%EC%A7%80_%EC%95%88%ED%8B%B0%ED%8C%A8%ED%84%B4.png?raw=true) + +이러한 Optional의 특성때문에 한 커뮤니티에는 **Optional의 안티패턴 26가지**를 설명하는 글이 있습니다. + +우리는 그 중 몇가지만을 확인해보도록 하겠습니다. + +
+ +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 인터페이스**를 구현하지 않기에 **직렬화 문제**를 가지게 됩니다. + +
+ +2. Optional에 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(); + } + } + ``` + 코드를 보면 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의 등장 배경과는 조금 동떨어진 것이 되겠죠? + +
+ +4. `Optional.get()` 전에는 값을 가지고 있는지 확인해야한다 + ```java + // 안 좋음 + Optional optionalPerson = personRepository.findById(regNo); + + 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)을 보시면 도움이 될 것 같습니다. + + 이번 게시글에 작성된 내용처럼 우리가 어떠한 기능을 사용할 때 올바른 방법을 알고 사용하는것은 정말 중요하다고 생각됩니다. + + 그렇기에 좋다는 이야기만 듣고 혹은 눈에 보인다고 아무렇게나 사용하는 것이 아닌, 어떤 것인지를 정확하게 알아보고 올바른 사용법을 파악하는 것은 개발자에게 정말 중요한 소양이라고 생각하게 되었습니다. + + 이상으로 마치며 긴 포스트를 읽어주셔서 감사합니다.