기본 콘텐츠로 건너뛰기

Null Object Pattern 과 Optional 사용


오늘 정리해 보고자 하는 건 누구나 다 아는 패턴 중 하나일 Null Object Pattern 에 대해서 한번 생각해 보려고 한다.

일단은 글작성 동기는 어떻게?

퇴사를 얼마 안남기고 막내 프로그래머(경력상 막내지만 착실한 친구)에게 Optional 을 설명해 주고 있었는데, 뭔가 부족한 설명을 한 것 같은 느낌이라 좀 더 어딘가에 정리를 해 놓고 싶었습니다. 
Optional 을 설명하다 보면 참 많이 돌아가게 되더구요. 전혀 생각도 못했습니다.

  • 시스템 내에서 NullPointerException 이 어떤 의미로 다가왔었는가
  • Null Check 로직이 많아지면 얼마나 코드를 읽기 어렵게 만들어 주는가
  • 이런 체크 로직이 많은 상황에서 새로운 다향성 객체가 만들어질 때 얼마나 많은 수정이 일어난는가
등등
그런 이유로 Optional 이 생겨나긴 했는데 일단 그 장점과 위험성은 정리해 놓은 분들이 많으니 블로그를 검색해봐라로 마무리 되었습니다.

그런데 말이죠 예전에도 Null Object Pattern 은 빈번하진 않아도 꽤 사용이 되었던 패턴입니다. 그 정의는 매우 단순합니다.

우선 ChatGPT 가 알려주는 Null Object Pattern 은 이렇습니다.
"Null Object Pattern is a design pattern in object-oriented programming that allows the use of a special null object to represent the absence of an object. Instead of returning a null reference from a method or property, the null object pattern provides a substitute object that behaves in a predictable and consistent way with the rest of the system. This can help to reduce the amount of null checks in code and improve code reliability and maintainability. The use of null objects is common in many programming languages, and in Java, the Optional class can be used to implement the null object pattern."

다음은 Notion AI 에게 물어봤어요.
"The Null Object Pattern is a design pattern that eliminates the need for null checks in an application. In this pattern, a null object is created to act as a surrogate for a missing object. This null object can then be used in place of the missing object, allowing the code to continue running without errors. The main advantage of this pattern is that it helps to simplify code and reduce the number of null checks that need to be performed. This can make code more readable and easier to maintain. Additionally, the null object can be used to provide default behaviors or values, reducing the need for additional conditional logic. One of the potential downsides of this pattern is that it can increase the complexity of the code. In order to use the null object effectively, it may be necessary to create additional classes and interfaces. Additionally, there may be cases where the null object does not provide a suitable replacement for the missing object, which can lead to unexpected behavior. Overall, the Null Object Pattern can be a useful tool for simplifying code and reducing the number of null checks that need to be performed. However, it is important to carefully consider the specific requirements of your application before deciding whether or not to use this pattern."

위키피디아에도 설명이 있어서 링크를 걸어봅니다. 링크는 여기를

설명은 급조된 느낌의좀 이상한 코드긴 한데 간단히 예제로 설명해 볼까 합니다.

Null Object Pattern은 보통 Strategy Pattern이나 Command Pattern 등을 사용할 때 조연으로 많이 보이는 패턴이기도 한데, 일단 가장 쉽고 편하게 만들어봤습니다.

public class PlainNullPattern {
    public static void main(String[] args) {
        ActionManager am = new ActionManager();
        am.performAction("P");
        am.performAction("T");
        am.performAction("");
        am.performAction("C");
    }

    static class ActionResult {
        public ActionResult(String actionName) {
            this.actionName = actionName;
            this.actionList = new ArrayList<>();
        }
        private String actionName;
        private List actionList;

        public String getActionName() {
            return actionName;
        }

        public void setActionName(String actionName) {
            this.actionName = actionName;
        }

        public List getActionList() {
            return actionList;
        }

        @Override
        public String toString() {
            return "Performed action " + this.actionName + ", Actions " + this.actionList.toString();
        }
    }

    static class ActionManager {
        private final Action defaultAction = new NoAction();
        private final Action physicalAction = new PhysicalAction();
        public void performAction(String type) {
            Action action = getAction(type);
            ActionResult result = action.normalAction();
            System.out.println(result.toString());
        }

        private Action getAction(String type) {
            switch(type) {
                case "P":
                    return physicalAction;
                default:
                    return defaultAction;
            }
        }
    }

    interface Action {
        ActionResult normalAction();
    }

    static class PhysicalAction implements Action {
        @Override
        public ActionResult normalAction() {
            ActionResult result = new ActionResult("PhysicalAction");
            result.getActionList().add("FirstAction");
            result.getActionList().add("SecondAction");
            return result;
        }
    }

    static class NoAction implements Action {
        @Override
        public ActionResult normalAction() {
            return new ActionResult("No Action");
        }
    }
}

뭐 대충 위와 같은 스타일이긴 합니다. 

시나리오를 설명하면

  • 몸과 마음이 너무 엉망이 되서 다양한 활동을 하고  활동 결과를 출력하려고 합니다.
  • 다양한 활동 중에서 몸의 건강을 되찾기 위해서 신체활동을 먼저 하려고 합니다. 나중엔 명상 활동도 추가되겠지만 지금은 일단 신체 활동만 하려고 해요.
  • 시작하면 다른것은 없고 해당 신체활동에 정의된 내용으로 몸을 움직이고 나중에 결과를 출력하면 됩니다.
  • 여러 활동을 해보려고 해서 행동마다 특정 코드를 줬습니다. 일반 신체활동은 P, 훈련은 T, 커뮤니티 활동은 C  그리고 아무것도 안하는 것도 일단 활동으로 하려고 해요. 다만 이 경우는 특정 로그만 남길겁니다.
뭐 대충 저런 코드를 짜게 되면 급조된 예제라 약간 이상한 부분들이 보이지만 대충 앞의 코드가 됩니다.

  1. 일단 Action 의 인터페이스를 정의해요. 이 클래스에는 normalAction 이라는 메서드 밖에 없습니다. 나중에 specialAction 이 있을지도 모른다는 생각에 명명을 저렇게 했습니다. 
  2. Action 인터페이스를 확장해서 구현한 PhysicalAction 이라는 클래스를 만듭니다. 신체활동은 두개의 Action 이 존재해서 결과의 actionList 에 두개의 활동 내용을 넣었어요. 물론 Action Result 이름엔 PhysicalAction 이라는 이름을 기록해서 이게 신체활동의 결과라는 것을 알려줬죠.
  3. 그리고 아무것도 안하는 Action 구현 클래스를 하나 만듭니다. 하는 것이라고는 결과에 아무 활동도 안함 이라는 것을 넣었어요.
  4. Action Manager 의 getAction 을 보면 "P" 파라미터 값을 제외하고 나머지는 모두 활동 안하는 것으로취급합니다.
  5. 그리고 performAction 에서는 가져온 Action 객체의 normalAction 을 호출하기만 하면 됩니다. 어떤 활동인지는 관심을 가지지 않는거죠. 일단 활동을 한다고 알려왔으니 해당 활동 클래스한테 활동을 시키고 결과를 받습니다.
일반적인 Old style 의 Null pattern 은 이렇게 구현을 했었습니다. 
시스템이 커져서 활동 이외에도 먹는것, 입는것 등을 관리하는 다양한 클래스가 나오는데 그것들을 다 저 패턴으로 만들려고 하면 큰 문제는 아니지만 괴장히 귀찮아집니다. 구현 안해도 되는 멍청하게 의미없는 행동(또는 아무 행동도 하지 않는) 클래스가 점점 늘어나게 됩니다. 그나마 뭐라도 해주는 경우라면 덜 지칠텐데 말이죠.

그래서 요즘 스타일의 소스들을 보다보니 아래와 같은 방식을 쓰는 경우가 많습니다. 즉, Optional 이라는 놈을 이용하는거죠.

public class UseOptional {

    public static void main(String[] args) {
        ActionManager am = new ActionManager();
        am.performAction("P");
        am.performAction("T");
        am.performAction("");
        am.performAction("C");
    }

    static class ActionResult {
        public ActionResult(String actionName) {
            this.actionName = actionName;
            this.actionList = new ArrayList<>();
        }
        private String actionName;
        private List actionList;

        public String getActionName() {
            return actionName;
        }

        public void setActionName(String actionName) {
            this.actionName = actionName;
        }

        public List getActionList() {
            return actionList;
        }

        @Override
        public String toString() {
            return "Performed action " + this.actionName + ", Actions " + this.actionList.toString();
        }
    }

    static class ActionManager {
        private final Action physicalAction = new PhysicalAction();
        public void performAction(String type) {
            Optional actionOptional = getAction(type);
//            ActionResult result = action.normalAction();

            actionOptional.ifPresentOrElse(action -> System.out.println(action.normalAction().toString()), () -> {
                System.out.println((new ActionResult("No Action")).toString());
            });
        }

        private Optional getAction(String type) {
            switch(type) {
                case "P":
                    return Optional.of(physicalAction);
                default:
                    return Optional.empty();
            }
        }
    }

    interface Action {
        ActionResult normalAction();
    }

    static class PhysicalAction implements Action {
        @Override
        public ActionResult normalAction() {
            ActionResult result = new ActionResult("PhysicalAction");
            result.getActionList().add("FirstAction");
            result.getActionList().add("SecondAction");
            return result;
        }
    }

}

 거의 비슷합니다. 다만 NoAction 클래스가 사라졌어요. 그리고 ActionManager.getAction 이 Optional 을 리턴합니다. 그리고 NoAction 대신 Optional.empty() 를 리턴합니다.

그리고 performAction  부분을 보시면 actionOptional.ifPresentOrElse 라는 것을 쓰고 있어요. 뭐 이 이외에도 Optional 이 제공해주는 다른 메소드를 써서도 가능합니다. 예제 특성상 저 정도가 가장 짧은 내용이더군요.

구현 클래스가 하나 줄어든 정도로 보이지만 실제로 개발자들이 만든는 프로젝트에서는 꽤 많은 클래스를 만들지 않아도 되는 장점이 있습니다.

거의 모든 패턴이 그렇지만, 저의 생각이지만 이 패턴을 꼭 써야 한다같은 생각은 해 본적이 없습니다. 보통 패턴을 쓸때는 처음부터 이 패턴을 써야지 하고 쓰는 경우는 많지 않았어요. 그 보다는 단순하게 개발해놨다가 필요한 시점에서 이런 패턴이 들어가면 코드도 간결해지고 가독성이 생긴다는 판단이 서면 리팩터링을 하는 것이 더 좋더군요.

실제 개발 과정에서 우리가 가장 많이 사용하는 Null Object Pattern 이 있는데, Collections.emptyList(). 라는 거 많이 쓰지 않으시나요? 결과가 Optional 일 때 해당 Optional 을 이용해서 다양한 메소드를 호출하곤 하죠.
Optional.emptyList 를 기본으로 하고 이후 적절한 결과를 다시 할당하면, 이후에 해당 Collection 을 처리하는 부분은 별다른 체크 없이 그대로 처리할 수 있습니다. 매번 이 Collection이 null 인지 체크가 없으니 코드는 단순해지고 가독성은 올라가게 됩니다.

참고로 일반적인 장/단점을 잠시 정리해보죠

장점

  • Null을 체크하던 보일러플레이트 코드들의 양이 확 줄어듭니다. 따라서, 가독성이 향상될 수 있습니다.
  • 적당한 Null 대용 코드들을 만들어야 하기 때문에 개발시간이 오래 걸리지만, 작동 방식을 이해하고 유지보수하는 노력 자체는 줄어들 수 있습니다.
  • Null Check 누락으로 인해 발생하는 오류를 줄일 수 있습니다. 그래서 자바 개발자들이 다 싫어하는 NullPointerException 에서 벗어날 수 있다는 것은 큰 장점일 수 있습니다. 하지만 예를 들어 Optional 은 NullPointerException 대신 다른 Exception 을 발생시킵니다.
단점
  • 클래스가 추가로 늘어나니 메모리 사용양은 증가합니다. 기본 행동에 따라서 다른 리소스들도 추가될 수 있어서 상대적으로 메모리 사용량이 많이 늘어날 수 있습니다.
  • 해당 처리에 대한 소스코드의 가독성은 향상되지만, 클래스 계층구조가 복잡해지는 단점이 있습니다.
  • Null 이 빈번하게 발생하는 시스템에서는 이 처리로 인해서 잠재적인 성능 하락을 경험할 수 있습니다.
대충 다 정리를 한 것 같습니다.

결론 
모든 패턴은 반드시 써야 한다거나 쓰지 말아야 한다거나 하는 부분은 아니라고 생각합니다. 적절한 판단에 따라 또는 개발자의 스타일에 따라 다양한 패턴이 사용되기도 하고, 아키텍처에 따라서 써야하는 경우도 있습니다. 보통은 어느정도 경험이 있는 개발자들이 결정하는 경우도 많이 있습니다. 그 때문인지 개발 연차가 얼마 안되시는 분들이 패턴공부를 하면 시기상조라고 생각하시는 분들도 많더군요.
저는 신입 프로그래머들에게도 기본적인 패턴 공부를 조금씩이라도 하라고 합니다. 대신 어려운 책들 말고 쉽게 써져 있는 책들로 공부하라고 해요. 요즘은 패턴을 쉽게 설명해주는 책들도 많아서 공부가 그리 어렵지 않거든요. 다만, 지금 배운 패턴을 "어디에 적용할꺼야"라는 생각보다는 그저 "아~~ 이런 패턴도 있구나" 정도의 마음가짐으로 일단 공부를 시작하면 되지 않을까 합니다. 
당장 프로그램에 큰 도움이 되지 않는다고 생각할 수도 있습니다. 하지만, 나중에 다른 훌륭한 개발자 분들의 소스를 볼때 프로그램의 구조를 훨씬 깊게 이해할 수 있는 기회가 오고, 본인도 비슷한 수준의 프로그램 구조로 개발할 수 있는 기반이 되지 않을까 싶네요.
Null Object Pattern은 패턴책의 교본이라고 불리는 GoF 의 디자인 패턴에도 독립 패턴으로 설명되지는 않지만, 다른 패턴 사용에서 조연의 역할을 하는 알아두면 도움되는 패턴이라고 생각합니다.

댓글

이 블로그의 인기 게시물

경력 개발자의 자기소개서에 대해서...

갑자기 뜬금없이 이런 글을 쓰다니 무슨 생각이야? 라고 생각하시는 분들이 있을지도 모르겠네요. 뜬금없음에 대한 변명은 잠시 접어두고 일단 오늘 쓰려고 하는 글을 시작해볼까 합니다. 개발자로 대충 16년을 그럭저럭 보내왔습니다. 시대적 상황으로 5년 차쯤에 대리로 처음 팀장을 시작했으니, 일반 개발자로 산 시간보다는 어쨌건 프로젝트 또는 팀의 리더로 산 시간이 더 많았던 것 같습니다. 그 기간 동안 남들보다 좀 심하게 회사를 많이 옮겨 다니다 보니 꽤 많은 면접을 볼 수 있는 경험이 있었고, 또 옮긴 회사가 대부분 팀을 리빌딩하는 곳이었다 보니 꽤 많은 채용절차에 관여할 기회가 있어서 어린 나이부터 비교적 많은 이력서를 검토했고 면접관으로도 여러 사람을 만날 수 있었습니다. 처음 면접을 보러 다니던 시절의 제 이력서의 자기소개서는 항상 "19XX년 봄 XX업계에 종사하시던 아버님과 집안일에 헌신적인 어머니의 유복한 가정에 1남 1녀의 막내로..." 로 시작되었습니다 (이 문장에 향수를 느끼시는 분들 많으실 거예요. ^^). 경력이 5년이 넘은 어느 날 도대체 이 문장을 왜 써야 하느냐는 의문이 생겨서 조금 바꾸긴 했습니다만, 그 뒤로도 꽤 오랜 세월을 이런 자기소개서가 항상 제 이력서에 붙어있었죠. 요즘 누가 저런 식으로 자기소개서를 써? 라고 생각하시는 분들 많으실 거로 생각해요. (대신 요즘은 대학 시절의 봉사활동이나 해외연수 이력이... 뭐 어차피 그놈이 그놈입니다.) 저런 자기소개서를 써야 한다는 것이 어디서 어떻게 시작된 것인지는 몰라도 회사를 그만두기 전인 2년 전까지도 약간의 표현은 다를지 모르지만 비슷한 문장으로 시작하는 자기소개서를 이력서에 첨부해서 보내는 지원자들을 볼 수 있었습니다. 이제 제가 뜬금없는 이런 글을 쓰게 된 이유를 밝히고 계속 진행해야겠네요. 블로그에 올릴 글을 준비하는 일이 생각보다 힘들어요. 블로그에 올리려고 준비한 주제에 맞는 소스를 작업하고 거기에 글을 입히다 보면 가끔

Springframework 5에서 바뀌는 것들에 대한 간단 정리 및 생각

Spring framework 5 에 대해 많은 분이 기대와 두려움을 가지고 계시지 않을까 생각합니다. 특히 기대를 하고 계신 분들은 Reactive Programming 지원을 기대하고 계시지 않은가 생각이 드는데요. 7월 초에 John Thompson 이란 분이 D-Zone에 아주 깔끔하고 멋지게 정리를 잘해서 글을 쓰셨더라구요. 해당 글은  https://dzone.com/articles/whats-new-in-spring-framework-5 에서 확인을 하실 수 있습니다. 혹시 Spring framework 5에서 달라지는 내용의 좀 더 자세한 내용이 필요하신 분들은 Spring framework github의 wiki 를 참고하시면 됩니다. 본 포스트는 언제나 그렇듯이 윗글에 대한 번역이 아닙니다. 그저 윗글을 다시 정리하면서 제 생각을 한번 정리해 놓은 포스트입니다. Spring framework 5는 현재 5.0.0.RC2(2017.07.23일 기준)까지 릴리즈된 상황입니다. Spring framework 5에서 크게 변화하는 내용을 John Thompson은 8가지로 깔끔하게 정리해주고 있습니다. 1. JDK 지원 버전의 업데이트 5버전은 원래 JDK 9 버전의 지원을 위해서 시작됐던 프로젝트로 알고 있는데 맞는지는 모르겠네요. JDK 9의 Release가 늦어져서 Spring framework 5가 먼저 Release 될 것으로 보이지만, JDK 9가 Release가 되면 언제건 적용할 수 있다고 합니다. 좀 아쉬운 부분은 JDK의 최소 버전은 JDK 8이라는 부분이 아닐까 싶네요. 이 때문에 Spring framework 5에 무관심한 분들도 많으실 거라고 생각합니다. 지금 진행하는 프로젝트는 JDK 8을 기반으로 합니다만, 최근까지 다니던 회사의 경우는 JDK 7까지가 업그레이드 한계였던 회사였습니다. 아마도 JDK 업그레이드를 쉽게 못 하시는 회사들이 많으니 "나랑은 관계없는 얘기군"

Gradle 을 이용해서 Multiproject 를 구성하는 방법 (중 하나)

개요 회사에서 Gradle 을 이용하게 된 이래로 계속 Multi-project 형태로 설정하여 진행을 해 오고 있다. 매번 멀티 프로젝트 형태로 만들어지고 있는 회사의 프로젝트들이다 보니 그때마다 다시 이전 빌드 스크립트를 보면서 만드는데 프로젝트들이 복잡하다보니 필요없는 설정들까지 복사해서 쓰고 있는 부분들이 있어서 한번 정리를 했으면 하고 있었다. 가장 기본적인 상태의 멀티프로젝트용 build.gradle 에 대한 여러가지 방법 중 한가지라 생각하고 참고가 된다면 좋겠다. 요구사항 기본 요구사항은 다음과 같다. 1) 멀티프로젝트는 디렉터리를 기반으로 아래와 같이 그룹으로 만들어 질 수 있어야 한다.   - Shared : 다른 프로젝트에서 Dependency로 추가될 수 있는 공통 라이브러리를 포함하는 라이브러리 모듈 그룹   - Web : Front 모듈 그룹   - Server : 주로 어플리케이션 간의 설정 등을 관리하는 Server 모듈 그룹   - Service : Web API 서버 모듈 그룹 2) 모든 Subproject 들은 Java project 이며, 프로젝트 명은 "모듈그룹명-모듈명" 으로 만든다. 즉, Server 모듈의 configuration-server 모듈이라면 server-configuration-server의 형태로 만들어지면 된다. 이후에 작업된 모든 내용은 Ubuntu 14.04 OS와 IntelliJ IDEA 15에서 작업되었다. (윈도우즈와 이클립스에서도 동일한 내용을 동작이 될 것으로 보인다. 다만, 테스트되지 않았을 뿐이다) Main Gradle Script 작업 1) 파일 구성   - build.gradle : gradle script 파일   - settings.gradle : sub project 관리를 위한 파일 2) build.gradle 파일 작성   - 우선은  프로젝트 기본 정보로 group 정보와 version 을 설정한다. 본인의