'Pattern' 이란 특정 컨텍스트 내에서 주어진 문제에 대한 해결책이다.


- 'Context' 란 패턴이 적용되는 상황을 뜻하고, 반복적으로 일어날 수 있는 상황이어야만 한다.

- 'Problem' 이란 이루고자 하는 목적을 뜻하며 제약조건도 포함된다.

- 'Solution' 이란 누구든지 적용해서 일련이 제약조건 내에서 목적을 달성할 수 있는 일반적인 디자인을 뜻한다.


※ 'Force' : Problem 이 목적과 제약조건으로 구성되는데, 이를 합쳐서 Force 라 한다.

Solution 이 Force 의 양면 사이에서 균형을 이룰 수 있어야만 제대로 된 패턴이 만들어진다.



# 1~11 장 까지 배웠던 패턴 복습 #

- Strategy

교환 가능한 행동을 캡슐화하고 위임을 통해서 어떤 행동을 사용할지 결정한다.

- Observer

상태가 변경되면 다른 객체들에게 연락을 돌릴 수 있게 해준다.

- Decorator

객체를 감싸서 새로운 행동을 제공한다.

- Factory Method

생성할 구상 클래스를 서브클래스에서 결정한다.

- Abstract Factory :

클라이언트에서 구상 클래스를 지정하지 않으면서도 일군의 객체를 생성할 수 있도록 해준다.

- Singleton :

오직 하나의 객체만 생성되도록 한다.

- Command :

요청을 객체로 감싼다.

- Adapter

객체를 감싸서 다른 인터페이스를 제공한다.

- Facade

일련의 클래스에 대해서 간단한 인터페이스를 제공한다.

- Template Method :

알고리즘의 개별 단계를 구현하는 방법을 서브클래스에서 결정한다.

- Iterator

컬렉션이 어떻게 구현되었는지 드러내지 않으면서 컬렉션 내에 있는 모든 객체에 대해

반복 작업을 처리할 수 있게 해준다.

- Composite :

클라이언트에서 객체 컬렉션과 개별 객체를 똑같이 다룰 수 있도록 해준다.

- State

상태를 기반으로 한 행동을 캡슐화한 다음 위임을 통해서 필요한 행동을 선택한다.

- Proxy :

객체를 감싸서 그 객체에 대한 접근을 제어한다.



* 디자인 패턴 분류하기

1) 생성 패턴(Creational pattern) :

객체 인스턴스 생성을 위한 패턴으로, 클라이언트와 그 클라이언트에서 생성해야 할

객체 인스턴스 사이의 연결을 끊어주는 패턴이다.

- Singleton, Factory Method, Abstract Factory


2) 행동 패턴(Behavioral pattern) :

클래스와 객체들이 상호작용하는 방법 및 역할을 분담하는 방법과 관련된 패턴이다.

- Observer, Command, Iterator, State, Strategy, Template Method


3) 구조 패턴(Structural pattern) :

클래스 및 객체들을 구성을 통해서 더 큰 구조로 만들 수 있게 해주는 것과 관련된 패턴이다.

- Adapter, Composite, Decorator, Facade, Proxy



* GoF (Gang of Four)

- 가장 훌륭한 패턴 카탈로그이자 디자인 패턴의 정석인

'Design Patterns : Element of Reusable Object-Oriented Software'를 

(한글판 이름은 'GoF 의 디자인 패턴 : Design Patterns'기술한 4인방 (Gang of Four) 을 가르키는 말이다.


- 에릭 감마(Erich Gamma), 리차드 헬름(Richard Helm), 랠프 존슨(Ralph Johnson),

존 블라사이즈(John Vlissides) 를 가르켜 GoF 라고 한다. 처음으로 패턴 카탈로그를 만드신 분들이다.


by kelicia 2014. 6. 5. 00:12


* Compound Pattern :

일반적으로 자주 생길 수 있는 문제를 해결하기 위한 용도로 

2개 이상의 패턴을 결합해서 사용하는 것을 뜻한다.



패턴 몇 개를 결합해서 쓴다고 해서 무조건 컴파운드 패턴이 되는 것은 아니다.

위의 정의에도 이미 언급했듯이 무엇을 위한 용도인지 확실히 이해하자.


진정한 컴파운드 패턴이라 할 수 있는 'MVC' 에 대해 알아보고,

이 MVC 를 웹 환경에 맞게 변형시킨 '모델 2 (Model 2)' 까지 살펴보겠다.


이 책을 공부하기 전까지 MVC 가 컴파운드 패턴인지도 몰랐다.

예전에 MVC 관련 포스팅을 한 적이 있지만 내가 무지했다는 기분을 떨쳐낼 수 없다ㅠ_ㅠ



1. MVC


MVC 는 잘 알다시피 Model - View - Controller 의 약자이다.


Model :

모든 데이터, 상태 및 어플리케이션 로직이 들어있다. View 와 Controller 에서 Model 의 상태를 조작하거나

가져오기 위한 인터페이스를 제공한다. 또한 Model 에서 자신의 상태 변화에 대해서 Observer 들한테 연락을

해주긴 하지만, 기본적으로 Model 은 View 및 Controller 에게 별 관심이 없다.

View :

Model 을 표현하는 방법을 제공한다. 일반적으로 화면에 표시하기 위해 필요한 상태 및 데이터는

Model 에서 직접 가져온다.

Controller :

사용자로부터 입력을 받아서 그것이 Model 에게 어떤 의미가 있는지 파악한다.



이 MVC 에는 어떤 패턴들이 결합되어 있는 형태인지 알아보자.


이미 앞 장에서 살펴본 패턴들이 눈에 띄는데 하나씩 살펴보겠다.



Model 이 Observable 에 해당하고, Model 의 상태가 바뀔 때마다 

Model 로부터 정보를 전달받고 싶은 녀석들이 Observer 에 해당한다.

이를 이용하면 Model 을 View 와 Controller 로부터 완전히 독립시킬 수 있다.


이 패턴에 대해 잘 모른다면 '2장 Observer 패턴'을 참고하자.



Controller 는 View 의 행동(Behavior)에 해당하며, 

View 가 다른 행동을 원한다면 간단하게 다른 Controller 로 교환하기만 하면 된다.


즉, Controller 가 전략을 제공하고 View 에서는 어플리케이션의 겉모습에만 신경쓰고

인터페이스의 행동에 대한 결정은 모두 Controller 에게 맡긴다.

이는 View 를 Model 로부터 분리시키는 데에 도움이 된다.


자세한 'Strategy 패턴'은 1장을 참고하자.



View 안에서는 내부적으로 Composite 패턴을 써서 윈도우, 패널, 버튼 같은 다양한 구성요소를 관리한다.

각 디스플레이 항목은 복합 객체(윈도우 등) 또는 잎(버튼)이 될 수 있다.


Controller 가 View 에게 화면을 갱신해달라고 요청할 때, 최상위 View 구성요소한테만

화면을 갱신하라고 요청하면 된다. 나머지는 Composite 패턴에 의해 자동으로 처리된다.


이해가 잘 안 된다면 'Composite 패턴'은 9장을 참고하자.



책에 예제 코드가 있긴하지만 포스팅하기엔 양이 많아 생략한다ㅡ_ㅡ

MVC 가 왜 진정한 Compound 패턴인지만 이해하기로 하자.




2. Model 2


MVC 를 브라우저/서버 모델에 맞게 변형시켜서 사용하고는 하는데,

가장 많이 쓰이는 방법이 '모델 2 (Model 2)' 이다.


어떤 식으로 돌아가는 지 살펴보자.


① HTTP 요청

보통 사용자 ID, 비밀번호와 같은 Form 데이터가 함께 서블릿으로 전달된다.

서블릿에서는 이러한 데이터를 받아 Parsing 한다.


② Servlet 이 Controller 역할을 한다.

사용자 요청을 처리하고 대부분의 경우에 모델(보통 DB)에 어떤 요청을 하게 된다.

요청을 처리한 결과는 일반적으로 자바빈(JavaBean) 형태로 포장된다.


③ Controller 는 컨트롤을 View 에게 넘긴다.

View 는 JSP 에 의해 표현된다. JSP 에서는 (④ 자바빈을 통해 얻은) 모델의 View 를

나타내는 페이지만 만들어주면 된다. 물론 다음 단계를 위해 몇 가지 제어해야 할 일이 있을 수도 있다.


⑤ View 에서 HTTP 를 통해 웹 브라우저에게 페이지를 전달한다.



- Model 2 는 단지 깔끔한 디자인에 불과한 것이 아니다 -

디자인적인 면에서 Model, View, Controller 를 분리시켜줄 뿐만이 아니라

제작 책임까지도 분리시켜줄 수 있다.

Model 2 가 등장하면서 개발자들은 Servlet 에만 전념하면 되고,

웹 제작자들은 간단한 Model 2 스타일의 JSP 만 다루면 되는 환경이 조성되었다.

그래서 웹 제작자들은 HTML 과 간단한 JavaBeans 만 건드리면 된다.



* Design Patterns 과 Model 2

- 모델 2 는 MVC 를 웹에 맞게 적응시킨 것이다. 

비록 모델 2 가 MVC 와 똑같이 생긴 것은 아니나 여전히 MVC 의 핵심 요소들을 포함하고 있다.


Observer 패턴

View 는 더 이상 Model 의 Observer 라 할 수 없다. 

Model 한테 등록해서 Model 의 상태가 바뀌었다는 연락을 받는다거나 하지 않기 때문이다. 

하지만 Controller 가 빈(Bean)을 건네주는 덕분에 Model 의 상태가 바뀐 것을 간접적으로 연락을 받는다.

View 에서는 브라우저로 HTTP 응답을 할 때만 Model 의 상태 정보가 필요하기 때문에

HTTP 응답을 하지 않는 상태에서 Model 로부터 연락을 받아봤자 할 일이 없다.


Strategy 패턴

고전적인 MVC 처럼 View 객체에 Controller 객체에 대한 레퍼런스가 들어가는 방식으로 구현되지 않는다.

하지만 여전히 (Servlet 으로 만든) Controller 가 View 의 행동을 구현하는 전략 객체로 쓰이고,

다른 행동을 원할 때 다른 Controller 로 바꿀 수 있다.


Composite 패턴

HTML 코드를 통해서 웹 브라우저에 렌더링된다는 차이점은 있지만 

결국은 여기에서 쓰이는 View 도 중첩된 그래픽 구성요소로 이루어진다.




* 객체지향의 기초 :

1) 추상화    2) 캡슐화    3) 다형성    4) 상속



* 객체지향의 원칙 :

1) 바뀌는 부분은 캡슐화 한다.

2) 상속보다는 구성을 활용한다.

3) 구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.

4) 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.

5) 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다(OCP).

6) 추상화된 것에 의존해라. 구상 클래스에 의존해서는 안 된다(의존성 뒤집기 법칙).

7) 친한 친구들하고만 이야기한다(최소 지식 원칙).

8) 먼저 연락하지 마세요. 저희가 연락 드리겠습니다(헐리우드 원칙).

9) 어떤 클래스가 바뀌게 되는 이유는 한 가지 뿐이어야만 한다(단일 역할 원칙).



* 이 장에서의 정리 :

- MVC 패턴은 Observer, Strategy, Composite 패턴으로 이루어진 Compound 패턴이다.

- Model 에서는 Observer 패턴을 이용해 옵저버들에 대한 의존성은 없애고, 옵저버들에게

  자신의 상태가 변경되었음을 알릴 수 있다.

- View 에서는 Composite 패턴을 이용해 사용자 인터페이스를 구현한다. 

  보통 패널이나 프레임, 버튼과 같은 중첩된 구성요소로 구성된다.

- Controller 는 Strategy 패턴을 이용해 View 에 대한 전략 객체가 된다.

  View 에서는 Controller 를 바꾸기만 하면 다른 행동을 활용할 수 있다.

- MVC 는 이 3 가지 패턴을 통해 서로 연결된다. 

  느슨하게 결합되기 때문에 깔끔하면서도 유연하게 구현할 수 있다.

- 새로운 Model 을 기존의 View 및 Controller 하고 연결해서 쓸 때는 Adapter 패턴을 활용하자.

- Model 2 는 MVC 를 웹 어플리케이션에 맞게 적용한 디자인이라 할 수 있다.



(이미지 출처 :

http://www.07net01.com/linux/Head_First_Design_Patterns_Notes_286645_1367843500.html

http://stackoverflow.com/questions/5217611/the-mvc-pattern-and-swing)


by kelicia 2014. 6. 3. 17:27


이번에는 프록시 패턴의 활용 예제를 살펴보겠다.

앞서 포스팅한 내용 중에 가상 프록시(Virtual Proxy), 보호 프록시(Protection Proxy) 에 대해 

간단히 언급했는데 하나씩 자세히 살펴보겠다.


그 전에 Proxy 패턴의 클래스 다이어그램은 중요하니 다시 복습하자.


앞에서 포스팅했던 다이어그램 그림과는 조금 차이가 있지만 결국 같은 말이다.



1. 가상 프록시 (Virtual Proxy)

- 생성하기 힘든 자원에 대한 접근을 제어할 수 있다.


여기서 생성하기 힘든 자원이란 예를 들어 Image 같이 객체 생성 시간이 긴 녀석들을 의미한다.

예제 코드로는 이미지 뷰어를 만들어 볼 것이다. 


가상 프록시가 백그라운드에서 서버로부터 이미지를 불러들이는 동안 

화면에서는 로딩중이라는 메시지를 보여주게 할 것이다.


설계는 다음과 같다.


Icon 인터페이스와 ImageIcon 클래스는 javax.swing 을 이용할 것이다.


그러면 ImageProxy 코드를 보자. 앞서 먼저 본 원격 프록시(Remote Proxy)와 헷갈리지 말자.

class ImageProxy implements Icon
{
    ImageIcon imageIcon;    // RealSubject
    URL imageURL;
    Thread retrievalThread;
    boolean retrieving = false;

    public ImageProxy(URL url) { imageURL = url; }

    public int getIconWidth() {
        if ( imageIcon != null ) { return imageIcon.getIconWidth(); }
            else { return 800; }
    }

    public int getIconHeight() { // getIconWidth() 와 비슷 }

    public void paintIcon(final Component c, Graphics g, int x, int y) {
        if ( imageIcon != null ) {
            imageIcon.paintIcon(c, g, x, y);
        } else {
            g.drawString("Loading Image, please wait...", x+300, y+200);
            if ( !retrieving ) {
                retrieving = true;
                retrievalThread = new Thread(new Runnable() {
                    public void run() {
                        try {
                            imageIcon = new ImageIcon(imageURL, "Image");
                            c.repaint();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
                retrievalThread.start();
            }
        }
    }
}

메소드 별로 if, else 문으로 나뉘는데 10장에서 본 State 패턴을 이용해

좀 더 깔끔한 코드를 만들 수 있다.


다음은 ImageProxy 를 감싸줄 Component 와 테스트 코드이다.

class ImageComponent extends JComponent 
{
    private Icon icon;

    // ImageComponent(Icon icon), setIcon() 생략

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        // 기타 인자값 생략
        icon.paintIcon(this, g, x, y);
    }
}
public class ImageProxyTestDrive 
{
    ImageComponent imageComponent;
    public static void main(String[] args) throws Exception {
        ImageProxyTestDrive testDrive = new ImageProxyTestDrive();
    }

    public ImageProxyTestDrive() throws Exception {
        // 프레임 및 기타 설정 코드 생략

        Icon icon = new ImageProxy(initialURL);
        imageComponent = new ImageComponent(icon);
        frame.getContextPane().add(imageComponent);
    }
}


Q. 전에 가져왔던 이미지를 캐시에 저장해 두는 ImageProxy 와 비슷한 객체를 구현할 수 있지 않나?

- 가상 프록시의 변종 가운데 하나인 캐싱 프록시(Caching Proxy) 라는 것이 있다.

캐싱 프록시는 기존에 생성했던 객체들을 캐시에 저장해 뒀다가 재요청이 들어왔을 때,

상황에 따라 캐시에 저장해두었던 객체를 리턴할 수 있다.


Q. 원격 프록시와 가상 프록시가 같은 패턴에 속하는 게 맞나?

- 프록시 패턴는 다양하게 활용된다. 활용된 그 모든 녀석들의 공통점은

'클라이언트에서 실제 객체의 메소드를 호출하면, 그 호출을 중간에 가로챈다'는 점이다.


이렇게 간접적으로 작업을 처리하면 

요청 내역을 원격 시스템에 있는 객체에 전달할 수도 있고,

생성하는 데 많은 비용이 드는 객체를 대변해줄 수도 있고,

클라이언트 별로 호출할 수 있는 메소드를 제한하는 보디가드 역할을 하는 것도 가능하다.


이 장에서 배우는 프록시 패턴은 시작에 불과하다.

이제 보호 프록시에 대해 알아보고 그 외에 활용되는 방법들을 더 알아볼 것이다.



2. 보호 프록시 (Protection Proxy)

- 접근 권한이 필요한 자원에 대한 접근을 제어할 수 있다.


- Java 에는 java.lang.reflect 패키지에 프록시 기능이 내장되어 있다. 이 패키지를 이용하면

즉석에서 1개 이상의 인터페이스를 구현하고, 메소드 호출을 지정해 준 클래스에 전달할 수 있는

프록시 클래스를 만들 수 있다. 실제 프록시 클래스는 실행중에 생성되기 때문에 

이러한 자바 기술을 '동적 프록시(dynamic proxy)' 라고 부른다.


이번에는 이 동적 프록시를 활용해서 보호 프록시를 만들어 볼 것이다.

보호 프록시를 만들기 전에 동적 프록시가 어떤 식으로 돌아가는지 클래스 다이어그램을 보자.


Proxy 패턴의 공식적인 다이어그램과는 조금 차이가 있다.


동적 프록시의 경우, Proxy 클래스를 Java 에서 만들어 주기 때문에 Proxy 클래스에게

무슨 일을 할지 알려주기 위한 방법이 필요하다. 

하지만 Proxy 클래스를 직접 만들지 않기 때문에 필요한 코드를 InvocationHandler 에 집어넣어 

InvocationHandler 가 Proxy 에 대해서 호출되는 모든 메소드에 대해 응답하는 역할을 맡도록 한다.



예제로는 결혼 정보 서비스를 만들 것이다.

고객들이 서로 상대방에 대한 선호도 점수를 줄 수 있는 기능을 추가할 것이고,

이 서비스는 어떤 사람에 대한 정보를 가져오거나 설정할 수 있게끔 'PersonBean' 을 중심으로 삼는다.


본인이라면 당연히 내 정보를 설정할 수 있고, 타인이 내 정보를 설정하게 하는 건 불가해야 한다.

본인이라면 내 선호도 점수를 매기는 건 불가하고, 타인이라면 내 선호도 점수를 매길 수 있다.

여기서 접근 권한에 따른 제어가 필요하다는 것을 느낄 것이다.


먼저 Subject 인 'PersonBean' 과 RealSubject 인 'PersonBeanImpl' 부터 코드를 살펴보자.

public interface PersonBean 
{
    // String getName(), getGender(), getInterests() 선언
    int getHotOrNotRating();    // 선호도의 평균 점수를 리턴

    // void setName(), setGender(), setInterests() 선언
    void setHotOrNotRating(int rating);
}
public class PersonBeanImpl implements PersonBean 
{
    String name, gender, interests;
    int rating, ratingCount;

    // getName(), getGender(), getInterests(), getHotOrNotRating() 구현

    // setName(), setGender(), setInterests(), setHotOrNotRating() 구현
}


< PersonBean 용 동적 프록시 만들기 3 단계 >

① InvocationHandler 를 만든다.

- 프록시의 메소드가 호출되었을 때 실제 할 일을 지정해 주는 핸들러만 만들면 된다.

② 동적 프록시를 생성하는 코드를 작성한다.

- 프록시 클래스를 생성하고 그 인스턴스를 만들기 위한 코드를 말한다.

③ PersonBean 객체를 적절한 프록시로 감싼다.

- 적절함의 척도는 본인(Owner)이냐 본인이 아니냐(Non-Owner) 이다.

PersonBean 객체를 사용하고자 하는 객체에 따라 적절한 프록시를 생성해주자.



① InvocationHandler 를 만든다.

- 호출핸들러는 본인을 위한 핸들러, 타인을 위한 핸들러 이렇게 2개를 만든다.

- 이 핸들러에는 invoke() 하나 뿐이다. 코드 보기 전에 어떻게 돌아가는지 살펴보자.

1) proxy.setHotOrNotRating(10); 라고 프록시의 메소드가 호출되었다고 가정해보면,

2) 프록시는 핸들러의 invoke(Object proxy, Method method, Object[] args) 를 호출한다.

그러면 invoke() 에서는 1) 에서 호출된 메소드를 어떻게 처리할 것인지 구현하면 된다.

import java.lang.reflect.*;

public class OwnerInvocationHandler implements InvocationHandler 
{
    PersonBean person;

    public OwnerInvocationHandler(PersonBean person) {
        this.person = person;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException {
        try {
            if (method.getName().startsWith("get")) {
                return method.invoke(person, args);
            } else if (method.getName().equals("setHotOrNotRating")) {
                throw new IllegalAccessException();
            } else if (method.getName().startsWith("set")) {
                return method.invoke(person, args);
            }
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }
}

위 코드는 보다시피 본인을 위한 핸들러이고, 타인을 위한 핸들러 NonOwnerInvocationHandler 는

try/catch 구문 안의 조건문만 조금 바꾸면 되니 생략하겠다.


② 동적 프록시를 생성하는 코드를 작성한다.

PersonBean getOwnerProxy(PersonBean person) 
{
    // 이 메소드에서는 RealSubject 를 인자로 받아오고, Proxy 를 리턴한다.
    // 'Proxy 의 인터페이스 = RealSubject 의 인터페이스' 이므로 리턴 타입은 PersonBean 이다.

    return (PersonBean) Proxy.newProxyInstance( 
                person.getClass().getClassLoader(),
                person.getClass().getInterfaces(),
                new OwnerInvocationHandler(person));
}

이렇게 newProxyInstance 의 마지막 인자 값만 바꿔서 getNonOwnerProxy() 메소드도 작성하면 된다.


③ PersonBean 객체를 적절한 프록시로 감싼다.

public class MatchMakingTestDrive 
{
    // 인스턴스 변수 선언

    public static void main(String[] args) {
        MatchMakingTestDrive test = new MatchMakingTestDrive();
        test.drive();
    }

    public MatchMakingTestDrive() {
        initializeDatabase();
    }

    public void drive() {
        PersonBean joe = getPersonFromDatabase("Joe JavaBean");

        // Owner 인 경우, 본인용 프록시 생성
        PersonBean ownerProxy = getOwnerProxy(joe);

        // ownerProxy.setHotOrNotRating() 대한 예외처리 필요

        // Non-Owner 인 경우, 타인용 프록시 생성
        PersonBean nonOwnerProxy = getNonOwnerProxy(joe);

        // nonOwnerProxy.setHotOrNotRating() 를 제외한 setter 메소드에 대한 예외처리 필요
    }

    // getOwnerProxy(), getNonOwnerProxy() 같은 메소드
}

위 코드는 테스트 코드이니 경우에 따라 어떤 프록시를 생성할 것인지만 살펴보면 된다.



Q. 동적 프록시에서 어느 부분이 '동적'이라는 거지?

- 프록시가 동적이라고 한 이유는 클래스가 실행중에 생성되기 때문이다.

실제로 코드가 실행되기 전까지는 프록시 클래스 자체가 없다. 

전달받은 인터페이스를 바탕으로 즉석에서 클래스가 생성된다. ->  단계 코드 참고


Q. InvocationHandler 는 특이한 프록시인건가?

InvocationHandler 자체는 프록시가 아니다. 

메소드 호출을 처리하기 위해 프록시에서 활용하는 클래스일 뿐이다.


Q. 어떤 클래스가 Proxy 클래스인지 알아낼 방법이 있나?

- Proxy 클래스에는 isProxyClass() 라는 정적 메소드가 있다. 

동적 프록시 클래스에 대해서 이 메소드를 호출하면 true 라고 리턴될 것이다.


Q. 왜 스켈레톤(Skeleton) 을 사용해야 되나?

- Java 1.2 부터는 RMI 런타임에서 클라이언트 호출을 reflection 을 이용해 직접

원격 서비스로 넘길 수 있게 되어서 사실 스켈레톤은 안 써도 된다.

하지만 이 장에서는 메커니즘을 이해하는 데 도움이 되기 때문에 스켈레톤을 언급했다.


Q. Java 5 부터는 스터브(Stub) 도 더 이상 만들 필요가 없다고 하는데 정말인가?

- 그렇다. Java 5 부터는 RMI 와 동적 프록시가 결합되어서 스터브 마저도 동적 프록시를 통해

동적으로 생성된다. 따라서 'rmic' 을 전혀 쓰지 않아도 된다. 원격 객체의 메소드를

호출하고 그 결과를 리턴받는 작업이 전부 자동으로 처리되기 때문이다.



3. 기타 프록시 패턴의 활용

- 방화벽 프록시

일련의 네트워크 자원에 대한 접근을 제어함으로써 주 객체를 '나쁜' 클라이언트들로부터 보호해준다. 

기업용 방화벽 시스템에서 자주 쓰인다.

- 스마트 레퍼런스 프록시 (Smart Reference Proxy) :

주 객체가 참조될 때마다 추가 행동을 제공한다. ex) 객체에 대한 레퍼런스 갯수를 카운트.

- 캐싱 프록시 (Caching Proxy) :

비용이 많이 드는 작업의 결과를 임시로 저장해준다. 

여러 클라이언트에서 결과를 공유하게 해 줌으로써 계산 시간 or 네트워크 지연을 줄여주는 효과가 있다. 

웹 서버 프록시 또는 컨텐츠 관리 및 퍼블리싱 시스템에서 자주 쓰인다.

- 동기화 프록시 (Synchronization Proxy) :

여러 스레드에서 주 객체에 접근하는 경우에 안전하게 작업을 처리할 수 있게 해준다.

분산 환경에서 일련의 객체에 대한 동기화 된 접근을 제어해주는 자바 스페이스에서 쓰인다.

- 복잡도 숨김 프록시 (Complexity Hiding Proxy) :

복잡한 클래스들의 집합에 대한 접근을 제어하고, 복잡도를 숨겨준다. 

'퍼사드 프록시(Facade Proxy)' 라고 부르기도 한다.

- 지연 복사 프록시 (Copy-On-Write Proxy) :

클라이언트에서 필요로 할 때까지 객체가 복사되는 것을 지연시킴으로써 객체의 복사를 제어한다.

'변형된 가상 프록시'라고 할 수 있다.

Java 5 의 CopyOnWriteArrayList 에서 쓰인다.



* 객체지향의 원칙 :

1) 바뀌는 부분은 캡슐화 한다.

2) 상속보다는 구성을 활용한다.

3) 구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.

4) 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.

5) 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다(OCP).

6) 추상화된 것에 의존해라. 구상 클래스에 의존해서는 안 된다(의존성 뒤집기 법칙).

7) 친한 친구들하고만 이야기한다(최소 지식 원칙).

8) 먼저 연락하지 마세요. 저희가 연락 드리겠습니다(헐리우드 원칙).

9) 어떤 클래스가 바뀌게 되는 이유는 한 가지 뿐이어야만 한다(단일 역할 원칙).



* 이 장에서의 정리 :

- Proxy Pattern 을 이용하면 어떤 객체에 대한 대변인을 내세워서 클라이언트의 접근을 제어할 수 있다.

- 원격 프록시는 클라이언트와 원격 객체 사이의 데이터 전달을 관리해준다.

- 가상 프록시는 인스턴스를 생성하는 데 비용이 많이 드는 객체에 대한 접근을 제어한다.

- 보호 프록시는 호출한 쪽의 권한에 따라 객체에 있는 메소드에 대한 접근을 제어한다.

- Decorator 패턴에서는 객체에 행동을 추가하지만, Proxy 패턴에서는 접근을 제어한다.

- 다른 Wrapper 를 쓸 때와 마찬가지로 프록시를 쓰면 디자인에 포함되는 클래스와 객체의 수가 늘어난다.



※ Proxy 의 사전적 의미 : 

1) 대리[위임](권)    2) 대리인    3) 대용물



# Pattern 짧게 복습 #

- Decorator : 다른 객체를 감싸서 새로운 행동을 추가해준다.

- Facade : 여러 객체를 감싸서 인터페이스를 단순화 시킨다.

- Adapter : 다른 객체를 감싸서 다른 인터페이스를 제공한다.

- Proxy : 다른 객체를 감싸서 접근을 제어한다.



이번 포스팅도 길었다, 휴 힘들어ㅡ_ㅡ


(이미지 출처 : http://www.jamessugrue.ie/softwaredev/design-patterns-uncovered-the-proxy-pattern)


by kelicia 2014. 6. 2. 00:04


이번 장도 방대한 양을 자랑하는 덕분에^^.. 개념/활용으로 나눠서 포스팅하겠다.


10장에서 보았던 상황 설정에 이어서 추가 요청이 들어왔다고 한다.

각지에 설치된 껌볼기계에 대한 위치, 재고, 상태(State) 를 클라이언트에서 

모니터링 할 수 있도록 설계해야 된다고 한다.


우선 모니터링용 코드를 작성해보자.

public class GumballMachine 
{
    // 기타 인스턴스 변수
    String location;

    public GumballMachine(String location, int count) {
        // 기타 생성자 코드
        this.location = location;
    }

    public String getLocation() { return location; }

    // 기타 메소드
}
public class GumballMonitor
{
    GumballMachine machine;

    public GumballMonitor(GumballMachine machine) {
        this.machine = machine;
    }

    public void report() {
        // machine 객체의 Getter 메소드 이용해 위치, 재고, 상태 출력
    }
}


이제 각지의 껌볼기계에 있는 객체와 통신을 해서 현황에 대한 정보를 받아야 되는데,

이를 위해 '원격 프록시' 를 이용할 것이다.


위 그림에서 Local Heap 안의 Gumball Monitor 가 Client,

Remote Heap 안의 Gumball Machine 이 원격 객체(Remote object)에 해당한다.


Client 객체에서는 원격 객체의 메소드를 호출하는 것처럼 행동하지만

실제로는 Local Heap 의 프록시(Proxy) 객체의 메소드를 호출한다.


네트워크 통신과 관련된 저수준 작업은 이 프록시 객체에서 처리해준다.


'원격 프록시는 원격 객체에 대한 로컬 대변자 역할을 한다'고 생각하면 된다.



나는 '그럼 통신 관련 코드도 짜야되는 거임? 제길ㅡㅡ?' 이라고 생각했지만

JAVA 에 내장되어있는 원격 호출 기능인 RMI 을 활용한다고 한다.


'RMI' 란 'Remote Method Invocation' 의 약자로 번역하면 '원격 메소드 호출' 이다.

그러면 원격 메소드의 기초와 RMI에 대해 알아보자.


지저분하게 설명이 붙어있긴 하지만 하나씩 보겠다.

우선 원격 메소드의 기초를 이해하기 위해 점선 화살표부터 보자.


호출 과정은 다음과 같다. (보조 객체는 Helper 라고 생각하면 된다)


① 클라이언트 객체에서 클라이언트 보조 객체의 어떤 메소드를 호출한다.

② 클라이언트 보조 객체에서는 메소드 호출에 대한 정보(메소드 이름, 인자 등)을 잘 포장해서

네트워크를 통해 서비스 보조 객체에게 이를 전달한다.

③ 서비스 보조 객체에서는 전달받은 정보를 해석해 어떤 메소드를 호출할 지 알아낸 다음,

서비스 객체의 진짜 메소드를 호출한다.

④ 서비스 객체의 메소드가 호출되고 실행이 끝나면 서비스 보조 객체에게 결과를 리턴한다.

⑤ 서비스 보조 객체는 결과를 잘 포장해서 네트워크를 통해 클라이언트 보조 객체에게 전달한다.

⑥ 클라이언트 보조 객체는 이를 해석해 클라이언트 객체에게 리턴한다.

클라이언트 객체 입장에서는 메소드 호출이 어디로 전달되고 어디에서 왔는지 전혀 알 수 없다.


RMI 에서는 위 그림에서 클라이언트 보조 객체, 서비스 보조 객체를 만들어준다.

또한 클라이언트에서 원격 객체를 찾아서 그 원격 객체에 접근하기 위해 쓸 수 있는

룩업(Lookup) 서비스도 제공해준다.


그러니 네트워크 및 입출력 관련 코드를 직접 작성할 필요는 없다.

하지만 네트워크나 입출력 기능을 쓸 때는 위험이 따르는 법이니 예외 처리에 대비해야 한다는 걸 명심하자.


RMI 용어 : 클라이언트 보조 객체는 스터브(Stub), 서비스 보조 객체는 스켈레톤(Skeleton) 이라 부른다.



<< 원격 서비스 만들기 5 단계 >>

1) 원격 인터페이스 만들기 : ex) MyService.java

- 클라이언트에서 원격으로 호출할 수 있는 메소드를 정의한다.

- 스터브와 실제 서비스에서는 모두 이 인터페이스를 구현해야 한다.

2) 서비스 구현 클래스 만들기 : ex) MyServiceImpl.java

- 실제 작업을 처리하는 클래스이다. 1)에서 정의한 원격 메소드를 실제로 구현한 코드가 있는 부분.

3) rmic 를 이용해 스터브와 스켈레톤 만들기 : ex) MyServiceImpl_Stub.java, MyServiceImpl_Skel.java

- 클라이언트 및 서비스 보조 객체를 생성한다.

- 터미널에서 'rmic MyServiceImple(2)에서 작성한 클래스)' 라고 명령어만 치면 끝.

4) RMI 레지스트리(Registry) 를 실행

- 터미널에서 클래스에 접근할 수 있는 디렉토리로 이동한 후에 'rmiregistry' 명령어 실행 

(단, 3)에서 입력한 명령어와 다른 터미널에서 실행)

5) 원격 서비스 시작 :

- 다른 터미널을 열고 원격 서비스의 객체 인스턴스를 만든 클래스를 실행시킨다.

명령어는 잘 알다시피 'java (클래스이름)'



위 5 단계를 따라서 GumballMachine 에 적용해보겠다.


1. 원격 인터페이스 만들기

import java.rmi.*;

public interface GumballMachineRemote extends Remote {
    public int getCount() throws RemoteException;
    public String getLocation throws RemoteException;
    public State getState() throws RemoteException;
}

주의할 점은 원격 메소드의 인자 및 리턴값은 반드시 Primitive 형식 or Serializable 형식 이어야 한다.

Primitive 형식이나 String 또는 API 에서 많이 쓰이는 형식(배열, 컬렉션 등)은 괜찮지만

직접 만든 형식일 경우 Serializable 인터페이스를 구현해야 한다.


원격 메소드의 인자는 모두 직렬화(Serializable)를 통해 포장된 다음 네트워크를 통해 전달된다.

리턴 값도 마찬가지이다.


그러므로 위 코드에서 State 라는 형식이 있으니 약간 수정이 필요하다.

import java.io.*;

public interface State extends Serializable {
    public void insertQuarter();
    public void ejectQuarter();
    public void turnCrank();
    public void dispense();
}

참 쉽죠잉?


하지만 하나 더 고려해야할 점이 있다.

모든 State 객체에는 뽑기 기계(GumballMachine)에 대한 레퍼런스가 들어있는데

State 객체가 리턴될 때 이 뽑기 기계까지 직렬화해서 보내는 건 바람직하지 못하다.

그러니 다음과 같이 해주자.

public class NoQuarterState implements State 
{
    transient GumballMachine gumballMachine;

    // 기타 메소드
}

'transient' 라는 키워드를 붙이면 JVM 에서는 이 필드를 직렬화하지 않는다.

이 키워드를 붙였는데 억지로 저 필드를 호출하면 좋지 못할 일이 생기니 기억해두자.



2. 서비스 구현 클래스 만들기


GumballMachine 이미 10장에서 구현했지만 서비스 역할과 네트워크를 통해 들어온 요청을

처리하기 위해 약간 수정해주자.

import java.rmi.*;
import java.rmi.server.*;

public class GumballMachine extends UnicastRemoteObject implements GumballMachineRemote
{
    // 인스턴스 변수들

    public GumballMachine(String location, int numberGumballs) throws RemoteException {
        // 생성자 코드
    }

    // getCount(), getState(), getLocation() 등 기타 메소드
}

원격 객체가 되기 위한 'UnicastRemoteObejct' 를 확장하였지만 주의해야 할 점이 있다.

UnicastRemoteObject 의 생성자에서 RemoteException 을 던진다.

어떤 클래스가 생성될 때 그 수퍼클래스의 생성자도 반드시 호출되기 때문에

서브클래스의 입장에서도 RemoteException 을 선언해야 한다.


그리고 클라이언트에서 인터페이스에 있는 메소드를 호출할테니

GumballMachineRemote 를 구현해주자.


뽑기 기계의 원격 서비스 부분은 다 끝났지만 클라이언트에서 이 서비스를 쓸 수 있도록

서비스의 객체 인스턴스를 만든 후, RMI 레지스트리에 등록하기만 하면 된다.


서비스를 구현한 객체를 등록하면 RMI 시스템에서는 스터브만 레지스트리에 등록한다.

클라이언트에서는 스터브만 필요하기 때문이다.


레지스트리 등록을 처리해주는 간단한 테스트용 클래스(GumballMachineTestDrive.java)를 만든 후 

메인함수에 다음과 같은 코드를 넣어주자.

try {
    gumballMachine = new GumballMachine(args[0], count);    //args[0] 은 Location 정보
    Naming.rebind("(서비스 이름)", gumballMachine);
} catch (Exception e) {
    e.printStackTrace();
}

try/catch 구문으로 감싼 이유는 객체 인터스턴스를 생성할 때 생성자에서

예외를 던질 수 있으니(RemoteException) 감싸주었다.


그리고 Naming.rebind() 를 살펴보면,

클라이언트는 '서비스 이름' 을 써서 레지스트리를 검색하기 때문에 이름을 지정해주어야 한다.

주의해야 할 점은 레지스트리 등록 코드가 들어있는 클래스가 실행될 때 

(원격 서비스 만들기의 4단계에 해당되는) RMI 레지스트리가 돌아가고 있어야 한다는 점이다.


3. rmic 를 이용해 스터브와 스켈레톤 만들기


터미널에서 rmic GumballMachine 을 때려주면 JDK 에 포함되어 있는 rmic 툴이 

서비스를 구현한 클래스를 받아서 스터브와 스켈레톤 이렇게 2개의 클래스를 생성해준다.


4. RMI 레지스트리 실행


터미널에서 rmiregistry 명령어를 입력해주자.


5. 원격 서비스 시작


터미널에서 java GumballMachineTestDrive seattle.mightygumball.com 100 라고 입력해주면 된다.

'GumballMachineTestDrive' 은 테스트용 코드 작성한 클래스 이름이고,

'seattle.mightygumball.com' 은 뽑기 기계가 위치한 장소, '100' 은 껌볼 갯수 입력해준 것이니 신경끄자.



휴, 여기까지가 서버측에서 해야할 일이다.

이제 GumballMonitor 클라이언트를 살펴보자.

import java.rmi.*;

public class GumballMonitor
{
    GumballMachineRemote machine;

    public GumballMonitor(GumballMachineRemote machine) {
        this.machine = machine;
    }

    public void report() {
        try {
            // machine 객체의 Getter 메소드 이용해 위치, 재고, 상태 출력
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

1) GumballMachine 구상 클래스 대신 원격 인터페이스를 사용했다.

2) RemoteException 클래스를 사용해야 되서 rmi 를 import 했고, machine 객체의 Getter 메소드를 

네트워크를 통해 호출해야 하므로 RemoteException 에 대비해서 try/catch 구문으로 잡아주었다.



서버와 클라이언트 모두 준비 완료다.

평소 테스트 코드는 잘 안 넣는 편인데 이번에 넣어보겠다.

import java.rmi.*;

public class GumballMonitorTestDrive
{
    public static void main(String[] args) {
        String location = "rmi://seattle.mightygumball.com/gumballmachine";

        try {
            GumballMachineRemote machine = (GumballMachineRemote) Naming.lookup(location);
            GumballMonitor monitor = new GumballMonitor(machine);
        } catch (Exception e) {
            e.printStackTrace();
        }

        monitor.report();
    }
}

참고로 아까 GumballMachineTestDrive 클래스에서 서비스를 레지스트리에 등록할 때,

"(서비스이름)" 이라고 했는데 실은 서비스 이름을 '//seattle.mightygumball.com/gumballmachine' 라고 등록했다.


Naming.lookup() 은 RMI 패키지에 있는 정적 메소드로 서비스 이름을 받아

그 이름을 통해 RMI 레지스트리에서 룩업 작업을 수행해준다.


RMI 레지스트리에서 프록시를 가져오고 나면 그 프록시를 클라이언트인 GumballMonitor 에게 넘겨준다.



* RMI 사용할 때 가장 흔하게 범하는 실수 :

1) 원격 서비스를 돌리기 전에 rmiregistry 를 실행시키 않은 경우

2) 인자와 리턴 형식이 직렬화 가능한지 확인하지 않은 경우 (런타임에서 오류가 발생)

3) 클라이언트에 스터브 클래스를 건네주지 않은 경우


여기서 Client 는 Monitor, MyServiceImpl 는 Machine 이라는 게 파악이 될 것이다.


서버 쪽에 스터브가 있는 이유는 - 아까 파란색 글씨로 표시해두었지만 -

서비스를 구현한 클래스가 RMI 레지스트리에 등록되면, 자동으로 스터브 클래스가 대신 등록되기 때문이다.



※ 전문가 Tip ※  (내가 전문가라는 뜻이 절대 아님, 책에 이렇게 나와있어서 그냥 갖다쓴거지)

클라이언트에서는 lookup 을 하려면 (rmic으로 생성한) Stub 클래스를 보유하고 있어야한다.

간단한 시스템에서는 그냥 Stub 클래스를 직접 클라이언트로 ctrl + c,v 해주면 된다.

하지만 그런 방법보다 더 좋은 방법이 있다. 바로 '동적 클래스 다운로딩(Dynamic Class Downloading)'이다.


동적 클래스 다운로딩을 사용하면 직렬화된 객체에 그 객체의 클래스 파일이 있는 위치를 나타내는

URL 이 내장된다. 그리고 역직렬화 과정에서 로컬 시스템의 클래스 파일을 찾지 못하게 된다면

내장된 URL 로부터 HTTP GET 요청을 통해 클래스 파일을 가져온다.


따라서 클래스 파일을 보내줄 수 있는 웹 서버가 있어야 되고 클라이언트의 보안 매개변수를

변경해야 될 수도 있다. 그 밖에도 더 신경써야할 것이 있지만 여기까지만 언급한다고 하신다.



RMI 덕분에 포스팅이 겁나 길어졌다. 이제 정리 좀 해보자.


원격 프록시(Remote Proxy)는 일반적인 프록시 패턴을 구현하는 방법 가운데 하나이다.

이 외에도 여러가지 변형된 방법들이 많다. 그건 다음 포스팅에서 다루고 우선 프록시 패턴의 정의를 보자.


* Proxy Pattern :

어떤 객체에 대한 접근을 제어하는 용도로 대리인이나 대변인에 해당하는 객체를 제공하는 패턴


정의 자체는 심플하지만 '접근을 제어하는 방법' 면에서 변형된 프록시 녀석들이 많다.


- 원격 프록시 : 원격 객체에 대한 접근 제어가 가능

- 가상 프록시 (Virtual Proxy) : 생성하기 힘든 자원에 대한 접근을 제어할 수 있다.

- 보호 프록시 (Protection Proxy) : 접근 권한이 필요한 자원에 대한 접근을 제어할 수 있다.


이정도만 하고 프록시 패턴의 클래스 다이어그램으로 넘어가자.


Proxy 에는 RealSubject 에 대한 레퍼런스가 들어있다.

Proxy 에서 RealSubject 를 생성하거나 제거하는 역할을 책임지는 경우도 있다. 


클라이언트는 항상 Proxy 를 통해서 RealSubject 하고 데이터를 주고 받는다. 

Proxy 와 RealSubject 는 똑같은 인터페이스(Subejct) 를 구현하기 때문에 

RealSubejct 객체가 들어갈 자리면 어디든지 Proxy 를 대신 집어넣을 수 있다


Proxy 는 RealSubject 에 대한 접근을 제어하는 역할도 맡게 된다.

RealSubject 가 원격 시스템에서 돌아가거나, 그 객체의 생성 비용이 많이 들거나, 어떤 방식으로든지 

RealSubject 에 대한 접근이 통제되어 있는 경우에 접근을 제어해주는 객체가 필요할 수 있다.



여기까지 1차적으로 포스팅을 마무리하고

활용 부분과 기타 정리할 부분은 2차 포스팅으로 넘어가겠다.


(이미지 출처 :

http://dlucky.tistory.com/231

http://darkmirr.egloos.com/m/1219935

http://blog.lukaszewski.it/2014/01/31/design-patterns-proxy/)


by kelicia 2014. 6. 1. 02:46


이번에도 이해를 돕기 위한 상황설정 들어가겠다.

주로 문방구 앞에서 보이는 뽑기 기계(Gumball Machine)에 들어갈 코드를 작성해야 하는데

다음과 같은 상태 다이어그램(State Diagram)이 주어졌다고 하자.


보다시피 4개의 상태가 존재한다.

(No Quarter : 동전 없음, Has Quarter : 동전 있음, Gumball Sold : 껌볼 판매, Out of Gumballs : 매진)



상황 파악이 대충 되었으니 일단 코드를 작성해보자.

public class GumballMachine {
    final static int SOLD_OUT = 0;
    final static int NO_QUARTER = 1;
    final static int HAS_QUARTER = 2;
    final static int SOLD = 3;

    int state = SOLD_OUT;
    int count = 0;    // 껌볼 갯수

    public GumballMachine(int count) {
        this.count = count;
        if ( count > 0 ) { 
            state = NO_QUARTER; 
        }
    }

    public void insertQuarter() {
        if ( state == HAS_QUARTER ) { // 출력 : 동전은 한 개만 }
        else if ( state == NO_QUARTER ) { 
            state = HAS_QUARTER;
            // 출력 : 동전 받음
        } 
        else if ( state == SOLD_OUT ) { // 출력 : 매진 }
        else if ( state == SOLD ) { // 출력 : 껌볼 나오는 중 }
    }

    public void ejectQuater() { // 동전 반환시에 해야할 일 }
    public void turnCrank() { // 손잡이 돌릴 때 해야할 일 }
    public void dispense() { // 껌볼 내줄 때 해야할 일 }

    // 기타 메소드
}

코드 길게 넣는 거 안 좋아하지만... 나중에 나올 코드와 비교하기 좋게 일단 썼다.

(코드 자세히 읽다보면 가끔 뭐 할려고 했는지 까먹음ㅡ_ㅡ)


어쨌든 주목할 부분은

1) 상태 값 저장을 위한 인스턴스 변수

2) 각 메소드 별로 각각의 상태에 대해 덕지덕지 붙은 if 문


내가 프로그래밍 초창기 때 많이 써먹던 방법...이긴 하나

딱 봐도 이상적이지 못 하다는 느낌과 더불어 차후 요구사항 변경으로 인해

코드가 계속 지저분해질 것 같다는 걱정에 눈 앞이 깜깜해진다.


구체적으로 여러가지 문제점이 있다.

- OCP 를 지키고 있지 않다는 점

- 객체지향 디자인이라고 하기엔 무리가 있다

- 지저분한 조건문으로 인해 상태전환이 분명하게 드러나있지 않다

- 다른 기능을 추가하는 과정에서 기존 코드에 없던 새로운 버그가 생길 위험이 높다



문제점을 파악했으니 이제 State 패턴을 적용해 해결하자.

디자인에 들어있는 모든 상태를 캡슐화시켜 State 인터페이스를 구현하는 클래스를 만든다.



SoldState 클래스 코드를 간단히 보자.

public class SoldState implements State 
{
    GumballMachine gumballMachine;

    public SoldState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    public void insertQuarter() { // 출력 : 껌볼 나오는 중 }
    public void ejectQuarter() { // 출력 : 이미 껌볼 나왔음 }
    public void turnCrank() { // 출력 : 손잡이는 1번만 돌려 }

    public void dispense() {
        gumballMachine.releaseBall();
        if ( gumballMachine.getCount() > 0 ) {
            gumballMachine.setState(gumballMachine.getNoQuarterState());
        } else {
            // 출력 : 매진
            gumballMachine.setState(gumballMachine.getSoldOutState());
        }
    }
}


그리고 GumballMachine 클래스를 다시 작성해보자.

public class GumballMachine 
{
    State soldState;
    // 그 외 3가지 State 객체 선언

    State state = soldOutState;
    int count = 0;

    public GumballMachine(int numberGumballs) 
    {
        soldState = new SoldState(this);
        // 그외 3가지 State 객체 초기화

        this.count = numberGumballs;
        if ( numberGumballs > 0 ) {
            state = noQuarterState;
        }
    }

    public void insertQuarter() { state.insertQuarter(); }
    public void ejectQuarter() { state.ejectQuarter(); }
    public void turnCrank() { 
        state.turnCrank();
        state.dispense();
    }
    
    void setState(State state) { this.state = state; }
    void releaseBall() {
        // 출력 : 껌볼 굴러가는 중
        if ( count != 0 ) { count--; }
    }

    // Getter 메소드를 비롯한 기타 메소드
}


이렇게 패턴을 적용하고 나니 어떤 결과를 얻었을까?

- 각 State 의 행동(Behavior)을 별개의 클래스로 국지화 시킴

- 지저문한 if 문이 사라짐

- 각 State를 변경하는 것에 대해서는 닫혀있으면서 GumballMachine 자체는

  새로운 상태 클래스를 추가하는 확장에 대해서는 열려있도록 바뀜(OCP)

- 포스팅 상단에 있는 다이어그램에 훨씬 가까우면서도 이해하기 좋은 코드 베이스와 클래스 구조 생성



* State Pattern : 

객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있다.

마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있다.



State 패턴 다이어그램을 살펴보자.



내가 1장을 너무 허접하게 포스팅하는 바람에

눈치를 못 챘는데 위 다이어그램은 1장에서 다뤘던 Strategy 패턴과 똑같다 !



Strategy 패턴, State 패턴의 다이어그램은 같지만 

이 2가지의 패턴은 용도에 있어서 차이점이 있다.


< Strategy 패턴 >

- 일반적으로 Client 에서 Context 객체한테 어떤 전략 객체를 사용할 지를 지정해준다.

주로 실행시에 전략 객체를 변경할 수 있는 유연성을 제공하기 위한 용도로 쓰인다.

- SubClass 를 만드는 방법을 대신하여 유연성을 극대화하기 위한 용도로 쓰인다.

상속을 이용해서 Class 의 행동을 정의하다보면 행동을 마음대로 변경하기가 힘들지만

Strategy 패턴을 사용하면 구성을 통해 행동을 정의하는 객체를 유연하게 바꿀 수 있다.


< State 패턴 >

- State 객체에 일련의 행동이 캡슐화 된다. 상황에 따라 Context 객체에서 여러 State 객체 중

하나의 객체에게 모든 행동을 맡기게 된다. Client 는 State 객체에 대해 자세히 몰라도 된다.

- Context 객체에 지저분하게 조건문을 집어넣는 대신에 사용할 수 있는 패턴이라 할 수 있다.

- Context 객체에서는 미리 정해진 State 전환 규칙을 가지고 자기 State 를 변환한다.




Q. 아까 코드를 보면 구상 State 클래스에서 GumballMachine(Context) 의 SetState() 를 이용해

다음 State 를 결정했는데 반드시 그래야 하나?


- 항상 그런 건 아니다. State 전환이 고정되어 있으면(정적일 경우) Context 에서 

State 전환의 흐름을 결정하는 코드를 작성해도 된다. 하지만 아까와 같이 실행 중에 껌볼의 갯수에 따라 

State 전환이 동적으로 결정되는 경우에는 State 클래스에서 처리하는 것이 좋다.


- State 전환 코드를 State 클래스에 집어넣으면 State 클래스들 사이에 의존성이 생긴다는 단점이 있다.

아까 코드에서는 GumballMachine 객체의 Getter 메소드를 써서 그나마 의존성을 최소화하려고 노력했다.



+ State 패턴을 이용하다 보면 디자인에 필요한 Class 의 갯수가 늘어나지만

유연성을 향상시키기 위해 지불해야 하는 비용이라고 생각하자.

개인적으로 지저분한 if 문은 사절이다.



* 객체지향의 원칙 :

1) 바뀌는 부분은 캡슐화 한다.

2) 상속보다는 구성을 활용한다.

3) 구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.

4) 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.

5) 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다(OCP).

6) 추상화된 것에 의존해라. 구상 클래스에 의존해서는 안 된다(의존성 뒤집기 법칙).

7) 친한 친구들하고만 이야기한다(최소 지식 원칙).

8) 먼저 연락하지 마세요. 저희가 연락 드리겠습니다(헐리우드 원칙).

9) 어떤 클래스가 바뀌게 되는 이유는 한 가지 뿐이어야만 한다(단일 역할 원칙).



* 이 장에서의 정리 :

- State 패턴을 사용하면 Procedure 형 State Machine 을 쓸 때와 달리 클래스를 이용하여 각 State 를 표현한다.

- 각 State 를 캡슐화함으로써 차후에 생길 변동사항을 국지화 시킬 수 있다.

- Strategy 패턴에서는 일반적으로 Context 클래스를 만들 때 행동 or 알고리즘을 설정한다.

- State 클래스를 여러 Context 객체의 인스턴스에서 공유하도록 디자인할 수도 있다.



# Pattern 짧게 복습 #

- State : 상태를 기반으로 하는 행동을 캡슐화하고 행동을 현재 상태한테 위임한다.

- Strategy : 바꿔 쓸 수 있는 행동을 캡슐화 한 다음, 실제 행동은 다른 객체에 위임한다.

- Template Method : 알고리즘의 각 단계를 구현하는 방법을 서브클래스에서 구현한다.



(이미지 및 기타 출처 :

http://www.cnblogs.com/zhuqiang/archive/2012/05/04/2482415.html

http://blog.lukaszewski.it/2013/12/29/design-patterns-state/

http://stevenjsmin.tistory.com/78)


by kelicia 2014. 5. 30. 21:24


다른 챕터에 비해 이번 챕터는 내용이 많다ㅡ_ㅡ. 휴


우선 어떤 상황을 가정하겠다.

서로 다른 클래스 A, B에서 각자의 방식으로 구현된 배열들을 Clinet 에서 출력하는 코드를 작성할 때, 

- A 클래스의 배열은 ArrayList, B 클래스의 배열은 기본 배열로 구현되었다고 예를 들겠다. -

다음과 같은 방법을 쓸 수 있다.

for(int i=0; i<arrayListItems.size(); i++){
	MenuItem menuItem = (MenuItem) arrayListItems.get(i);
	// 출력
}

for(int i=0; i<arrayItems.length; i++){
	MenuItem menuItem = arrayItems[i];
	// 출력
}

배열 타입 때문에 코드 자체는 조금 다르지만 명백하게 같은 일을 하고 있다.

이렇게 구상 클래스에 맞춰서 코딩하면 코드 중복 및 수정, no 캡슐화와 같은 폐해가 뒤따른다.


그래서 !

이를 해결하기 위해 '반복을 캡슐화'할 것이다.



1. Iterator 패턴


- Iterator (반복자) 라는 인터페이스를 통해 Array, List, HashTable 은 물론이고 

어떤 종류의 컬렉션(또는 집합체, Aggregate)에 대해서도 반복자를 구현할 수 있다.


다음과 같이 인터페이스를 직접 만들어도 되고,

public interface Iterator {
    boolean hasNext();
    Object next();
}

아니면 java.util.Iterator 를 사용해도 된다. (위의 인터페이스에서 void remove()만 추가하면 된다)

참고로 ArrayList 의 메소드 중에는 반복자를 리턴하는 iterator() 라는 메소드가 있다.


하지만 기본 Array 에서는 iterator()가 없으니 

아까 위에서 기본 배열 방식으로 구현한 B 클래스를 위한 IteratorB 클래스를 만든 후,

그 클래스에 java.util.Iterator 를 import 해서 Iterator 인터페이스를 구현해보겠다.

import java.util.Iterator;

public class IteratorB implements Iterator {
    MenuItem[] arrayItems;
    int position = 0;

    public IteratorB(MenuItem[] arrayItems) {
        this.arrayItems = arrayItems;
    }

    public Object next() {
        MenuItem menuItem = arrayItems[position];
        position = position + 1;
        return menuItem;
    }

    public boolean hasNext() {
        if ( position >= arrayItems.length || arrayitems[position] == null ) {
            return false;
        } else {
            return true;
        }
    }

    public void remove() {
        if ( position <= 0 ) {
            throw new IllegalStateException("can't remove");
        }
        if ( arrayItems[position-1] != null ) {
            for( int i = position-1; i < (arrayItems.length-1); i++ ) {
                arrayItems[i] = arrayItems[i+1];
            }
        }
    }
}

Tip. remove()가 필요없다면 예외를 던질 수도 있다.

public void remove() {
    throw new UnsupportedOperationException("Unsupprted Operation 'remove()'");
}


반복자를 구현했으면 이 반복자를 사용하기 위해 객체를 생성해줘야 한다.


ArrayList 로 구현한 A 클래스에서는

public Iterator createIterator() {
    return arrayListItems.iterator();
}

Array 로 구현한 B 클래스에서는

public Iterator createIterator() {
    return new IteratorB(arrayItems);
}


자, 반복자 패턴을 위한 멍석을 다 깔아놓았으니 Client 클래스 코드에서 출력 코드를 작성해보자.

public class Client {
    A arrayListItems;
    B arrayItems;

    public Client ( A arrayListItems, B arrayItems ) {
        this.arrayListItems = arrayListItems;
        this.arrayItems = arrayItems;
    }

    public void printMenuItems() {
        Iterator iteratorA = arrayListItems.createIterator();
        Iterator iteratorB = arrayItems.createIterator();
        printMenuItems(iteratorA);
        printMenuItems(iteratorB);
    }

    public void printMenuItems(Iterator iterator) {
        while ( iterator.hasNext() ) {
            MenuItem menuItem = (MenuItem) iterator.next();
            // 출력
    }
}


여기서 끝난 게 아니다. 아직 Client 가 A, B 라는 구상 클래스를 참조하고 있다.

화룡점정으로 집합체 클래스인 A, B 의 인터페이스를 통일시켜주자.


아까 위에서 A, B 클래스 각자 createIterator() 를 구현했다.

public interface Aggregate {
    public Iterator createIterator();
}

Aggregate 라는 인터페이스를 이처럼 작성하고

A, B 클래스 선언부에 implements Aggregate 만 추가해주면 통일끝.

그러면 Client 는 구상 클래스가 아닌 인터페이스를 참조할 수 있다.

public class Client {
    Aggregate arrayListItems;
    Aggregate arrayItems;

    public Client ( Aggregate arrayListItems, Aggregate arrayItems ) {
        this.arrayListItems = arrayListItems;
        this.arrayItems = arrayItems;
    }

    // 이하 생략
}



휴, 힘들다. 코드가 여기저기 튀어나와 복잡하니 정리해보겠다.

클래스 다이어그램을 보자.

이 클래스 다이어그램으로 위에서 설명했던 것이 정리가 된다.

(참고로 Factory Method 패턴의 다이어그램과 상당히 유사한 형태이다)


* Iterator Pattern : 

컬렉션 구현 방법을 노출시키지 않으면서도 그 집합체 안에 들어있는 모든 항목에

하나씩 접근할 수 있게 해주는 방법을 제공한다.


이것이 바로 반복을 캡슐화하는 패턴이다.


Iterator 패턴을 사용하면 모든 항목에 일일이 접근하는 작업을

컬렉션 객체(Concrete Aggregate)가 아닌 반복자 객체에서 맡게된다는 장점이 있다.


이렇게 하면 컬렉션의 인터페이스 및 구현이 간단해지고,

컬렉션은 반복작업에서 손 떼고 컬렉션 관리에만 전념하면 된다.



* 단일 역할 원칙

- 어떤 클래스를 바꾸는 이유는 한 가지 뿐이어야 한다.


즉, 한 클래스에는 1가지 역할만 맡도록 해야 한다.

Iterator 패턴의 경우 반복작업과 컬렉션 관리를 신경쓰는 컬렉션을

관리라는 역할에만 충실할 수 있도록 도와주었으니 이 원칙을 준수한다.


+ 응집도(cohesion) :

한 클래스 또는 모듈이 특정 목적 또는 역할을 얼마나 일관되게 지원하는지를 나타내는 척도.

응집도가 높다는 것은 일련의 서로 연관된 기능이 묶여있다는 것을,

응집도가 낮다는 것은 서로 상관 없는 기능들이 묶여있다는 것을 뜻한다.



※ 마무리 Tip 

(1) HashTable 의 경우 iterator() 를 지원하는 자바 컬렉션이지만

① hashTableItems.iterator() -> (X)

② hashTableItems.values().iterator -> (O)


HashTable 이 <key, value> 로 구성되있다는 점을 떠올리면 ② 가 왜 옳은지 알 수 있다.


(2) JAVA 컬렉션 프레임워크에서는 ListIterator 라는 반복자 인터페이스도 제공하는데,

이 인터페이스에는 Iterator 의 메소드 외에도 previous() 를 비롯해 몇 개 더 추가되어 있다.

그러니 next() 때문에 한 방향으로만 접근해야 되는지에 대한 걱정은 안 해도 된다.




2. Composite Pattern


- Iterator 패턴과 Composite 패턴을 꼭 묶어서 생각할 필요는 없다 -

Iterator 패턴 덕분에 출력 코드를 for 문으로 도배하는 것을 막을 수 있었다.

하지만 Client 클래스의 printMenuItems() 를 보자.

public void printMenuItems() {
    Iterator iteratorA = arrayListItems.createIterator();
    Iterator iteratorB = arrayItems.createIterator();
    print(iteratorA);
    print(iteratorB);
}

새로운 Concrete Aggregate 가 추가될 때마다 여기에 코드를 추가해주어야 한다.

이는 'OCP(Open Closed Principle) 원칙 - 확장은 open, 코드 변경은 close -' 에 위배된다.


게다가 집합체(Aggregate) 안에 또 다른 집합체를 갖고 있을 경우 골치가 아파진다.

ex) array = { 1, 2, 3, {7, 8, 9} } 



자, 더 유연한 방법으로 아이템에 대해서 반복작업을 수행할 수 있어야 한다.

리팩토링이 필요한 시기라 할 수 있다.


우선 골치 아파지기 전에 다음과 같이 Tree 구조를 사용하는 게 좋을 듯 싶다.

* Composite Pattern :

객체들을 트리(Tree) 구조로 구성하여 부분-전체 계층구조를 구현한다.

이 패턴을 이용하면 Client 에서 개별 객체와 복합 객체(Composite)를 똑같은 방법으로 다루도록 할 수 있다.


정의 내린 김에 Composite 패턴의 클래스 다이어그램도 보자.


복합 객체(Composite) 에는 구성요소(Component) 가 들어가있다.

구성요소는 2 종류로 나뉜다.

하나는 복합 객체, 다른 하나는 잎(Leaf)이다.

복합 객체에는 일련의 자식들(Children)이 있고, 그 자식노드 역시 복합객체 or 잎일 수 있다.

그리고 잘 알다시피 잎은 자식이 하나도 없다.


즉, 재귀적인 구조를 갖는다.



이제 패턴을 이용해서 아까의 문제로 돌아가서 적용해보자.

아래 다이어그램에서 웨이트리스(Waitress) 는 Client 다.

원래 책의 예제에서는 아까 언급했던 A, B 클래스가 서로 다른 음식점의 메뉴(Menu)에 해당한다.


MenuComponent 가 

MenuItem(Leaf) 와 Menu(Composite) 에서 쓰이는 인터페이스 역할을 한다는 점을 기억해두자.


MenuComponent 의 기본 구현을 해보자면,

public abstract class MenuComponent 
{
    // Composite's Method
    public void add(MenuComponent menuComponent) {
        throw new UnsupportedOperationException(); // body 는 이하 모두 동일
    }
    public void remove(MenuComponent menuComponent) { }
    public MenuComponent getChild(int i) { }

    // Leaf, Composite's Method
    public String getName() { }
    public String getDescription() { }
    public double getPrice() { }
    public boolean isVegitarian() { }

    public void print() { }
}

MenuItem 은 위 클래스 다이어그램 만으로도 충분하므로 Menu 코드만 보겠다.

Menu 코드에서 print() 를 자세히 보면 재귀적인 방법으로 출력하는 것을 볼 수 있다.

public class Menu extends MenuComponent 
{
    ArrayList menuComponents = new ArrayList();

    // 생성자 코드

    // 기타 메소드

    public void print() { 
        Iterator iterator = menuComponents.iterator();
        while ( iterator.hasNext() ) {
            MenuComponent menuComponent = (MenuComponent) iterator.next();
            menuComponent.print();
    }
}

Client 코드는 간단하다.

public class Client 
{
    MenuComponent allMenus;

    public Client (MenuComponent allMenus) {
        this.allMenus = allMenus;
    }

    public void print() {
        allMenus.print();
    }
}

테스트 코드에서는 

MenuComponent allMenus = new Menu(); 를 선언하고 

여러 메뉴 항목들을 만들어 allMenus 에 추가 한 다음(add()),

Client client = new Client(allMenus); -> client.print(); 하면 말끔하다.



Q. Composite 패턴에서 계층 구조 관리와 메뉴와 관련된 작업을 처리하는데,

단일 역할 원칙에 위배되는 건 아닌가?

- Composite 패턴은 단일 역할 원칙을 깨지만 대신에 투명성(Transparency)을 확보하는 패턴이다.


Component 인터페이스에 계층 구조 관리 기능과 잎(Leaf)의 기능을 전부 집어넣음으로써

Client 에서 복합 객체와 잎 노드를 똑같은 방식으로 처리할 수 있도록 할 수 있다.

어떤 것이 복합 객체고 잎 노드인지가 Client 입장에서는 투명하게 느껴진다.


2가지 기능을 하다보니 안정성은 약간 떨어지나 

(Client 입장에서는 구분을 못 하므로 어떤 객체에 부적절한 메소드를 호출하는 일이 있을 수 있다)

만약 쪼개서 디자인한다면 투명성이 떨어져 코드에 조건문, instanceof 를 써야할 수도 있다.


모든 디자인 원칙을 준수하는 건 어렵기 때문에 상황에 따라 원칙을 적절하게 사용해야한다.




* 복합 객체(Composite) 에 대한 반복자(Iterator) 구현


끝난 게 아니다. createIterator() 가 남아있다.


인터페이스인 MenuComponent 에 이 메소드를 추가할 것이다.

그러면 Menu 와 MenuItem 클래스에서 추가로 구현하자.


Menu 는

public Iterator createIterator() {
    return new CompositeIterator(menuComponents.iterator());
}

MenuItem 는 NullIterator 객체를 리턴하는데

NullIterator는 Iterator 를 구현한 클래스이고 Command 패턴에서 봤던 Null 객체와 같은 개념이다.

public Iterator createIterator() {
    return new NullIterator();
}


마지막 코드다. CompositeIterator 클래스이다.

import java.util.*;

public class CompositeIterator implements Iterator 
{
    Stack stack = new Stack();

    public CompositeIterator(Iterator iterator) {
        stack.push(iterator);
    }

    public Object next() {
        if (hasNext()) {
            Iterator iterator = (Iterator) stack.peek();
            MenuComponent component = (MenuComponent) iterator.next();
            if (component instanceof Menu) {
                stack.push(component.createIterator());
            }
            return component;
        } else {
            return null;
        }
    }

    public boolean hasNext() {
        if (stack.empty()) {
            return false;
        } else {
            Iterator iterator = (Iterator) stack.peek();
            if (!iterator.hasNext()) {
                stack.pop();
                return hasNext();
            } else {
                return true;
            }
        }
    }

    public void remove() {
        throw new UnsupportedOperationException();
    }
}

재귀때문에 조금 빡칠 수도 있다.

이해하고 보면 별 것도 아니니 인내를 가지고 이해하자.



- Composite 패턴의 가장 큰 장점은 클라이언트를 단순화 시킬 수 있다는 점이다.

아까 투명성 얘기에서 말했다시피 클라이언트는 복합 객체인지, 잎 객체인지 신경쓰지 않아도 된다. 

올바른 객체에 대해서 올바른 메소드를 호출하고 있는지 확인하려고 

여기저기 if 문을 사용하지 않아도 된다는 이야기이다.

그리고 메소드 하나만 호출하면 전체 구조에 대해서 반복해서 작업을 처리할 수 있는 경우도 자주 있다.


- 복합 구조가 너무 복잡하거나 복합 객체 전체를 Traveling 하는 데

너무 많은 자원이 필요한 경우에는 복합 노드를 Caching 해두는 것도 도움이 된다.

예를 들어 복합 객체에 있는 모든 자식노드에서 어떤 계산을 하고, 그 모든 자식노드들에 대해서

반복적인 작업을 수행해야 한다면 계산 결과를 임시로 저장하는 캐시를 만들어 속도를 향상시킬 수 있다.



* 객체지향의 원칙 :

1) 바뀌는 부분은 캡슐화 한다.

2) 상속보다는 구성을 활용한다.

3) 구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.

4) 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.

5) 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다(OCP).

6) 추상화된 것에 의존해라. 구상 클래스에 의존해서는 안 된다(의존성 뒤집기 법칙).

7) 친한 친구들하고만 이야기한다(최소 지식 원칙).

8) 먼저 연락하지 마세요. 저희가 연락 드리겠습니다(헐리우드 원칙).

9) 어떤 클래스가 바뀌게 되는 이유는 한 가지 뿐이어야만 한다(단일 역할 원칙).



* 이 장에서의 정리 :


- Iterator 패턴 - 

. 반복자(Iterator)를 이용하면 내부 구조를 드러내지 않으면서도 클라이언트로부터

  컬렉션 안에 들어있는 모든 원소들에 일일이 접근하도록 할 수 있다.

. 집합체에 대한 반복 작업을 별도의 객체로 캡슐화 할 수 있다.

. 컬렉션에 모든 데이터에 대해서 반복 작업을 하는 역할을 컬렉션에서 분리할 수 있다.


- Composite 패턴 -

. 개별 객체와 복합 객체를 모두 담아둘 수 있는 구조를 제공한다.

. 클라이언트에서 개별 객체와 복합 객체를 동일한 방법으로 다룰 수 있다.

. 복합 구조는 구성 요소로 이루어진다. 구성요소에는 복합 객체와 잎 노드가 있다.

. 상황에 따라 투명성(Transparency)과 안전성 사이에서 적절한 평형점을 찾아야 한다.



# Pattern 짧게 복습 #

- Observer : 어떤 상태가 변경되었을 때 일련의 객체들에게 이를 알려준다.

- Adapter : 하나 이상의 클래스의 인터페이스를 변환한다.

- Facade : 일련의 클래스들에 대한 인터페이스를 단순화시킨다.

- Iterator : 컬렉션의 구현을 드러내지 않으면서도 컬렉션에 있는 모든 객체들에 대해 반복 작업이 가능하다.

- Composite : 클라이언트에서 객체 컬렉션과 개별 객체를 같은 방식으로 처리할 수 있다.



(이미지 및 기타 출처 :

http://stevenjsmin.tistory.com/77

http://sadiles.blog.me/10110397609

http://blog.lukaszewski.it/2013/11/19/design-patterns-composite/

http://blog.lukaszewski.it/2013/10/14/design-patterns-iterator/

http://www.cnblogs.com/zhuqiang/archive/2012/04/27/2474233.html

http://www.cnblogs.com/zhuqiang/archive/2012/05/03/2481288.html)



P.S. 이번 포스팅 겁나 힘들다 흑ㅠ_ㅠ


by kelicia 2014. 5. 29. 03:40


여기서 말하는 '템플릿'은 우리가 기존에 알고 있는 '틀', '골격'의 개념과 같다.

잘 알고 있기때문에 정의부터 정리해보겠다.


* Template Method Pattern :

메소드에서 알고리즘의 골격을 정의한다. 일부 단계는 서브클래스에서 구현하도록 할 수 있다.

템플릿 메소드를 이용하면 알고리즘의 구조는 그대로 유지하면서 

특정 단계만 서브 클래스에서 새로 정의하도록 할 수 있다.



클래스 다이어그램을 보고 이해해보자.


이 다이어그램 가지고는 조금 설명이 부족하다.

코드로 좀 더 자세히 살펴보자.

abstract class AbstractClass
{
  // SubClass 에서 템플릿을 건드리는 것을 방지하지 위해 final 선언
  final void templateMethod(){
    primitiveOperation1();
    primitiveOperation2();
    concreteOperation();
    hook();
  }

  abstract void primitiveOperation1();
  abstract void primitiveOperation2();

  // 오버라이드 불가
  final void concreteOperation(){
    // implementation here
  }

  void hook() { }
}


* 후크(hook) 란?

- 추상 클래스에서 선언되는 메소드지만 기본적인 내용(default)만 구현되어 있거나

아무 코드도 들어있지 않은 클래스 이다. 이렇게 하면 서브 클래스 입장에서는 

hook()를 오버라이드 하여 다양한 위치에서 알고리즘에 끼어들 수 있다.



설명은 다 했으나 일상에서의 예시로 이해해보자.

일반적으로 커피와 차를 타는 것의 방법은 매우 유사하다. (어디까지나 일반적이다, 예민해하지 말자)


커피 : 물 끓이기 -> 커피 우려내기 -> 컵에 붓기 -> 취향껏 우유나 설탕 첨가

차 : 물 끓이기 -> 차 우려내기 -> 컵에 붓기 -> 취향껏 레몬 등 첨가


이미 템플릿 메소드 패턴에 대해 이해했으니 어떻게 디자인 해야할 지 느낌이 온다.

다음은 책의 예제코드를 참고했다.


추상 클래스 : CaffeineBeverage

- 템플릿(prepareRecipe) : 

boilWater() -> brew() -> pourInCup() -> wantCondiments() ? addCondiments() : nothing

- 추상 메소드 : brew(), addCondiments()

- 구상 메소드 : boilWater(), pourInCup()

- hook : wantCondiments() { return false; // default }


서브 클래스1 : Coffee

- 추상 메소드 구현 : brew(), addCondiments()

- hook 오버라이드 : wantCondiments()


서브 클래스2 : Tea

- 추상 메소드 구현 : brew(), addCondiments()



* 헐리우드 원칙 (Hollywood Principle)

- 먼저 연락하지 마세요. 저희가 연락 드리겠습니다.


이 원칙을 활용하면 '의존성 부패(dependency rot)' 를 방지할 수 있다.

어떤 고수준 구성요소가 저수준 구성요소에 의존하고, 그 저수준 구성요소는 다시 고수준 구성요소에

의존하고, 그 고수준 구성요소는 다시 또 다른 구성요소에 의존하고, 그 다른 구성요소는 또 저수준

구성요소에 의존하는 것과 같은 식으로 의존성이 복잡하게 꼬여있는 것을 의존성 부패라 한다.


또한 저수준 구성요소에서 시스템에 접속을 할 수는 있지만 (컴퓨테이션에 참여할 수는 있지만),

언제 어떤 식으로 그 구성요소들을 사용할 지는 고수준 구성요소에서 결정하게 된다.

즉, 헐리우드 원칙과 같은 맥락이다.


- 위의 예제를 보면 알 수 있듯이 Template Method 패턴이 이 원칙을 활용한다는 것을 알 수 있다.

뿐만 아니라 Factory Method, Observer 패턴 등 여러 패턴에서도 이 원칙이 활용된다.



Q. '헐리우드 원칙'과 '의존성 뒤집기 원칙(Dependency Inversion Principle)'은 어떤 관계?

- 의존성 뒤집기 원칙

가능한 구상 클래스 사용을 줄이고 대신 추상화된 것을 사용해야 한다.

- 헐리우드 원칙

저수준 구성요소가 컴퓨테이션에 참여할 수 있으면서도 저수준과 고수준 계층 사이에

의존성을 만들지 않도록 프레임워크 또는 구성요소를 구축하기 위한 기법.


따라서 객체를 분리시킨다는 목표를 공유하고 있으나,

의존성 뒤집기 원칙이 훨씬 더 강하고 일반적인 내용을 담고 있다고 할 수 있다.


Q. 저수준 구성요소는 고수준 구성요소에 있는 메소드를 호출 할 수는 없나?

- 그런 건 아니다. 상속을 통해 호출하는 경우가 종종 있지만 저수준과 고수준 구성요소 사이에

확연하게 드러나는 순환 의존성만은 피하자 !



*  Template Method 패턴 vs. Strategy 패턴 vs. Factory Method 패턴

- Template Method 패턴

. 알고리즘의 개요를 정의해 알고리즘 구조 자체는 그대로 유지한다.

. 알고리즘의 일부 단계를 구현하는 것을 서브클래스에서 처리한다.

. 상속을 이용하기 때문에 코드의 재사용성이 좋아 코드 중복이 거의 없다.

- Strategy 패턴

. 일련의 알고리즘군(群)을 정의하고 그 알고리즘을 서로 바꿔가면서 쓸 수 있게 해준다.

. 바꿔 쓸 수 있는 행동을 캡슐화하고, 어떤 행동을 사용할 지는 서브클래스에 맡긴다.

. 상속과 객체 구성을 통해서 알고리즘 구현을 선택할 수 있게 해준다.

- Factory Method 패턴

. 어떤 구상 클래스를 생성할 지를 서브 클래스에서 결정한다.

. 특화된 Template Method 이다.



* 객체지향의 원칙 :

1) 바뀌는 부분은 캡슐화 한다.

2) 상속보다는 구성을 활용한다.

3) 구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.

4) 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.

5) 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다(OCP).

6) 추상화된 것에 의존해라. 구상 클래스에 의존해서는 안 된다(의존성 뒤집기 법칙).

7) 친한 친구들하고만 이야기한다(최소 지식 원칙).

8) 먼저 연락하지 마세요. 저희가 연락 드리겠습니다(헐리우드 원칙).



* 이 장에서의 정리 :

- 템플릿 메소드가 들어있는 추상 클래스에서는 추상 메소드, 구상 메소드, 후크를 정의할 수 있다.

- 서브 클래스에서 템플릿 메소드에 정의된 알고리즘을 함부로 바꾸게 하고 싶지 않다면 final 로 선언하자. 

구상 메소드 역시 마찬가지이다.

- 후크(hook)는 추상클래스에 있는, 아무 일도 하지 않거나 기본 행동을 정의하는 메소드이다.

서브클래스에서 오버라이드 가능.

- 헐리우드 원칙에 의하면, 저수준 모듈을 언제 어떻게 호출할 지는 고수준 모듈에서 결정하는 게 좋다.



(이미지 출처 : http://www.cnblogs.com/zhuqiang/archive/2012/04/27/2474179.html)


by kelicia 2014. 5. 28. 02:06

1. Adapter Pattern


어댑터 패턴은 우리가 잘 알다시피 일상에서도 자주 볼 수 있다.


위의 이미지가 미국식 플러그라 조금 적응 안 되지만,

어쨌든 해외에 나가서 이처럼 국내 플러그의 인터페이스에 맞게 전기를 사용하려면

어댑터를 이용해서 해외 플러그에 맞게 변환해주어야 한다.



지금 알아보려는 어댑터 패턴도 이와 마찬가지이다.


왼쪽의 기존 시스템과 오른쪽의 업체에서 제공한 클래스 라이브러리.

이 2 개의 코드를 바꾸지 않고 중간에서 호환이 가능하게끔 변환해 주는 녀석을 어댑터라고 한다.


* Adapter Pattern :

한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다.

어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.


어댑터에는 2가지 종류가 있다.

이제 타겟(Target)과 어댑티(Adaptee) 라는 표현이 나올텐데,

타겟은 내가 사용하고 싶은 녀석, 어댑티는 내 의사와 관계없이 제공받은 녀석 이라고 생각하자.


(1) 객체 어댑터 :

- 객체 구성(composition) 을 사용한다.

- Adaptee 뿐만 아니라 그 서브 클래스에 대해서도 어댑터 역할을 할 수 있다는 장점이 있다.


(2) 클래스 어댑터 :

- 다중 상속을 사용한다. 즉, 자바에서는 적용할 수 없는 어댑터라 할 수 있다. (자바는 다중상속 불가)

Adaptee 전체를 다시 구현하지 않아도 된다는 장점이 있다.

- 또한 Adaptee 의 행동을 오버라이드 할 수 있다.

- 특정 Adaptee 클래스에서만 적용된다는 단점이 있다.


* 참고로 한 어댑터에 한 클래스만 감싸지 않아도 된다.

다중 어댑터도 얼마든지 가능하다. 대형 타켓 인터페이스를 구현해야 되는 경우

할 일이 많아진다고 생각할 수도 있지만, 그 경우에 어댑터를 사용하지 않는다고 하면

어차피 클라이언트 코드의 상당 부분을 고쳐야 한다. 결국 할 일이 많아진다는 건 변하지 않는다.


[예제 코드]

자바의 기존 콜렉션 인터페이스 'Enumeration', 새로운 콜렉션 인터페이스 'Iterator'.


<< interface >> Enumeration

 Adapter

<< interface >> Iterator 

hasMoreElements()

nextElement() 

hasNext()

next()

remove() 



Q. Enumeration 인터페이스를 사용하는 구형 코드를 사용해야 하는 경우가 종종 있지만

새로 만들 코드에서는 Iterator 만 사용할 계획이라면 어떻게 어댑터 패턴을 적용할 수 있을까?

public class EnumerationIterator implement Iterator 
{
    Enemeration enum;
    public EnumerationIterator (Enemeration enum) {
        this.enum = enum;
    }
    public boolean hasNext () {
        return enum.hasMoreElements();
    }
    public Object next() {
        return enum.nextElement();
    }
    public void remove() {
        throw new UnsupportedOperationException();
    }
}


Q. 반대로는 적용할 경우에는?

public class IteratorEnumeration implement Enumeration 
{
    Iterator iterator;
    public IteratorEnumeration (Iterator iterator) {
        this.iterator = iterator;
    }
    public boolean hasMoreElements () {
        return iterator.hasNext();
    }
    public Object nextElement() {
        return iterator.next();
    }
}



2. Facade Pattern

인터페이스를 단순화시키기 위해서 인터페이스를 변경한다.

(하나 이상의 클래스)의 복잡한 인터페이스를 깔끔하면서도 말쑥한 퍼사드(겉모양, 외관을 뜻함)로 덮어주자.

너무 설명만 하면 이해가 잘 안되니 예시를 들어보겠다.

집에 홈시어터가 있다고 가정하고 영화를 보려고 할 때, 어떤 절차를 거치게 될까?

생각보다 할 게 많다.


팝콘 기계on -> 팝콘 튀기기 -> 전등dim -> 스크린down -> 프로젝터on -> 프로젝터 input = DVD ->

프로젝터 wideScreenMode -> 앰프on -> 앰프 set for DVD -> 앰프 set Volume -> DVD on -> DVD play ...


켤 때만 해도 더럽게 할 게 많다.

끄는 것도 이만큼은 아니더라도 꽤 번거롭다.


퍼사드를 활용하면 저 위의 모든 순서를 watchMovie() 메소드 하나로 묶을 수 있다.

그러면 클라이언트는 이 메소드 하나만 부르면 되니 간편하다.


* Facade Pattern :

어떤 서브시스템의 일련의 인터페이스에 대한 통합된 인터페이스를 제공한다.

퍼사드에서 고수준 인터페이스를 정의하기 때문에 서브시스템을 더 쉽게 사용할 수 있다.


Q. 퍼사드에서 서브시스템 클래스들을 캡슐화하면 저수준 기능을 원하는 클라이언트에서는

어떻게 서브시스템 클래스에 접근할까?

- 퍼사드 클래스에서는 서브시스템 클래스들을 캡슐화 하지 않는다 !

그저 서브시스템의 기능을 사용할 수 있는 간단한 인터페이스를 제공할 뿐이다.

클라이언트에서 특정 인터페이스가 필요하다면 서브시스템 클래스를 그냥 사용하면 된다.

이와 같은 면은 퍼사드 클래스의 대표적인 장점이라 할 수 있다.

단순화된 인터페이스를 제공하면서도, 클라이언트에서 필요로 한다면 서브시스템의 모든 기능을

사용할 수 있도록 해준다는 것이 장점이다. 또한 클라이언트 구현과 서브시스템을 분리시킬 수 있다.


Q. 한 서브시스템에 퍼사드는 하나씩만 생성이 가능한가?

- 퍼사드의 갯수에는 제한이 없다.



3. Adapter 패턴 vs. Facade 패턴 vs. Decorator 패턴


각 패턴의 용도들을 비교해 차이점을 알아보자 !


* Adapter 패턴 : 한 인터페이스를 다른 인터페이스로 변환 (for 호환성)

인터페이스를 변경해서 클라이언트에서 필요로 하는 인터페이스로 적응시키기 위한 용도.


* Facade 패턴 : 인터페이스를 간단하게 바꿈 (for 간편함)

어떤 서브시스템에 대한 간단한 인터페이스를 제공하기 위한 용도.


* Decorator 패턴 : 인터페이스를 바꾸지 않고 책임(기능)만 추가함 (for 기능 추가)

객체를 감싸서 새로운 행동을 추가하기 위한 용도.



4. 최소 지식 원칙 (Principle of Least Knowledge) 또는 데메테르의 법칙 (Law of Demeter)

- 참고로 둘 다 같은 말이다.

- 정말 친한 친구들하고만 얘기하라.

- 최소 지식 원칙에 따르면 객체 사이의 상호작용은 될 수 있으면 아주 가까운 '친구' 사이에서만 허용하는 것이 좋다 !

- 이를 잘 따르면 여러 클래스들이 복잡하게 얽힌 시스템의 한 부분을 변경했을 때, 다른 부분까지

줄줄이 고쳐야 되는 상황을 미리 방지할 수 있다.


- 이 원칙을 지키기 위한 가이드 라인 : 다음 4 종류의 객체의 메소드만 호출하면 된다.

(1) 객체 자체

(2) 메소드에 매개변수로 전달된 객체

(3) 그 메소드에서 생성하거나 인스턴스를 만든 객체

(4) 그 객체에 속하는 구성요소

구성요소는 인스턴스 변수에 의해 참조되는 객체를 의미. 즉, 'A에는 B가 있다' 관계에 있는 객체.


- 위 가이드 라인에 따르면 다른 메소드를 호출해서 리턴 받은 객체의 메소드를 호출하는 건 바람직하지 않다고 한다.


- 객체들 사이의 의존성을 낮추고, 소프트웨어 관리가 더 용이해질 수는 있으나 단점도 있다.

다른 구성요소에 대한 메소드 호출을 처리하기 위해 'Wrapper' 클래스를 더 만들어야 할 수도 있다.

그러다 보면 시스템이 더 복잡해지면서 개발 시간이 늘어나고 성능 저하를 불러일으키는 요인이 될 것이다.


[예제 코드]


- 원칙을 따르지 않은 경우 -

public float getTemp() {
    Thermometer thermometer = station.getThermometer();
    return thermometer.getTemperature();
}

station 으로부터 객체를 받은 다음 그 객체의 메소드를 호출.

즉, 위에 밑줄친 '다른 메소드를 호출해서 리턴 받은 객체의 메소드를 호출하는 형태'이므로 바람직하지 않다.

return station.getThermometer().getTemperature(); 와 별반 다를게 없다.


- 원칙을 따르는 경우 -

public float getTemp() {
    return station.getTemperature();
}

Station 클래스에 thermometer 에 요청하는 메소드를 추가하므로서 의존해야 하는 클래스 갯수를 줄인다.

가이드 원칙 (3) 에 따라 인스턴스를 만든 객체의 메소드를 호출 하였다.

public float getTemp() {
    Thermometer thermometer = station.getThermometer();
    return getTempHelper(thermometer);
}

public float getTempHelper(Thermometer thermometer) {
    return thermometer.getTemperature();
}

뭔가 교묘히 속이려고 하는 듯한 느낌의 코드이지만...

이 코드는 가이드 원칙 (2) 에 따라 원칙에 위배되지 않는 코드다.


위에 'return station.getThermometer().getTemperature();' 랑 뭐가 다르냐?! 할 수도 있지만....

다르다. 적어도 난 다르다고 생각한다.

사실 책에는 위의 코드가 원칙에 위배되지 않았다고만 하지 정작 자세한 이유는 다루지 않았다.


그래서 그냥 내 생각을 적어보자면....음..아마도,

객체 사이의 의존성을 낮추는 관점에서 봤을 때

'station.getThermometer() 로부터 리턴받은 Thermometer 객체의 메소드 호출' 과

'Thermomter 객체의 메소드 호출' 의 차이이지 않을까 하고 생각해본다. 

- 몇 개의 클래스하고 연결이 되어 있는지 생각해보면 이해할 수 있을 것이다 -



* 객체지향의 원칙 :

1) 바뀌는 부분은 캡슐화 한다.

2) 상속보다는 구성을 활용한다.

3) 구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.

4) 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.

5) 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다(OCP).

6) 추상화된 것에 의존해라. 구상 클래스에 의존해서는 안 된다(의존성 뒤집기 법칙).

7) 친한 친구들하고만 이야기한다(최소 지식 원칙).



* 이 장에서의 정리 :

- 기존 클래스를 사용하려고 하는데 인터페이스가 맞지 않으면 어댑터를 쓰자.

- 큰 인터페이스 or 여러 인터페이스를 단순화 시키거나 통합시켜야 되는 경우에는 퍼사드를 쓰자.


- 어댑터는 인터페이스를 클라이언트가 원하는 인터페이스로 바꿔주는 역할을 한다.

- 퍼사드는 클라이언트를 복잡한 서브시스템과 분리시켜주는 역할을 한다.


- 어댑터를 구현할 때는 Target 인터페이스의 크기와 구조에 따라 코딩해야 할 분량이 결정된다.

- 퍼사드 패턴에서는 서브시스템을 가지고 퍼사드를 만들고, 실제 작업은 서브클래스에 맡긴다.


- 어댑터 패턴에서는 객체 어댑터 패턴 vs. 클래스 어댑터 패턴(다중상속必)이 있다.

- 어댑터는 다중 어댑터로 만들 수 있고, 퍼사드는 한 서브시스템에 여러 개 만들어도 된다.


- 어댑터, 퍼사드 모두 인터페이스를 바꿔주는데 !

Adapter : 인터페이스를 변환

Facade : 인터페이스를 통합/단순화 시킴

+ Decorator : 인터페이스를 바꾸지 않고, 객체를 감싸서 새로운 기능을 추가할 수 있다.



(이미지 출처 :

http://librairie.immateriel.fr/fr/read_book/9780596007126/ch07

http://www.cnblogs.com/zhuqiang/archive/2012/04/24/2468240.html

http://www.cnblogs.com/zhuqiang/archive/2012/04/24/2468323.html)

by kelicia 2014. 5. 27. 21:16


이번 장에서는 한 차원 높은 단계의 캡슐화를 다루는데,

바로 메소드 호출을 캡슐화하는 것이다. 


이를 활용해서 스케줄링, 로그 기록 시스템을 디자인할 수도 있고

취소(undo) 기능을 구현하기 위해 재사용할 수도 있다.


* Command Pattern :

요구 사항을 객체로 캡슐화 할 수 있으며, 매개변수를 써서 여러가지 다른 요구 사항을 집어넣을 수도 있다.

또한 요청 내역을 큐에 저장하거나 로그로 기록할 수도 있고 작업 취소 기능도 지원이 가능하다.

요청을 하는 객체그 요청을 수행하는 객체를 분리하고 싶다면 이 패턴을 사용하자!



Command 패턴을 이해하기 위해 간단하게 예를 들어보겠다.

다음의 그림은 식당에서 클라이언트의 주문이 실제 수행되는 과정이다.

(출처 : http://www.prayer-laputa.com/blog/archives/370)


웨이트리스는 클라이언트로부터 주문(Order)를 받아 - takeOrder()

주방장에게 그 주문을 전달해준다. - orderUp()

그러면 주방장은 주문서에 따라서 음식을 준비할 것이다. - make...()


여기서 주문서가 주문한 메뉴를 캡슐화 한다는 것을 숙지하자.


그리고 위 그림을 읽는 방법을 설명하자면

1) Client 는 Order 의 createOrder() 를 호출.

2) Order 는 웨이트리스의 takeOrder() 를 호출.

3) 웨이트리스는 Order 의 orderUp() 을 호출.

4) Order 는 주방장의 make...() 을 호출.

- 3) 이 이해가 잘 안 갈 수도 있지만 너무 신경안써도 된다. 흐름만 이해하자.



위 그림을 어느 정도 이해한 다음에

Command 패턴 다이어그램으로 넘어가보자.


위에서 보았던 다이어그램과 매우 유사하다는 것을 느낄 수 있다.


이해를 위해 설명을 하자면,

① Client 는 Command 객체를 생성한다.

② Invoker 에 ① 에서 생성한 Command 객체를 저장하기 위해 Client 는 setCommand() 를 호출한다.

③ 차후 Client 는 Invoker 에게 그 Command 객체를 실행시켜달라고 요청한다.




- Command 패턴의 흐름을 이해했다면 예제 코드로 넘어간다.

책에서는 가정 내에서 사용하는 전자제품들을 컨트롤할 수 있는 리모컨 API 를 디자인하는 예를 들었다.

아래 Class Diagram 안 보이면 Click.

(참고로 나는 리모컨에 Undo 기능이 있는 건 처음본다)


Client :

Concrete Command 를 생성하고 Receiver를 설정한다.


- Invoker :

Command 들을 관리하고 있고 Command 의 execute() 를 호출해서 Command 객체에게

특정 작업을 수행해 달라고 요청을 한다.


- Command :

모든 Command 객체에서 구현되어야 하는 인터페이스.


- Concrete Command :

execute() 에서 Receiver 에 있는 Action 들을 호출해 요청받은 작업을 수행하도록 한다.

undo() 의 예를 들면, LightOnCommand 의 경우 undo() 의 내용은 light.off() 정도 될 것이다.


- Receiver :

요구 사항을 처리하기 위해 어떤 일을 수행해야 하는지 알고 있다.


- Macro Command :

여러 Command 들을 한번에 실행시킬 수 있는 새로운 종류의 Command. 

아래의 예제 코드를 보자.

public class MacroCommand implements Command {
    Command[] commands;
    public MacroCommand(Command[] commands){
        this.commands = commands;
    }
    public void execute() {
        for ( int i=0; i < commands.length; i++ ){
            commands[i].execute();
        }
    }
}

- Null 객체 :

Client 에서 따로 null 처리를 하지 않아도 되도록 만들어 놓은 객체.

execute(), undo() 모두 아무 일도 하지 않는다. 여러 디자인 패턴에서 유용하게 쓰인다고 한다.

public void onButtonWasPushed(int slot) {
    if ( onCommands[slot] != null ) {
         onCommands[slot].execute(); 
    }
}

Null 객체를 활용하면 클라이언트는 위의 코드처럼 작성할 필요가 없게된다.



Invoker 인 RemoteControl 클래스의 코드를 자세히 알고 싶다면 

이 포스팅 글 상단에 걸어놓은 링크를 따라가면 된다. 중국어의 압박이 있긴하지만 코드는 읽을 수 있으니까.



* Command 패턴 활용 :

(1) 요청을 Command 객체에 캡슐화해서 작업 큐(Queue) 에 저장하기

큐의 한쪽은 커맨드를 추가, 다른 한쪽은 커맨드를 처리하기 위한 스레드(Thread)들이 대기하고 있다고 하자.

각 스레드에서는 우선 커맨드의 execute() 메소드를 호출하고, 이 작업을 완료하면 커맨드 객체를

보내버리고 새로운 커맨드 객체를 가져오는 작업을 하게 된다.


위와 같은 방법으로 스케줄러, 스레드 풀, 웹 서버 등에 활용할 수 있다.


(2) 요청을 로그(Log)에 기록하기

어떤 애플리케이션에서는 모든 행동을 기록해놨다가 그 애플리케이션이 다운되었을 경우,

나중에 그 행동들을 다시 호출해서 복구를 할 수 있도록 해야 한다.

이를 위해 Command 인터페이스에 execute(), undo() 이외에 store(), load() 를 추가하자.


어떤 명령을 실행하면서 디스크에 실행 히스토리를 기록해 애플리케이션이 다운되면

커맨드 객체를 다시 로딩하고 execute() 메소드들을 자동으로 순서대로 실행하면 된다.


매번 저장하기 힘든 경우에는 마지막 체크 포인트 이후로 한 모든 작업을 저장한 다음에

다운되었을 때 기존 체크 포인트에 최근 수행된 작업을 다시 적용하는 방법을 사용하면 된다.


위와 같은 테크닉을 확장해서 DB의 트랜잭션을 활용해 commit, rollback 연산을 구현할 수 있을 것이다.



* 객체지향의 원칙 :

1) 바뀌는 부분은 캡슐화 한다.

2) 상속보다는 구성을 활용한다.

3) 구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.

4) 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.

5) 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다(OCP).

6) 추상화된 것에 의존해라. 구상 클래스에 의존해서는 안 된다(의존성 뒤집기 법칙).



* 이 장에서의 정리 :

- Command 패턴을 이용하면 요청을 하는 객체와 그 요청을 수행하는 객체를 분리할 수 있다.

- 또한 작업 취소(Undo) 기능을 지원할 수 있고, 로그나 트랜잭션 시스템 구현에도 활용된다.

- Command 객체는 Action 을 수행하는 Receiver 를 캡슐화 한다.

- Command 객체의 execute() 는 Receiver 의 Action 을 호출한다.

- Macro Command 는 여러 개의 Command 를 한꺼번에 호출할 수 있게 해주는 간단한 방법이다.

Macro Command 에서도 어렵지 않게 Undo() 기능을 지원할 수 있다.


by kelicia 2014. 5. 21. 20:46

- 이번 장의 특성상 따로 Diagram 을 생성하지 않았다. 그리고 책 내용에 좀 더 충실하게 포스팅해봤다. -


* Singleton Pattern

해당 클래스의 인스턴스가 오직 하나 만들어지고,

어디서든지 그 인스턴스에 전역 접근할 수 있도록 하기 위한 패턴이다.


- 어떤 상황에서 사용될까?

스레드풀이라든가 캐시, 대화상자, 사용자 설정 또는 레지스트리 설정을 처리하는 객체,

로그 기록용 객체, 프린터나 크래픽 카드 같은 디바이스를 위한 디바이스 드라이버 같은 걸 예로 들 수 있다.

이런 상황에서 2개 이상의 인스턴스를 만들게 된다면, 프로그램이 이상하게 돌아간다든가

자원을 불필요하게 잡아먹는다든가 일관성 없는 결과를 초래할 수 있다.


- 전역변수(Global Variable)도 방법이 될 수 있지 않나?

전역변수에 객체를 대입하면 애플리케이션이 시작될 때 객체가 생성될 것이다.

그런데 여기서 그 객체가 자원을 많이 차지한다고 가정했을 때, 그 객체가 애플리케이션 종료 시까지

한 번도 사용되지 않는다면 괜히 자원만 잡아먹는 쓸데없는 녀석이 될 수 있다.

Singleton 패턴의 경우에는 필요할 때만 객체를 생성할 수가 있다.



< 고전적인 구현법 >

public class Singleton {
    private static Singleton uniqueInstance;
    private Singleton() { }
    public static Single getInstance() {
        if ( uniqueInstance == null ) {
            // 인스턴스가 필요한 상황이 닥치기 전에는 아예 인스턴스를 생성하지 않는다.
            // 이 기법을 '게으른 인스턴스 생성(Lazy instantiation)' 이라 한다.
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

클래스의 객체가 자원을 많이 잡아먹는 경우에는 위와 같이 '게으른 인스턴스 생성' 기법이 꽤 유용하다.

위 코드를 읽을 때는 접근한정자와 static 키워드를 주의깊게 읽자.



하지만 주의해야 할 점이 있다. 

* 고전적인 방법이 다중 스레드 환경에서도 아무 문제 없을까?


1번 스레드 

2번 스레드 

uniqueInstance 값 

 public static Single getInstance() {

 

null

  public static Single getInstance() {

 

  if ( uniqueInstance == null ) {

 

 

   if ( uniqueInstance == null ) {

 

   uniqueInstance = new Singleton();

 

object 1 

   return uniqueInstance;

 

 

    uniqueInstance = new Singleton(); 

object 2

 

   return uniqueInstance;

 


위의 경우, 객체가 2 개가 만들어져 Singleton 의 의미가 없어지는 문제가 발생한다.

어떻게 해야할까?

public static synchronized Singleton getInstance() {
    if ( uniqueInstance == null ) {
        uniqueInstance = new Singleton();
    }
    return uniqueInstance;
}

synchronized 키워드를 추가하면, 한 스레드가 사용을 끝내기 전까지 다른 스레드는 기다려야 한다. 

다시 말해서 2개 이상의 스레드가 동시에 실행시키는 일은 발생하지 않는다.

그러나 ! !

책에 의하면... 메소드를 동기화하면 성능이 100배 정도 저하된다는 것은 기억하라고 한다.



* Singleton 패턴에서의 3 가지 해법 :

(1) getInstance() 의 속도 이슈가 그렇게 중요하지 않다면 그냥 동기화를 해도 된다.

- 애플리케이션에 큰 부담을 주지 않는다면 바로 위의 코드처럼 놔둬도 된다.

(2) 인스턴스를 필요할 때 생성하지 말고 처음부터 만들어 버린다.

- 애플리케이션에서 반드시 Singleton의 인스턴스를 생성하고 항상 사용한다면 or

 인스턴스를 실행중에 수시로 만들고 관리하기 성가시다면 정적으로 초기화 해버린다.

 이 방법은 JVM 에서 유일한 인스턴스를 생성하기 전까진 그 어떤 스레드도 uniqueInstance 에 접근 불가.

public class Singleton {
    private static Singleton uniqueInstance = new Singleton();
    private Singleton() { }
    public static Singleton getInstance() { return uniqueInstance; }
}

(3) DCL(Double-Checking Locking) 을 써서 getInstance() 에서 동기화되는 부분을 줄인다.

- 인스턴스가 생성되어 있는지 확인한 다음, null 인 경우만 동기화 할 수 있기때문에

 처음에만 동기화를 하고 이후에는 동기화하지 않아도 된다. 

 이 방법으로 오버헤드를 극적으로 줄여 속도를 향상시킬 수 있다. 

- volatile 키워드를 사용하면 다중 스레딩을 쓰더라도 uniqueInstance = new Singleton(); 의 과정이

 올바르게 진행될 수 있다.

public class Singleton {
    private volatile static Singleton uniqueInstance;
    private Singleton() { }
    public static Singleton getInstance() { 
        if ( uniqueInstance == null ) {
            synchronized ( Singleton.class ) {
                if ( uniqueInstance == null ) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance; 
    }
}

단, (3) 방법은 자바 1.4 이전 버전에는 사용할 수 없다고는 하나 

현재 14.05.20 기준으로는 버전 1.8 까지 나왔으니 크게 염려할 필요는 없지않나 싶다.


3가지 방법 모두가 다중 스레딩 환경에서 발생하는 문제를 해결할 수 있다.

하지만 속도 이슈와 자원 문제, 구현 시나리오를 염려해두면서 적절한 해법을 선택하도록 하자.



* 객체지향의 원칙 :

1) 바뀌는 부분은 캡슐화 한다.

2) 상속보다는 구성을 활용한다.

3) 구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.

4) 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.

5) 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다(OCP).

6) 추상화된 것에 의존해라. 구상 클래스에 의존해서는 안 된다(의존성 뒤집기 법칙).



이 장에서의 정리 :

- 어떤 클래스에 Singleton 패턴을 적용하면 애플리케이션에 그 클래스의 인스턴스가

 최대 1개까지만 있도록 할 수 있다. 또한 그 유일한 인스턴스에 전역 접근할 수 있다.

- 사실 모든 애플리케이션은 다중 스레딩을 쓸 수 있다고 생각해야하기 때문에 고전적인 구현은 되도록 피하자.

- 클래스 로더가 여러 개 있으면 싱글턴이 제대로 작동하지 않고 여러 개의 인스턴스가 생길 수 있다.

 (클래스 로더마다 서로 다른 네임스페이스를 정의하기 때문이다. 

 클래스 로더가 2개 이상일 때, 각 클래스 로더마다 한번씩 같은 클래스를 여러 번 로딩할 수도 있다. 

 그러니 클래스 로더를 직접 지정해서 이 문제를 피할 수 있다고 한다.)


※ 사실 클래스 로더가 뭔지 잘 모르겠다.


by kelicia 2014. 5. 20. 00:33
| 1 2 |