주요글: 도커 시작하기


최근에 온라인 쇼핑 시스템을 구축하는 프로젝트를 시작했다. 쇼핑 시스템 자체는 솔루션을 약간 커스터마이징해서 사용할 예정이다. 이 시스템의 사용자는 다음의 요구 사항을 갖고 있다.

  • 현재 사용중인 ERP 시스템에서 쇼핑 시스템의 판매 정보(매출 등)도 함께 보고 싶음
  • 현재 사용중인 ERP 시스템의 재고와 쇼핑 시스템의 재고를 맞추고 싶음
  • 이 ERP 시스템은 외부에서 서비스로 제공
우리가 맡은 책임 중 하나는 쇼핑 시스템과 외부 ERP 시스템 사이의 연동을 처리하는 것이다. 이 프로젝트는 이제 막 시작되었으며 현재는 쇼핑 시스템의 디자인과 요구 사항을 정리하는 과정에 있다. 아직 쇼핑 시스템의 개발자가 본격 투입 전이고, ERP 시스템과의 연계 방식을 논의한 회의를 몇 차례 가졌다.

이 시점에서 우리는 무엇을 할 수 있을까? ERP 시스템과의 연동을 위한 논의만 했을 뿐, 아직 연동을 할 수 있는 시스템은 마련되지 않았다. 또한, 쇼핑 시스템과의 연계 방식은 논의조차 시작하지 못했다. 우리가 만들어야 할 (쇼핑 시스템과 외부 ERP 시스템 간의) 중계 기능의 구현을 시작하기에는 미결정 사항들이 (많이) 존재한다. 게다가 그 결정을 바로 할 수 있는 것도 아니다.

문제는 이 프로젝트의 일정이 녹녹치 않다는 데 있다. 만약 우리가 중계 기능을 제 때에 만들어내지 못하면, 우리 때문에 전체 일정이 밀리게 된다. 그런데, 구현을 위해 필요한 결정을 지금 바로 할 수 없는 처지이다. 그렇다면, 우리는 나중에 뒤에 가서 '그 때 외부 ERP 시스템 개발자와 쇼핑 시스템 개발자가 구현에 필요한 정보를 늦게 알려줘서 어쩔 수 없었다'라고 변경해야 하나? 이런 변명이 통한다면야 좋겠지만, 뒤에 가서 일정을 맞추느라 개!고생을 하게 될 것이다.

고수준 모듈과 저수준 모듈 구별하기

자! 그럼, 우리는 뻔히 보이는 고생을 그대로 받아들여야 하나? 답은 "아니오"이다. ERP 시스템과의 연동 API가 정해지지 않았어도, 아직 쇼핑몰 시스템의 개발자와 미팅을 할 수 없어도, 우리는 중계 기능의 일부를 미리 만들어 낼 수 있다. 우리가 만들 수 있는 영역은 바로 "고수준 모듈"과 관련된 코드이다.

소프트웨어는 고수준 모듈과 저수준 모듈로 구분할 수 있다. 고수준 모듈은 의미 있는 기능을 제공하는 모듈이며, 저수준 모듈은 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현이 된다. 우리가 만들어볼 중계 기능의 일부를 예로 들면 고수준 모듈과 저수준 모듈은 아래와 같이 구분할 수 있다.


여기서, 우리가 저수준 모듈에서 할 수 있는 것이라곤 결과를 보관할 DB 테이블을 정하는 정도이다. 쇼핑 DB에서 데이터를 가져오는 방법도, ERP 서비스 제공 업체에 데이터를 넘기는 방법도 우리가 마음대로 할 수 없다. 하지만, 이런 저수준의 구현 상세함이 정해지지 않았다 하더라도 우리는 고수준의 기능을 구현할 수 있다. 비밀은 바로 DIP에 있다.


DIP를 적용해서 저수준 모듈 없이 고수준 모듈 구현하기


DIP는 Dependency Inversion Principle의 약자로, 이 원칙에 대한 설명은 본 글에서는 생략한다. 이에 대한 내용이 궁금한 독자는 http://en.wikipedia.org/wiki/Dependency_inversion_principle 글 또는 필자가 지은 '객체 지향과 디자인 패턴' 책을 참고하기 바란다.


DIP를 적용해서 저수준 모듈이 고수준 모듈에 의존하게 바꾸면, 저수준 모듈의 상세한 구현 없이도 고수준 모듈을 (어느 정도 수준까지) 만들어낼 수 있다. 아래 그림은 DIP를 적용한 결과를 보여주고 있다.



OrderInfoSync가 의존하는 타입은 모두 인터페이스이므로 Mock을 이용해서 얼마든지 다양한 시나리오의 테스트 코드를 만들 수 있다. 우리는 아래쪽에 붉은 색 상자안에 위치한 저수준 모듈 클래스의 구현이 존재하는지 여부에 상관없이 OrderInfoSync 클래스를 구현할 수 있다.


실제로 지금 이 방식으로 외부 연동을 위한 고수준 모듈의 코드를 한 줄 한 줄 만들어나가고 있다. 물론, 저수준 모듈의 구현없이 고수준 모듈을 완벽하게 구현할 순 없지만, 상당한 양의 구현을 사전에 미리 만들어 낼 수가 있다. 그리고 이 핵심에는 DIP가 있다!



지인분께서 http://arload.wordpress.com/2009/02/18/misconception_of_gof_dp/ 이 링크를 보내주시면서, 의견을 물으셨다. 이 글을 보니 뭔가 결론이 글을 읽는 사람들에겐 불명확하다. 이 글의 주요 요지는 이렇다.


블로그 내용 요점 정리:

  • Erich Gamma의 인터뷰(http://www.artima.com/lejava/articles/designprinciplesP.html)의 내용:
    • A common misunderstanding is that composition doesn’t use inheritance at all. Composition is using inheritance, but typically you just implement a small interface and you do not inherit from a big class (번역해 보자면: 조합에 대한 잘못된 이해는 상속을 전혀 사용하지 않는다고 생각하는 건데, 실제로 조합은 상속을 사용한다. 단지, 큰 클래스를 상속받는 것이 아니라 작은 인터페이스를 구현할 뿐이다.)
  • 인터뷰 기반 정리:
    • 두번재 원칙인 Favor Object Composition over Class Inhertance 에서 Object Composition은” 상속을 사용하지 말라는 의미가 아니다”라는 것
    • Object Composition은 내부적으로 Interface Inheritance를 사용하다는 말이 명시적으로 있음
  • GoF 책 언급:
    • Class inheritance defines an object’s implementation in terms of another object’s implementation In short, it’s a mechanism for code and representation sharing..
  • GoF 책 문장으로 정리:
    • 두번째 원칙의 Class Inheritance는 Implementation Inheritance 였던 것
  • 그리고, 두 번째 원칙을 명확히 함:
    • Favor Object Composition (with Interface Inheritance) over Implementation Inheritance.
    • Favor Object Composition (with Subtyping) over Subclassing.


최종적으로 "Composition over inheritance"는 "구현 상속 보다는 인터페이스 상속을 사용한 객체 조합을 선호"하라는 말로 해석되어 질 수 있다고 결론을 내린다. 그런데, 문제는 여기서부터 출발한다. "인터페이스 상속을 사용한 객체 조합"? 완전 오해의 소지가 충만하다.


객체 지향을 잘 모르는 분들이 위 글을 보면 정말로 무슨 의미인지 정확히 이해가 안 될 수 있다. 그리고, 필자도 "구현 상속 보다는 인터페이스 상속을 이용한 객체 조합"이란 문장을 보면서 어떤 구조로 받아들여야 할지 애매하다.


여기서 한 가지 명확하게 할 점은,, "조합은 상속을 사용"한다라는 Erich Gamma의 인터뷰 내용이다. 여기서 상속을 사용한다는 말은 조합 대상이 되는 객체들이 상속을 사용한다라는 의미이다. 예를 들어, Erich Gamma가 예로 든 자바의 Listener를 보자.



Erich Gamma가 말한 "조합은 상속을 사용"한다라는 것은, 다른 객체를 포함하는 Button 객체가 아니라 조립 대상이 되는 ActionListener 타입의 객체에서 상속을 사용한다는 것이다. Erich Gamma의 인터뷰에 있는 " typically you just implement a small interface and you do not inherit from a big class" 이 문장은 Button 입장이 아니라, SomeActionListener 입장에서 쓰여진 문장이다. 즉, 일반적으로 조립 대상이 되는 객체(SomeActionListener)는 큰 클래스를 (구현) 상속 받기 보다는 (위 예제의 ActionListener와 같은) 작은 인터페이스를 상속받는다고 말한 것이다.


다시 Erich Gamma의 인터뷰 내용인 'A common misunderstanding is that composition doesn’t use inheritance at all. Composition is using inheritance, but typically you just implement a small interface and you do not inherit from a big class"의 의미를 해석해 보자. 이는 아래와 같다.

  • 조합을 사용한다고 해서 전혀 상속을 사용하지 않는 것은 아니다.
  • 조합 대상이 되는 객체에서 상속을 사용한다. (예, SomeActionListener의 객체는 ActionListener 인터페이스를 상속하고 있다.)

그런데, 앞서 블로그 글에서 내린 결론은 "구현 상속 보다는 (인터페이스 상속을 사용한) 객체 조합을 선호하라"인데, 이 문장만 보면, Erich Gamma가 전달하고자 했던 "조립 대상이 되는 객체가 (인터페이스) 상속을 사용해서 구현"한다라는 의미가 잘 드러나지 않는다. 


링크의 글에서 정확하게 어떤 내용으로 "Favor Object Composition (with Interface Inheritance) over Implementation Inheritance."라고 했는지 모르겠지만(정말, 이 문장만으로는 정확하게 무엇을 말하는지 잘 모르겠다), 링크의 글을 보는 분들은 Erich Gamma가 말한 "Composition is using inheritance"라는 문장을 오해 없이 이해하길 바란다.

김용훈님(http://bluepoet.me)과 진행했었던 리팩토링 사례를 공유합니다.


비지터 패턴 요약



GoF 커맨드 패턴 요약




GoF 패턴 Chain of Responsibility 요약



GoF 디자인 패턴의 Template Method (템플릿 메서드) 패턴 요약




GoF Strategy 패턴 요약



GoF 패턴 State 요약




요즘 이런 저런 것들을 정리해보고 있는데, 그 중 일부 내용을 적어본다.


의존의 양면성


다음 코드를 보자.


public class Authenticator {

    public boolean authenticate(String id, String password) {

        Member m = findMemberById(id);

        if (m == null) return false;


        return m.equalPassword(password); // password가 m의 암호와 동일하면 true

    }

    ...

}


이 클래스를 사용하는 코드는 다음과 같이 checkPassword() 메서드를 이용해서 사용자가 입력한 암호가 올바른지 여부를 판단할 것이다.


public class AuthenticationHandler {


    public void handleRequest(String inputId, String inputPassword) {

        Authenticator auth = new Authenticator();

        if (auth.authenticate(inputId, inputPassword)) {

            // 아이디/암호 일치할 때의 처리

        } else {

            // 아이디/암호 일치하지 않을 때의 처리

        }

    }


}


위 코드에서 AuthentcationHandler 클래스는 Authenticator 클래스를 사용하고 있다. 즉, AuthenticationHandler 클래스가 Authenticator 클래스에 의존하고 있고, Authenticator 클래스에 변화가 생기면 AuthenticationHandler 클래스도 영향을 받게 된다.


그런데, 잘못된 아이디를 입력한 것인지 아니면 암호가 틀린 것인지 여부를 확인해서 시스템상에 로그로 남겨달라는 요구가 추가되었다. 이 요구를 충족시키려면 Authenticator의 authenticate() 메서드는 단순히 boolean 값을 리턴하면 안 된다. 아이디가 잘못되었는지 암호가 잘못되었는지 여부를 알려줄 수 있어야 한다. 예를 들면 AuthenticationHandler 클래스의 코드는 다음과 같이 익셉션을 통해서 인증 실패 이유를 구분할 수 있어야 한다.


public class AuthenticationHandler {

    public void handleRequest(String inputId, String inputPassword) {
        Authenticator auth = new Authenticator();
        try {
            auth.authenticate(inputId, inputPassword);
            // 아이디/암호가 일치하는 경우의 처리
        } catch(MemberNotFoundException ex) {
            // 아이디가 잘못된 경우의 처리
        } catch(InvalidPasswordException ex) {
            // 암호가 잘못된 경우의 처리
        }
    }

}


AuthenticationHandler 클래스를 위와 같이 작성하려면 Authenticator 클래스의 authenticate() 메서드가 다음과 같이 변경되어야만 한다.


public class Authenticator {

    public void authenticate(String id, String password) {

        Member m = findMemberById(id);

        if (m == null) throw new MemberNotFoundException();


        if (! m.equalPassword(password)) throw new InvalidPasswordException();

    }

    ...

}


AuthenticationHandler 클래스가 Authenticator 클래스에 의존하고 있는 상황에서, AuthenticationHandler 클래스의 변경 요구 때문에 Authenticator 클래스에 변화가 발생한 것이다. 이는 의존이 다음과 같이 상호간에 영향을 준다는 것을 보여준다.

  • 내가 변경되면 나에게 의존하고 있는 코드에 영향을 준다.
  • 나의 요구가 변경되면 내가 의존하고 있는 타입에 영향을 준다.

GoF Mediator 패턴 요약:



참고자료:


GoF Observer 패턴 요약:



  1. 훈이 2013.04.24 14:29

    굿

GoF Facade 패턴 요약



GoF Proxy 패턴 요약



+ Recent posts