소스 코드를 기록하는 남자

전략 패턴(Strategy Pattern)

디자인 패턴

전략 패턴은 디자인 패턴의 꽃이라 할 수 있다. 따라서 스프링을 공부하고자 하는 사람은 꼭 이해하고 넘어가야 할 부분이다. 주의깊게 보도록 해보자.

 

전략 패턴을 구성하는 요소는 세 가지다.

  1. 전략 메소드를 가진 전략 객체
  2. 전략 객체를 사용하는 컨텍스트 (전략 객체의 사용자/소비자)
  3. 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트

전략 패턴의 맹점은 다음과 같다. 

 

"전략 패턴에서 클라이언트는 다양한 전략 중 하나를 선택해 생성한 후 컨텍스트에 주입한다."

 

예를 들어 군인이 있다고 가정해보자. 군인은 다양한 무기중 주어진 환경에 적합한 무기를 사용할 것이다. 또한 이를 보급해주는 보급 장교가 있다고 해보자. 그럼 여기서 무기는 전략이 되고, 군인은 컨텍스트, 보급 장교는 제 3자인 클라이언트가 된다.

 

다음은 코드를 볼 예정인데, 다양한 전략, 무기를 공통된 방법으로 사용하기 위해서 인터페이스를 정의할 것이다.

 

package 전략패턴;

public interface 전략 {
    public abstract void runStrategy();
}

public class 총 implements 전략{

    @Override
    public void runStrategy() {
        System.out.println("총 : 탕탕탕");
    }
}

public class 칼 implements 전략{
    @Override
    public void runStrategy() {
        System.out.println("칼 : 슝슝");
    }
}

public class 활 implements 전략{
    @Override
    public void runStrategy() {
        System.out.println("활 : 슉 슉 슈슝");
    }
}

public class 군인 {
    void runContext(전략 strategy) {
        System.out.println("전투 시작");
        strategy.runStrategy();
        System.out.println("전투 종료");
    }
}

public class Client {
    public static void main(String[] args) {
        전략 strategy = null;
        군인 rambo = new 군인();

        strategy = new 총();
        rambo.runContext(strategy);

        strategy = new 칼();
        rambo.runContext(strategy);

        strategy = new 활();
        rambo.runContext(strategy);

    }
}

 

위의 코드의 흐름은 클라이언트(=보급장교)가 군인인 람보에게 총, 칼, 활을 주고 전투가 시작되고 전투가 끝나는 흐름이 된다. 위에 보면 전략인 무기가 다양하게 변경되면서 컨텍스트를 실행할 수 있게 된 것이다. 전략 패턴은 디자인 패턴의 꽃이라 했던 것처럼, 다양한 문제 상황의 해결책으로 사용된다.

 

혹여라도 SOLID에 대한 부분이나 이전에 디자인 패턴이 공부한 기억이 있다면, 템플릿 메소드 패턴과 유사하다는 생각이 들 수도 있다. 같은 문제에 해결책으로 템플릿 메소드 패턴과 전략 패턴을 선택하여 사용할 수 있는데 상속이라는 제한이 있는 템플릿 메소드 패턴보다는 전략 패턴을 Java 진형에서 더 많이 사용한다고 한다.

 

마지막 한마디로 정리하자.

 

"클라이언트가 전략을 생성해 전략을 실행할 컨텍스트에 주입하는 패턴"

 

 

 

guy-who-writes-sourcecode.tistory.com/

 

소스 코드를 기록하는 남자

 

guy-who-writes-sourcecode.tistory.com

 

객체 지향 그리고 SOLID

OOP

"SOLID가 왜 등장하게 되었을까?"라는 고민을 던져보면 사실 답은 크게 어렵지 않다. 많은 개발자들은 꾸준히 무언가를 해결하고자 했고, 자신들의 철학들을 꾸준히 코드에 녹여내려 했다는 점이라는 것이다. 즉, 객체 지향이라는 철학을 코드에 녹이려고 한 산출물이 SOLID이다.

 

 

따라서 SOLID를 충분히 이해하기 위해서는 객체 지향이란 무엇인가에 대한 고민을 꾸준히 할 필요가 있다는 것이다. 객체 지향과 SOLID는 항상 가까이 두어 정기적으로 읽어 내 습관처럼 만들어야 한다.

 

관심사의 분리 (SoC, Separation Of Concerns)

SOLID를 논한다면 항상 언급되는 것은 SoC가 아닐까 싶다. 관심사의 분리라는 말은 "관심이 같다면 모으고, 관심이 다르면 분리시켜 서로에게 영향을 끼치지 않도록 하는 것"이다. 이 관심사 분리에 해당하는 것은 속성, 메서드, 클래스, 모듈, 패키지 전부 포함된다. 이렇게 분리하는 이유가 무엇일까? 관심사가 다르다면 분명히 변화되는 시점이 다르기 때문이다.

 

 

이 전 포스팅에서도 언급했듯이, 분리가 많아지면 파일이 많아지는 것은 필연적이다. 하지만 객체 지향과 SOLID를 잘 녹여낸 코드를 만든다면 파일이 많아짐에도 불구하고 충분히 가치있을 것이다.

 

 

SoC를 잘 적용한다면 자연스럽게 인터페이스 분리 원칙(ISP), 단일 책임 원칙(SRP), 개방 폐쇄 원칙(OCP)에 도달하게 될 것이다. 스프링 또한 극한의 SoC를 추구한다.

 

SOLID의 각 원칙을 설명할 때는 한글화된 이름을 쓰기도 하지만, 대부분의 개발자들은 약어를 쓰더라. 필자도 면접에서 질문을 받았었던 기억이 있는데 SoC를 아는가 라고 물어봤다. 분명히 관심사의 분리라는 것은 기억하고 있었지만 SoC라는 약어에 대해서 익숙하지 않아서 "잘 모르겠습니다"라고 대답했다.

 

 

따라서 약어에 대해 익숙해지길 바란다.

  • SRP(단일 책임 원칙) : 하나의 클래스를 변경해야 하는 이유는 오직 하나 뿐이다.

  • OCP(개방 폐쇄 원칙) : 자신의 확장에는 열려있고, 주변의 변화에 대해서는 닫혀 있어야 한다.

  • LSP(리스코프 치환 원칙) : 서브 타입은 언제나 자신의 기반 타입으로 교체 가능해야 한다.

  • ISP(인터페이스 분리 원칙) : 클라언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다.

  • DIP(의존 역전 법칙) : 자신보다 변하기 쉬운 것에 의존하지 마라.

 

 

객체 지향은 현실 세계를 반영한다고 했다. 즉 현실 세계에 대한 모델링이 객체 지향이라는 것이다. 더 생각해본다면, 객체 지향 세계는 모델링을 통해 추상화되었고, 현실 세계랑 같다. SOLID의 포커싱은 좀 더 모델링을 통한 추상화에 초점을 맞추고 있다.

SOLID - DIP ( 의존 관계 역전의 법칙 ) 에 대하여

OOP

https://zdnet.co.kr/view/?no=00000039137043

 

[객체지향 SW 설계의 원칙] ④ 리스코프 치환 원칙

과거 헐리우드에서는 배우들이 좋은 영화의 배역을 구하기 위해 영화제작사에 자주 전화를 걸었다고 한다. 배우들의 잦은 전화 때문에 영화기획사 담당자들이 자신의 업무를 할 수 ...

zdnet.co.kr

 

 

DIP (Dependency Inversion Principle) 은 의존 관계 역전 원칙이라 명명되었다. 개인적인 생각이지만, DIP 는 개발의 패러다임이 바꿨다 할 수도 있을 것 같다. 그만큼 중요한 내용이고, 이해하려고 노력하였으나 개인적으로 DIP를 이해하는 것이 쉽지 않았다고 생각한다. 물론 지금도 완벽한 이해를 하고 있는것은 아니라 생각한다. 시작 해보자!

의존 관계 역전의 법칙

포스팅을 공부하면서 개발자로서 꼭 읽어봐야 할 서적이 한 개 더 추가됐다. 읽을 책이 늘어난다는 것은 나름대로 기분 좋고 행복한 일이다. GoF (Gang of Fours). Design Patterns 책에서 "템플릿 메소드" 패턴을 소개하면서 헐리우드 원칙을 이야기한다

헐리우드 원칙이란 캐스팅 프로세스에서 배우가 직접 영화기획사에 전화하는 방식으로 진행되었던 부분을 배우는 자신이 자신있는 역할이나 어떤 영화 배역을 맡고 싶다고 등록하면, 영화기획사가 선별하여 전화하는 프로세스로 변경한 것

헐리우드 원칙을 적용하여 얻고자 했던 효과는 무엇일까? 살펴보면 헐리우드 원칙 이전의 캐스팅 프로세스는 영화 기획사는 누가 봐도 수동적이며, 배우는 능동적이다. 하지만 이 과정에서는 영화 기획사는 많은 배우들의 전화에 시달려야 하며, 배우는 직접 찾아서 전화해야 되는 노동이 숨겨져 있다.

 

 

헐리우드 원칙을 적용함으로써? 그 고된 노동을 큰 폭으로 줄일 수 있다는 것이다. 배우는 단순히 자신의 정보를 등록하면 되며 기획사는 필요한 배우에게 연락하면 된다. 확실히 프로세스가 간편해 지지 않는가? 따라서 기존의 능동적인 배우, 수동적인 기획사는 헐리우드 원칙을 적용함으로써 "수동적인" 배우, "능동적인" 기획사로 역전 된다.

 

그래서 의존 관계 역전의 법칙이라 하며, DIP에도 이 헐리우드 원칙의 구조와 목적을 그대로 도입한다고 생각하면 된다.

통제권의 역전

이전의 구조 지향 프로그래밍과 객체 지향 프로그래밍의 프레임워크 사용 방법을 비교해보겠다. 구조 지향적 프로그램은 main() 함수에서 시작해서 여러 함수들을 호출하는 것으로 프로그래밍 매우 절차적이다.

 

 

반면 프레임워크를 사용하는 방식은 프레임워크에 객체를 등록하므로 실행의 통제권을 프레임워크에게 위임한다. 단편적으로 HTTP 서버에 서블릿을 등록하고 HTTP 서버에게 서블릿 실행을 요청하는 URL이 접수되면 HTTP 서버는 등록된 서블릿을 실행한다. 직관적으로 이해가 잘 되지 않을 것이다. 나도 그랬다. 그러니 아까 이야기했던 할리우드 원칙을 적용해 비교해보겠다.

 

 

https://image.zdnet.co.kr/images/stories/etc/2005/06/0608/39137043_01.gif

[그림 1]

 

 

https://image.zdnet.co.kr/images/stories/etc/2005/06/0608/39137043_02.gif

[그림 2]

 

 

[그림 1]을 보자. Actor라는 존재가 직접 CastingMgr에게 전화를 하고 CastingMgr은 Director에 전달한 다음 Actor가 다시 확인을 하는 Process며 이 과정에서 통제권이 Actor에게 있다. 통제의 흐름은 호출자(Caller, =Actor)에서 서비스 함수(Callee, =CastingMgr)에게 이전되며, 서비스 함수 루틴이 종료되면 다시 호출자에게로 통제가 반환된다. 따라서 CastingMgr은 Actor의 요청에 대해 수동적으로 서비스한다.

 

 

[그림 2]를 보자. Actor는 CastingMgr에게 자신을 regist 한다. 그리고 이전에는 단순히 호출 받기만 했던 Director는 Actor에게 직접 confirm()을 실행한다. 두 관계가 역전이 된 것이다. 이것을 '통제권의 역전(Inversion of Control)'이라 한다. 추후 스프링 프레임워크를 공부할 일이 있다면, 근간이 되는 개념이라 볼 수 있으니 기억해두면 좋을 것이다.

 

 

DIP가 포스팅 주제인데, "왜 IoC를 이렇게 길게 다루지" 에 대한 의문이 생겼을 수도 있지만, IoC는 DIP의 중요한 골격이 된다. 이제 좀 더 집중해야 될 시간이다.

 

 

서비스 요청자(Actor)는 서비스 제공자(프레임워크)에게 자신을 등록하고 서비스 제공자는 서비스를 마친 후 서비스 요청자에게 미리 정의해 둔 인터페이스를 통해 결과를 알려준다.

 

이 문장을 쉽게 풀어보겠다.

Actor는 서비스 제공자인 CastingMgr에 자신을 등록하고 CastingMgr은 서비스를 마친 후 Actor에게 미리 정의해둔 인터페이스 confirm()을 통해 결과를 알려준다.

 

중요한 포인트다. 미리 정의해둔 인터페이스는 훅(Hook) 메소드라 부르며 훅 메소드는 '역전'을 위한 매개 포인트가 된다. 추후 포스팅에서도 계속 등장하기 때문에 꼭 기억해두길 바란다.

훅 메소드? 확장성을 확보하는 기능

'미리 정의해둔 인터페이스'로 다양한 루틴을 정의할 수 있다. 가령 서블릿 개발을 해 본 사람이라면 doGet() 이나 doPost() 와 같은 인터페이스가 있다는 것을 알 것이다. 이러한 인터페이스는 개발자로부터 무한한 확장을 제공한다. 단지 서블릿 컨테이너는 서블릿 호출이 왔을 때 해당하는 서블릿의 doGet()이나 doPost()을 실행하면 된다.

 

 

자 그럼 doGet(), doPost() 메소드의 역할을 다시 한번 짚어보자.

doGet(), doPost() 메소드는 개발자에게 있어 확장성을 제공한다.

doGet(), doPost() 메소드는 서블릿 컨테이너에게 있어 훅 메소드의 역할을 한다.

자 그럼 IoC를 골격으로 하는 DIP로 얻을 수 있는 것은 무엇일까?

DIP

허허.. DIP를 이야기하는 줄 알고 들어봤더니 서두가 겁나 길다. 할 수 있을 것이다. 나도 짧고 명확하게 이해가 잘 되는 글을 좋아한다. 하지만 이 DIP는 길어질 수 밖에 없다.

https://image.zdnet.co.kr/images/stories/etc/2005/06/0608/39137043_03.gif

[그림 3]

DIP에서도 훅 메소드를 통해 확장성을 제공한다. 이미 정의된 인터페이스를 통해서 확장을 보장하고, 이 인터페이스는 사용자로부터 사용자가 정의한 컴포넌트를 은닉시켜 사용자 정의 컴포넌트에 대한 의존성을 제거하기 위함이다. 즉 확장성을 보장하기 위해 추상화가 이용된다.

어렵다. 계속 가보겠다.

 

OCP와 DIP가 다른 점은 DIP는 IoC를 한다는 것이다. 더 어렵다. 이해가 안된다.

천천히 다시 이해해보자.

 

 

[그림 1]에서 [그림 3]과 같이 confirm()이란 인터페이스를 여러 Actor의 자식들이 확장할 수 있다. 만약 confirm()을 확장한 Actor들이 프레임워크에 등록 됐을 때 confirm()은 훅 메소드가 된다. 따라서 DIP는 확장되는 훅 메소드를 정의하기 위해 OCP를 이용하고 있다. 설계의 원칙은 이렇게 서로 관계성을 가지고 있으며 서로가 서로를 포함하기도 하고 이용하기도 한다.

DIP 케이스

사례는 꼼꼼히 읽어봤으면 좋겠다는 생각으로 스크랩을 한다.

 

사례 1 : 통신 프로그래밍 모델

일반적으로 소켓 프로그램은 클라이언트가 서버에게 요청을 send()하고 서버로부터 결과를 recv()하므로 서버의 서비스를 이용하게 된다. 멀티쓰레드 프로그래밍에서 이 send() & recv()를 하게 되면 recv()하는 동안 쓰레드는 서버의 응답이 오기까지 대기하게 된다. recv() 함수는 블럭되기 때문이다. 따라서 이 때 recv()하는 모든 쓰레드들은 블럭되기 때문에 쓰레드 자원이 아까워진다. 왜냐하면 서버로부터의 응답을 받기 위해 대기하는 동안 recv()를 호출한 쓰레드는 다른 작업을 할 수 없기 때문이다.

 

 

이 방식의 대안으로 제시되는 모델이 폴링(polling) 모델이다. 클라이언트 쓰레드는 서버에게 메시지를 보내고 recv()를 전담하는 쓰레드에게 recv()를 맡긴다. 그리고 이 쓰레드들은 다른 작업을 실행하면서 계속 일을 한다. 서버로부터 응답을 확인하고 싶은 시점에서 접수된 서버의 메시지를 가져온다. 따라서 클라이언트 쓰레드는 다른 일을 할 수 있는 기회비용을 얻을 수 있다.

 

 

https://image.zdnet.co.kr/images/stories/etc/2005/06/0608/39137043_06.gif

하지만 폴링 모델에서 어느 순간 클라이언트 쓰레드는 서버의 응답을 확인해야 한다. 단지 자신이 원하는 시점에 서버의 응답을 확인하는 장점과 응답을 기다리는 시간에 다른 작업을 할 수 있는 기회를 확보할 뿐이다. 이 모델까지는 확실히 모든 통제가 클라이언트 쓰레드의 스케쥴 안에 있다. 그리고 동기적으로 (자신이 원하는 시점에) 서버의 응답을 확인할 수 있다. 하지만 만약 서버의 응답이 예상보다 지연될 경우 클라이언트 쓰레드는 서버의 응답이 올 때까지 여러 번 응답 큐를 확인하는 비용이 따른다. 또한 서버의 응답을 확인하는 시점이 동기적이지 않아도 될 경우 더더욱 이 확인 작업은 지난해지게 된다. 즉, 서버의 응답에 대한 처리가 비동기적이어도 될 때, 그리고 클라이언트 쓰레드가 서버의 응답 확인하는 시도가 여러 번 발생할 때 폴링 모델도 오버헤드를 얻게 된다.

 

 

이 때 DIP를 적용하기 적당한 시점이 되는데 클라이언트 쓰레드는 메시지를 send()한 후에 recv()하는 대신 서버의 응답을 처리하는 훅 메쏘드를 Reply DeMuxer에 등록한다. - 구조적 프로그램에서는 함수 포인터를 등록하지만 객체지향 세계에서의 트렌드는 커멘드 오브젝트를 등록한다(GoF의 커멘드 패턴 참조). Reply DeMuxer의 recv()를 담당하는 쓰레드는 서버로부터 응답을 접수하면 대응하는 훅 메쏘드를 찾아 훅 메쏘드를 실행한다. 즉 recv() 쓰레드는 서버의 응답 접수(여기까진 폴링 모델)와 훅 메쏘드 실행을 담당한다.

https://image.zdnet.co.kr/images/stories/etc/2005/06/0608/39137043_07.gif

이 모델은 비동기 소켓 모델로서 DIP의 원칙을 그대로 따르고 있다. - 클라이언트 쓰레드들은 헐리우드 원칙에서의 배우로 receive 쓰레드는 영화기획사 담당자로 등치해 보라. 비동기 모델에서 얻을 수 있는 장점은 첫째, 클라이언트 쓰레드의 잦은 응답 확인을 제거할 수 있다. 둘째, 클라이언트 쓰레드는 응답을 확인하는 작업에서 자유로워지므로 다른 작업을 할 수 있는 기회비용을 확보할 수 있다. 물론 이 과정은 비동기적으로 이루어져도 괜찮은 상황에 한한다.

 

 

무엇보다 중요한 것은 이런 구조의 바탕에는 통제권이 클라이언트 쓰레드에서 Reply DeMuxer로 역전되는 IOC가 전제된다. DIP를 적용할 때 기대할 수 있는 장점은 상술한 두 가지 장점을 그대로 확보하는데 있다. 퍼포먼스를 높이고 요청에 대한 응답으로부터 관심을 제거하여 클라이언트의 역할을 단순화하는데 있다.

 

 

사례 2 : 이벤트 드리븐, 콜백 그리고 JMS 모델

자바 API는 소프트웨어 설계의 좋은 모델이 된다. 반면에 개발자로서 하고 싶은 마법들을 API 수준에서 제공해주니 마법을 부릴 기회가 줄어들어 약간 억울하기까지 하다. 자바 스윙에서 이벤트 모델에도 마법이 녹아 있다. 자바 스윙 컴포넌트는 이벤트를 처리할 java.awt.event.ActionListener를 등록(addActionListener())한다. 이 스윙 컴포넌트에 이벤트가 발생하면 등록된 ActionListener의 훅 메쏘드인 actionPerformed()를 후킹한다. 스윙 컴포넌트에는 복수 개의 ActionListener를 등록할 수 있는데 이유는 복수 개의 이벤트가 발생할 수 있기 때문이다. 이와 유사한 구조로 더 일반화된 Observer & Observable 인터페이스도 있다.

 

 

더 나아가서 분산 시스템에서도 똑같은 구조가 적용된다. 서버와 클라이언트간의 통신에 있어서 클라이언트는 서버에 자신의 원격 객체 레퍼런스를 등록한다. 서버는 자신의 작업을 진행하면서 원격 객체 레퍼런스를 통해 그때그때 필요한 정보를 클라이언트에게 제공한다. 이 구조를 위해서 클라이언트의 콜백(callback) 메쏘드가 미리 정의되어 있어야 한다. 콜백 메쏘드는 서버가 비동기적으로 클라이언트에게 정보를 전달하는 훅 메쏘드가 된다. 따라서 콜백의 구조는 원격지에서 훅킹이 제공되는 형태를 갖는다.

https://image.zdnet.co.kr/images/stories/etc/2005/06/0608/39137043_08.gif

이와 같은 구조는 비동기적인 분산 훅킹(콜백)구조를 형성할 때 사용된다. 가령 서버에게 장시간의 작업들을 할당하고 클라이언트가 각 작업의 결과에 대한 중간보고를 비동기적으로 받고 싶을 때 유용하다. 클라이언트의 호출이 비동기적이기 때문에 서버의 작업을 할당한 다음 클라이언트는 다시 자신의 작업이 진행된다. 따라서 앞서 예시한 소켓의 비동기 모델에서 recv() 쓰레드가 서버의 역할로 전이된 형태를 갖는다.

 

 

JMS의 토픽 모델은 좀 더 다양한 구조를 갖는다. - 이 모델은 전통적인 MOM 아키텍처에서 Publish/Subscribe 메시징 모델로 알려져 있다. 이 모델은 멀티캐스팅 같은 그룹 메시징을 제공할 때 유용한데, 가령 주식정보 시스템을 예로 들었을 때 주식정보 제공자는 가입한 모든 클라이언트에게 현재 증시정보를 멀티캐스팅한다. 이 때 주식정보 제공자는 Publisher가 되고 클라이언트 프로그램은 Subscriber가 된다.

참고로 이 모델의 장점은 클라이언트/서버에서 메시지 기반으로 패러다임이 바뀐다는 것이다. 기존의 클라이언트/서버 모델의 경우 서버는 클라이언트들을 상대한다. 따라서 클라이언트의 위치 정보와 인터페이스 등을 알아야 한다. Publish/Subscribe 모델에서는 이 클라이언트와 서버 간의 상호의존도가 제거된다.

 

 

이제부터 서버는 각종 클라이언트들에게 메시지를 보내는 것이 아니라 그냥 ‘주식정보’라는 메시지를 보내면 될 뿐이다. 즉, 어떤 클라이언트들이 얼마나 접속되어있는지, 각 클라이언트들의 위치와 인터페이스는 어떤지 등의 여부와 같은 클라이언트 정보는 관심 대상에서 제외되고(주식정보라는) 메시지에 관심을 집중하게 된다. 이 패러다임은 클라이언트가 몇 개 접속되어 있는지 혹은 아예 없든지, 클라이언트의 상태나 위치가 어떤지에 관심 없이 그룹 메시징 제공자에게 메시지를 보내기만 하면 될 뿐이다.

https://image.zdnet.co.kr/images/stories/etc/2005/06/0608/39137043_09.gif

이 모델에서 Subscriber들은 Topic 제공자에게 자신을 등록한다. Publisher가 Topic 제공자에게 메시지를 전송하면 JMS Topic 제공자는 등록된 Subscriber들에게 메시지를 멀티캐스팅한다. 이 때 메시지 멀티캐스팅을 하기 위해 등록된 각 Subscriber들의 onMessage()를 호출하게 된다. 그럼 상술한 훅 메쏘드들, 즉 ActionListener.actionPerformed(), MessageListener.onMessage(), 그리고 콜백 메쏘드는 어떤 의미를 가질까? 훅 메쏘드는 IOC이면서 확장 인터페이스를 제공한다. 사용자 정의 컴포넌트들이 자신의 목적에 맞게 이 메쏘드를 확장하여 사용할 수 있게 하기 위함이다.

 

 

정리

DIP의 키워드는 ‘IOC’, ‘훅 메쏘드’, ‘확장성’이다. 이 세 가지 요소가 조합되어 복잡한 컴포넌트들의 관계를 단순화하고 컴포넌트 간의 커뮤니케이션을 효율적이게 한다. 이 목적을 위해 Callee 컴포넌트(예를 들어 프레임워크)는 Caller 컴포넌트들이 등록할 수 있는 인터페이스를 제공해야 한다. 따라서 자연스럽게 Callee는 Caller들의 컨테이너 역할이 된다(JMS의 Topic 제공자, 스윙 컴포넌트, 배우 섭외 담당자들은 등록자들을 관리한다). Callee 컴포넌트는 Caller 컴포넌트가 확장(구현)할, 그리고 IOC를 위한 훅 메쏘드 인터페이스를 정의해야 한다. Caller 컴포넌트는 정의된 훅 메쏘드를 구현한다.

 

 

https://image.zdnet.co.kr/images/stories/etc/2005/06/0608/39137043_10.gif

이로써 DIP를 위한 준비가 완료됐다. 이 상태에서 다음과 같은 시나리오가 전개된다. Caller는 Callee에게 자신을 등록한다. Callee는 Caller에게 정보를 제공할 적당한 시점에 Caller의 훅 메쏘드를 호출한다. 바로 이 시점은 Caller와 Callee의 호출관계가 역전되는 IOC 시점이 된다. DIP를 이용해서 얻을 수 있는 장점은 무엇일까? 이 질문은 DIP를 사용할 수 있는 상황과도 밀접하게 연관되어 있다.

DIP는 다음과 같은 상황에서 사용된다. 비동기적으로 커뮤니케이션이 이루어져도 될 (혹은, 이뤄져야 할) 경우 컴포넌트 간의 커뮤니케이션이 복잡할 경우 컴포넌트 간의 커뮤니케이션이 비효율적일 경우(빈번하게 확인해야 하는)에 사용된다. DIP는 복잡하고 지난한 컴포넌트간의 커뮤니케이션 관계를 단순화하기 위한 원칙이다. 실세계에서도 헐리우드 원칙에서와 같이 귀찮도록 자주 질문과 요청하는 동료에게도 써먹어 볼만한 원칙이다.

SOLID - ISP ( 인터페이스 분리 원칙 ) 에 대하여

OOP

[객체지향 SW 설계의 원칙] ③ 인터페이스 분리의 원칙

 

[객체지향 SW 설계의 원칙] ③ 인터페이스 분리의 원칙

“사람은 다른 사람과 말을 할 때 듣는 사람의 경험에 맞추어 말해야만 한다. 예를 들어 목수와 이야기할 때는 목수가 사용하는 언어를 사용해야 한다.” - 플라톤의 파에톤(P...

zdnet.co.kr

좋은 내용이 있어 읽어보고 정리합니다. 단순한 개인 공부입니다. 잘못된 부분이 있으면 지적해주세요

 

인터페이스 분리 원칙?

ISP의 핵심은 '변화'가 핵심이 된다. 어떻게 변화할 것인지에 대해서 천천히 알아보자

다음과 같은 구조의 프로그램이 있다. 이 프로그램에서 제공하는 기능은 불법 결제 패턴에 해당하는 사용자 결제를 차단하고, 이를 담당자에게 메일로 리포팅하는 동작을 하게 된다.

 

하지만 여기서 추가적으로 SMS 리포팅 기능을 구현해야 하면 어떻게 될까?

 

아마도 CPRule에 메소드를 추가할 것인다. 그럼?

 

전혀 상관없는 클래스에게도 영향을 미치게 될 것이다. 이는 클래스의 재컴파일, 재배포 등과 같은 문제점을 야기한다.

 

이런 부분을 ISP를 통해서 해소할 수 있다. ISP 를 간단하게 정의하면 "클라이언트는 자기가 사용하지 않는 메소드에 의존 관계를 맺으면 안된다" 라는 것이다.

 

아직 한참 모자란 본인은 많은 부분에서 사용하지 않는 메소드가 있음에도 불구하고 의존 관계를 만들어서 사용했던 기억이 있다. 자 그럼 인터페이스를 사용해서 구조를 바꾸면 어떻게 될까?

 

CPRule 이라는 클래스의 응집도는 높히대, 자신이 사용하지 않는 메소드를 분리해서 사용할 수 있게 된다.

 

이러한 부분은 BlockTransaction, EmailReporting 클래스가 직접 CPRule 에 접근하지 않아도 CPRule이 제공하는 서비스를 이용할 수 있게 된 것이다.

 

전형적인 ISP 를 보자

 

A, B, C 세 개의 클라이언트가 Service 인터페이스에 의존 관계를 맺고 있다.

만약에 Service 인터페이스의 어느 하나가 바귄다면, 세 클라이언트 모두를 재 컴파일, 재배포하는 아주 귀찮은 상황이 발생한다. 하지만 이를 ? 아래와 같이 변경한다면

 

기존에 발생했던 변화의 확산이 일어나지 않는다.

 

로버트 C.마틴은 "클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다." 라고 했다.

이 뿐만 아니라 ISP를 이야기할 때 항상 인터페이스를 통한 메서드를 외부에 제공할 때는 최소한의 메서드만 제공하라는 것이다. 명심할 필요가 있다.

SOLID - SRP ( 단일 책임 원칙 ) 에 대하여

OOP

단일 책임 원칙 (SRP)

SRP (Single Responsibility Principle ) 이란..?

직역하면 단일 책임 원칙이다. 사실 직관적인 단어라 어려운 부분이 없다고 생각했다. 하지만 예시를 확인해 봤을때 이게 뭔 말인가? 했다. 그래서 가지고 있던 책 중에 클린 코드를 확인 해보았다.

단일 책임 원칙은 클래스나 모듈을 변경할 이유가 단 하나뿐이어야 한다.

 

아! 나는 기존에 정의된 하나의 클래스에 하나의 책임만을 가져야 된다는 것보다 이게 더 직관적으로 와닿았다.

Dashboard라는 클래스를 예시로 설명해보겠다.

 

public class Dashboard extends JFrame implements MetaData
{
    public Componet getLastFocusComp()
    public void setLastFocus(Component lastFocus)
    public int getMajorVersionNum()
    public int getMinorVersionNum()
    public int getBuildNum()
}

여기서 Dashboard의 역할은 두 개로 가정한다. 소프트웨어 버전 정보를 추적, Dashboard는 스윙 컴포넌트를 관리. 하지만 소프트웨어는 출시될때마다 버전 정보가 변경되며, 스윙 코드를 변경할 때마다 버전 번호가 달라진다. 자, 그럼 여기서 코드를 변경해야 하는 이유가 두 가지나 된다는 것이다.

 

따라서 이 코드에서 냄새를 제거해보자. 변경되는 이유를 찾았으니, 버전 정보를 다루는 메소드를 추출해서 새로운 클래스를 생성해보자. 여기서 버전 정보를 관리하는 메소드는 아래와 같다. 이를 Version 클래스로 만들어보자.

 

public class Version {
    public int getMajorVersionNum()
    public int getMinorVersionNum()
    public int getBuildNum()
}

이렇게 만들어진 Version 클래스는 다른 애플리케이션에서도 충분히 쉽게 사용될 것이다.

 

많은 주니어 개발자들은 깨끗하고 체계적인 소프트웨어보다 돌아가는 소프트웨어에 초점을 맞추는데, 사실 이건 비교하면 잘 정리된 여러 개의 수납장에 물건을 정리해서 사용할 것이냐? 큰 서랍장 하나에 모든 물건을 던져놓고 쓸 것이냐? 이다. 느껴지지 않는가? 냄새나는 코드의 제거는 중요하다.

마음의 정화를 가져오는 SRP, PEACE

SOLID - OCP ( 개방-폐쇄 원칙 ) 에 대하여

OOP

객체 지향에 대해서 본격적인 공부를 시작하면서 보게 된 포스팅이 매우 인상 깊어 글을 작성하게 되었다.

아래는 원본글이니, 객체 지향을 공부하기 시작한 사람이라면 꼭 볼만한 포스팅이라 생각한다.

 

[객체지향 SW 설계의 원칙] ① 개방-폐쇄 원칙

 

[객체지향 SW 설계의 원칙] ① 개방-폐쇄 원칙

소프트웨어 설계의 묘미는 개발자가 작업하는 시스템의 창조자 역할을 할 수 있다는 것이다. 실세계의 경우 좋은 세상을 만들기 위해 적절한 질서, 정책, 의식 등이 전제돼야 하...

zdnet.co.kr

개방-폐쇄 원칙

버틀란트 메이어는 개방-폐쇄 원칙을 다음과 같이 정의한다.

 

소프트웨어 구성 요소 ( 컴포넌트, 클래스, 모듈, 함수 ) 는 확장에 대해서는 개방되어야 하며, 변경에 대해서는 폐쇄되어야 한다. 이는 다시 말해 변경을 위한 비용은 가능한 줄이고 확장을 위한 비용은 가능한 극대화해야 한다는 의미다.

 

이를 달성하기 위해서는 변경되는 부분, 변경되지 않는 부분을 엄격하게 구분해야 한다. 구분을 통해서 변하는 것은 가능한 변하게 쉽게하고, 변하지 않는 것은 변하는 것에 영향을 받지 않도록 하는 것이다. 그리고 인터페이스를 변하는 것과 변하지 않는 것의 중간 지점에서 서로를 보호하는 중재자 역할을 해야된다.

 

따라서, 이를 통해서 얻을 수 있는 것은 객체 간의 관계를 단순화해 복잡도를 줄이고, 확장이나 변경에 의해 발생하는 충격을 줄일수 있다.

 

혹시 복잡한가?

 

그럼 좀 더 쉽게 로버트 C.마틴의 말을 인용해보겠다. "소프트웨어 엔티티는 확장에 대해서는 열려 있어야하고 , 변경에 대해서는 닫혀있어야 된다." 라는 말을 쉽게 풀어보면 "나는 확장될 수 있어야하며, 다른 사람이 변하더라도 나에게 영향을 끼치면 안된다". 좀 더 이해가 됐으면 좋겠다.

 

자, 위 포스팅에서는 중재자의 역할, OCP의 인터페이스 기능으로 하는 예로 24핀 표준 규격의 충전기를 든다. 공통된 규격의 충전기는 사용자가 충전기를 사용함에 있어서 신뢰할 수 있고, 충전기 제공자는 목적에 맞게 확장, 특화하여 차별화나 상품성을 높일 수 있다. 물론 지금은 USB-C type이 표준이라 볼 수 있겠다.

 

하나의 예를 더 들어보자.

 

스프링 입문을 위한 자바 객체 지향의 원리와 이해에 들어있는 예제를 보겠다..

 

위 [그림 1]을 보게 되면 운전자는 기존에 마티즈를 타다가 새로운 자동차 소나타를 구매했다. 차량 종류를 변경하자 기본에 마티즈에서 수동으로 하던 일을 자동으로 변경되었다. 단순히 스틱 차량에서 오토 차량으로 변경되었다고 해서 기존의 운전자의 역할이 변화되어야 되는가? 현실 세계에서 봤을때는 어느 정도 Yes 라는 대답은 하겠지만, 객체 지향에서는 아니다. OCP에 위반되기 때문이다. 그럼 어떻게 해야하는가?

 

 

다소 위 예제가 억지스럽다고 느낄 수 있지만, 그려려니 해주셨으면 한다.

[그림 2]와 같이 기존의 공통된 특성을 추출하여 클래스나, 인터페이스를 생성하면 운전자 클래스는 다양한 자동차가 생긴다 하더라도 운전자 클래스에는 영향을 끼치게 않게 된다. 또한 자동차 입장에서는 확장에는 개방되어 있다.

 

개방 폐쇄 원칙에 대해서 이 글을 보며 조금이라도 이해했다면? 대표적인 개방 폐쇄 원칙을 지킨 가장 큰 예제는 JDBC이다.

JDBC를 사용하는 클라이언트는 데이터베이스가 오라클, MySQL, MariaDB 어떤 데이터베이스를 사용하더라도 Connection을 설정하는 부분을 제외하고는 모두 동일하게 사용 가능하다.

 

자신의 코드는 수정할 필요가 없이 변화에 대해서는 닫혀있으며, 다양한 데이터베이스 연결에 대해서는 확장에 열려있다고

볼 수 있다. 이러한 개방 폐쇄 원칙을 잘 지킴으로써 완충 역할을 하고 있는 것이다.

OCP 주의점

여기서 3가지 주의점에 대해서 언급한다. 아직 주니어 개발자라 격하게 공감하지 못하지만 명언처럼 마음에 담아두게 되었다.

  1. 공통 모듈 설계

    • 공통된 루틴이나 변수를 리팩토링하여 분리를 하게 되었을 때, 공통 모듈이 작을 경우 공통 모듈 재사용을 얻기 위해 너무 잦은 모듈을 접근해야 하고 모듈 구성도를 지저분하게 할 경향이 높다. 설계자의 좋은 자질 중 하나는 OCP에서 확장되는 것과 변경되지 않는 모듈을 분리하는 과정에서 크기 조절을 잘 할 수 있는 결단력이라는 것이다.

  2. 확장을 보장하는 Open 모듈에서 인터페이스의 변경인가, 어댑터 사용인가

    • 확장을 보장하는 open 모듈 영역에서 예측하지 못한 확장 타입을 만났을 때 인터페이스 변경하려는 안과 어댑터를 사용하려는 안 사이에서 갈등하게 된다. 위의 두 예에서처럼 변경의 충격이 적은 후자를 택하는 경우가 대부분이다. 한 번 정해진 인터페이스는 시간이 갈수록 사용하는 모듈이 많아지기 때문에 바꾸는데 엄청난 출혈을 각오해야 한다. 그 대표적인 예가 자바의 deprecated API라 한다.

      인터페이스는 가능하면 변경하면 안 되며, 이를 달성하기 위해서는 여러 경우의 수에 대한 고려와 예측이 필요하다. 과도한 예측은 불필요한 작업을 만들기에, 설계자의 적절한 예측 능력이 필요하다.

  3. 커맨드의 역할

    • 요청자와 처리자 사이의 계약을 커맨드라 지칭하고 있다. 여기서 처리자는 execute()란 인터페이스만 알면 어떠너 처리도 수행할 수 있다. 따라서 서로 의미적 관계가 없는 Command들도 execute()란 메소드로 무엇이든 확장할 수 있다. OCP 구조에서 서버가 확장할 수 있는 운신의 폭이 넓어진 반면 클라이언트는 서버가 어떤 처리를 하는지 무지해진다.

      즉, 인터페이스 설계에서 적당한 추상화 레벨을 선택하는 것이 중요하다. 추상화라는 개념에 '구체적이지 않은' 정도의 의미로 약간 느슨한 개념을 갖고 있다.

      그래디 부치가 말하는 추상화는 '추상화란 다른 모든 종류의 객체로부터 식별될 수 있는 객체의 본질적인 특징' 이라고 한다. 따라서 이 '행위'에 대한 본질적인 정의를 통해 인터페이스를 식별해야 한다.

      포스팅에서 유일하게 이해되지 않는 부분은 '행위' 라는 부분이다. 혹여라도 지나가시던 객체 지향 고수분이 있으시다면 댓글을 큰 해소점이 될 것 같다.