ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 자바8 Optional 활용 1:NullPointException 뿌시기
    자바(Java) 강의/자바 8 2019. 1. 31. 15:21


    소프트웨어 엔지니어/개발자라면 늘 주의해야 하는 것이 있다. 바로 null체크를 하는 것이다. null체크는 아주 간단하지만 매우 귀찮다. 또 어떤 경우에는 코드의 심미성을 저하시킨다. 자바 8에서는 lambda와 함께 null체크를 대체 할 수 있는 Optional이라는 클래스를 제공한다. 오늘은 이 Optional의 기본적인 사용방법에 대해 설명하도록 하겠다.


    들어가기 전에...

    이 포스트는 여러분이 JDK 8을 이상을 사용하고 있으며 기본적인 자바 개발 환경 설정을 마쳤고 Java 8의 Lambda의 기본 사용법을 알고 있다고 몰라도 가능하긴 함 가정한다.


    NullPointerException이란?

     NullPointerException이란 프로그램이 null인 오브젝트의 멤버에 접근하려 할 때 발생하는 예외상황이다. 예를들어 다음 코드를 보자.

    public class Application {

    public static void main(final String [] args) {
    Person p = null;
    System.out.println(p.name);
    }

    class Person {
    String name;
    int age;
    }
    }

    위의 코드에서 Person p = null; // null이 할당 된 것을 볼 수 있다. 이처럼 p에 아무 오브젝트로 할당되지 않았기 때문에 p는 무늬만 Person이고 사실상 아무것도 접근 할 수 없다. 이 코드를 실행시키면,

    Exception in thread "main" java.lang.NullPointerException
    at Application.main(Application.java:5)

    다음과 같은 예외가 발생한다. 이해가 더 필요하다면 다음의 그림을 보자.

    Person p = new Person() 명령어는 내가 Person을 위해 사용 할 공간을 메모리에 할당 한 후, 그 레퍼런스를 p에게 할당하라는 뜻이다. 그러나 Person p = null을 할 경우, p에는 아무런 레퍼런스도 없으므로 실행 시 NullPointerException이 발생하는 것이다.

    null 체크 : if ( obj == null ) 

      이런 상황을 막기 위해서 소프트웨어 엔지니어들은 사용하는 오브젝트 들에 대해 항상 null 체크를 한다.

    public class Application {

    public static void main(final String [] args) {
    Person p = null;
    if( p == null ) {
    System.out.println("Person p is NULL!");
    return;
    }
    System.out.println(p.name);
    }

    class Person {
    String name;
    int age;
    }
    }

    null 체크를 하는 방법은 위처럼 간단하게 if ( obj == null ) 조건문으로 체크 할 수 있다. Java 8에서는 Optional이라는 것을 제공하는데, Optional이란 그냥 어떤 오브젝트의 Wrapper 오브젝트라고 생각하면 이해하기 쉽다.

    Optional

    import java.util.Optional;
    Optionaljava.util.Optional 패키지에 있으므로, 해당 패키지를 import하라.

    Optional.empty()

    import java.util.Optional;

    public class Application {

    public static void main(final String [] args) {
    Optional<Person> oP = Optional.empty(); //핵심

    if(oP.isPresent()) {
    Person p = oP.get();
    System.out.println("p exists " + p.name);
    }
    }

    static class Person {
    String name;
    int age;
    Person(String name, int age) {
    this.name = name;
    this.age = age;
    }
    }
    }

    위의 예제는 Optional.empty()는 빈 Optional 클래스를 리턴한다. null을 할당하는 것과 비슷한 느낌이지만 다른 점은 Optional이라는 오브젝트가 생성되고 그 안의 Personnull이라는 것이다. 따라서 바로 아랫줄의 isPresent()와 같은 api를 사용 할 수 있다. isPresent는 내부에 감싸진 오브젝트가 null이면 false, null이 아니면 true를 리턴한다. 따라서 내부에 Person 오브젝트가 존재하는 경우 조건문 안으로 분기한다. Person 오브젝트가 내부에 존재하는 것을 확인했으므로 get을 이용해 오브젝트를 가져오면 된다. isPresent로 널 여부를 확인했기 때문에 여기서부터는 pnull이 아님을 확신 할 수 있다.

    다시한번 api에 대해 설명하도록 하겠다.

    • Optional<Personal> : 이 Optional클래스는 Person타입의 오브젝트를 저장 할 것.

    • Optional.empty(): Optional 오브젝트를 생성하되 안의 Personnull로 지정 할 것.

    • isPresent() : Optional안의 Person 오브젝트가 존재하는가?

    • get() : Optional안의 Person 오브젝트를 가져 올 것.

    Optional.of(...)

    실제로 Person을 가진 Optional 오브젝트를 생성하는 방법은 다음과 같다.

    import java.util.Optional;

    public class Application {

    public static void main(final String [] args) {
    Optional<Person> oP = Optional.of(new Person("fsotwareengineer", 17)); //핵심
    Optional<Person> oP = Optional.empty(); //핵심
    if(oP.isPresent()) {
    Person p = oP.get();
    System.out.println("p exists " + p.name);
    }
    }

    static class Person {
    String name;
    int age;
    Person(String name, int age) {
    this.name = name;
    this.age = age;
    }
    }
    }

    이렇게 하면 Optional의 안에 새 Person 오브젝트를 할당 할 수 있다.


    Optional.ofNullable(..)

    Optional.of는 내부 오브젝트가 null일 때 쓸 수 없다. 혹시 내부에 들어가는 오브젝트가 null인지 아닌지 확실하지 않다면 Optional.ofNullable을 사용해야 한다. 아래의 예제를 보자.

    import java.util.ArrayList;
    import java.util.List;
    import java.util.Optional;

    public class Application {

    static List<Person> persons = new ArrayList<>();
    public static void main(final String [] args) {
    String name = "fsoftwareengineer";
    Optional<Person> oP = Optional.ofNullable(search(name)); //핵심
    if(oP.isPresent()) {
    Person p = oP.get();
    System.out.println("p exists" + p.name + " " + p.age);
    }
    }

    public static Person search(final String name) {
    for(Person p : persons) {
    if(p.name.equalsIgnoreCase(name)) {
    return p;
    }
    }
    return null;
    }

    static class Person {
    String name;
    int age;
    Person(String name, int age) {
    this.name = name;
    this.age = age;
    }
    }
    }

    예제에서 우리는 search 메서드를 이용하여 이름이 같은 Person을 찾아 리턴한다. 같은 이름이 없는 경우에는 null을 리턴한다. 따라서 이 메서드를 사용할 때 오브젝트가 리턴되는지 null이 리턴되는지 확신 할 수 없다. 그럴 때는 위 처럼 Optional.ofNullable을 사용한다.


    orElse(..)

    search메서드를 이용해 검색 했는데 null일 경우 새로운 오브젝트를 만들고 싶다고 가정해보자. 그럴때는 orElse를 사용 할 수 있다.

    public static void main(final String [] args) {
    String name = "fsoftwareengineer";
    Person p = Optional.ofNullable(search(name)).orElse(new Person(name, 17)); //핵심
    System.out.println("p exists " + p.name + " " + p.age);
    }

    수정된 코드를 보면,

    1) Optional<Person> 대신 Person을 사용 할 수 있게 되었다. 뒤의 Optiona.ofNullable부분이 Person을 반드시 리턴하기 때문이다. 내부의 오브젝트가 존재하면 그 오브젝트를 리턴하고 그게 아니라면 orElse에서 새로운 오브젝트를 만들어 리턴한다. 

    2) Optional<Person>이 사라졌고 p은 반드시 null이 아니기 때문에 isPresentnull체크를 할 필요가 없어졌다.


    orElseGet(..)

    orElse가 아니라 orElseGet을 사용하는 방법도 있다. 예를들어 new Person을 만드는 곳이 다른 메서드에 있다고 해보자.

    public static void main(final String [] args) {
    String name = "fsoftwareengineer";
    Person p = Optional.ofNullable(search(name)).orElseGet(() -> createPerson(name, 17));
    System.out.println("p exists " + p.name + " " + p.age);
    }
    public static Person createPerson(final String name, final int age) {
    return new Person(name, age);
    }

    orElseGetorElse와 달리 람다함수(lambda function) Supplier를 인풋으로 받는다. (람다가 익숙하지 않다면 넘어가도 좋다.)


    orElse vs orElseGet

    그렇다면 두 메서드의 차이점은 무엇인가? 두 메서드는 ofNullable의 안에 null이 들어가는 경우 차이가 없다. 대신 그 반대의 경우 차이를 보인다. 다음처럼 orElseGet을 이용해 코드를 수정해보자.

    public static void main(final String [] args) {
    String name = "fsoftwareengineer";
    Person dummyPerson = new Person(name, 17);
    Person p = Optional.ofNullable(dummyPerson).orElseGet(() -> createPerson(name, 17));
    System.out.println("p exists " + p.name + " " + p.age);
    }
    public static Person createPerson(final String name, final int age) {
    System.out.println("CreatePerson 메서드 안!");
    return new Person(name, age);
    }

    이 코드를 실행하면 결과는 다음과 같다.

    p exists fsoftwareengineer 17

    이제 orElse를 이용해 수정 해 보자.

    public static void main(final String [] args) {
    String name = "fsoftwareengineer";
    Person dummyPerson = new Person(name, 17);
    Person p = Optional.ofNullable(dummyPerson).orElse(createPerson(name, 17));
    System.out.println("p exists " + p.name + " " + p.age);
    }
    public static Person createPerson(final String name, final int age) {
    // 예를들어서 Person을 만든 후 데이터베이스에 저장한 후 리턴 한다거나...
    // 다양한 비즈니스 로직 들어가는 경우 문제가 생길 수 있음.
    System.out.println("CreatePerson 메서드 안!");
    return new Person(name, age);
    }

    결과가 어떻게 나오는가?

    CreatePerson 메서드 안!
    p exists fsoftwareengineer 17

    이렇게 나오지 않는가? 그렇다. orElseGetOptional내부의 오브젝트가 null일 때만 orElseGet안의 함수를 하지만, orElse는 오브젝트 내부의 null여부에 상관 없이 그냥 실행하고 본다. 왜 주의해야 할까? 코멘트로 남겼듯이 createPerson이 오브젝트를 생성하는 것 뿐만 아니라 데이터베이스에 무언갈 생성한다던지, 아니면 다른 비즈니스 로직을 실행하는 경우 다른 사이드이펙트를 초래할 가능성이 있기 때문이다.


    orElseThrow(..)

    만약 당신의 메서드가 null을 절대 허용하지 않는 다면 null이 넘어오는 경우 예외를 발생시킬 수 있다. 메인의 코드를 다음과 같이 고쳐보자.

    public static void main(final String [] args) {
    Person dummyPerson = null;
    Person p = Optional.ofNullable(dummyPerson).orElseThrow(() -> new RuntimeException());
    System.out.println("p exists " + p.name + " " + p.age);
    }

    이제 dummyPersonnull인 경우 RuntimeException을 발생시킬 것이다. 주로 인풋 검증(Validation)에서 많이 쓰인다.

    public void doSomethingOnPerson(final Person p) {
    Person person = Optional.ofNullable(p).orElseThrow(IllegalArgumentException::new);
    String name = Optional.ofNullable(person.name).orElseThrow(IllegalArgumentException::new); ...

    }

    <Optional을 이용한 사용자 인풋 검증의 예>


    이 이외에도 filter, map, flatMap등 다른 API들은 2부 포스트에서 설명하도록 하겠다. Optional은 반드시 필요한 함수는 아니기에 소프트웨어 엔지니어/개발자들 사이에서 호불호가 존재한다. 어떤 사람들은 nullOptional로 전부 대체하는 사람도 있고 기존의 방법을 고수하는 사람도 있다. 중요한건 둘 중 하나의 방법을 이용해 안정성 있고 유지보수가 용이한 소프트웨어를 만드는 것이다.

    '자바(Java) 강의 > 자바 8' 카테고리의 다른 글

    자바 Lambda (람다)  (4) 2019.08.20

    댓글

f.software engineer @ All Right Reserved