Telescoping Parameter 패턴은 "켄트 벡의 구현 패턴"에도 언급되었던 패턴이다.

기본적으로 이 패턴은 다수의 매개 변수를 가진 함수의 문제점을 해결하기 위한 패턴이다.


보통 생성자가 다수의 매개 변수 개수를 달리 하면서 생성이 가능한 경우에 주로 사용된다.


실제로도 많이 쓰이는 패턴이라서 Java 라이브러리의 ServerSocket 함수를 가지고 설명을 해볼까 한다.


우선 API를 기준으로 보면 Telescoping Parameter 패턴의 외형은 다음과 같다.


public ServerSocket(int port);

public ServerSocket(int port, int backlog);

public ServerSocket(int port, int backlog, InetAddress bindAddr); 


이처럼 매개 변수가 여럿이고 매개변수의 기본 값이 있는 경우에 인자 개수가 다른 API를 제공해 준다. 이렇게 하면 사용하는 입장에서는 필요에 따라 짧거나 긴 매개 변수를 가진 API를 호출할 수 있다. 이 모양이 마치 망원경을 접었다 폈다 하는 모양과 비슷하다고 해서 붙여진 이름이다.



내부 구현 시 고려 사항

이 패턴에 대해서는 다음과 같은 사항을 잘 생각해 봐야 한다. 이는 내부 구현에 있어서 지켜야 할 중요한 부분이다.


저 함수들은 모두 backlog 변수나 bindAddr 변수에 대한 기본 값이 있다는 전제 하에서 작성되었다. 따라서 결과적으로는 세 함수 모두 세 개의 파라메터 모두에 대한 설정을 하게 될 것이다.


이런 상황에서 매개 변수로 하려는 일은 동일할텐데 이를 각 함수에 구현하면 중복 구현의 문제가 발생한다. 따라서 아래와 같은 형태로 내부를 구현해 주어야 한다.


public ServerSocket(int port){

    this(port, 50, null);

}


public ServerSocket(int port, int backlog){

    this(port, backlog, null);

}


/* 결국 어떤 함수를 사용해도 아래 함수가 호출되게 된다 */

public ServerSocket(int port, int backlog, InetAddress bindAddr){

    setImpl();

    bind(new InetSocketAddress(bindAddr, port), backlog);

} 


실제 코드보다는 좀 더 간단하게 변경하였다. 매개 변수가 한 개인 경우 남은 두개의 기본 값을 채워 3개짜리 함수를 호출한다. 두 개짜리고 마찬가지로 3개짜리 함수를 호출하는 것으로 할 일을 마친다. 3개짜리 함수만 실제 필요한 동작을 수행하게 된다.



코드 중복의 방지

객체의 외부에서 객체로 변수 값을 직접 전달하는 기본 방식은 setter를 이용하는 것이다. 하지만 종종 생성자를 이용하여 변수를 초기에 셋팅하게 하는 것이 좋을 때가 있다. 이렇게 되면 생성자와 setter에 의해 변수가 셋팅되게 된다. 변수 셋팅은 아주 중요한 작업이다. 이 경우 생성자 내부에서는 setter를 호출해 주는 것이 좋다.


안티 패턴

class Example {

    int data;   

   

    public void Example(int data){

        this.data = data;

    }

   

    public void setData(int data){ this.data = data; }

}


이렇게 구현했을 경우 setData 내부에서 data가 새로 설정된 이후 동작을 변경해버리면 생성자를 통해 data를 변경했을 때에는 이것이 반영되지 않는다. 따라서 아래와 같이 구현하는 것이 좋다.


좋은 구현 방식

class Example {

    int data;   

   

    public void Example(int data){

        setData(data); // 이렇게 해야 setData() 함수 내부의 변경에 안전하다.

    }

   

    public void setData(int data){ this.data = data; }

}



이 문제에 대한 고려가 Telescoping Parameter 패턴의 구현에도 고스란히 반영되어 있다.


단순히 자꾸 함수를 호출하려는 것으로만 보일지 모르겠지만 실제로는 모든 함수들은 모든 매개 변수가 설정되는 것을 기대하고 있다. 따라서 매개 변수로 내부 변수를 설정하는 코드(예제에서는 bind() 함수를 호출하는 부분)는 한 곳으로 몰아 주기 위해 this 함수를 호출하는 것이다. 


Telecoping Parameter 패턴을 구현할 때에는 이 점을 꼭 염두해 두어야 한다.

'5.디자인패턴' 카테고리의 다른 글

Pluggable Selector 패턴  (1) 2016.08.21
State 패턴  (0) 2016.08.15
Strategy 패턴  (0) 2016.08.15
interface -abstract class - concrete class 패턴(인터페이스 구현 중복 해결 패턴)  (2) 2016.08.10
Enum Factory Method 패턴  (0) 2016.08.07
Posted by 이세영2
,

interface - abstract class - concrete class 패턴은 인터페이스 구현 시 자주 발생하게 되는 중복 구현을 방지하는 패턴이다.


해결하고자 하는 문제

- 구현해야 할 클래스에 대한 인터페이스가 이미 정해진 상태이다.

- 정해진 인터페이스를 통해 구현해야 할 클래스가 여러개이다.

- 인터페이스 API 중 일부가 모두 같은 구현부를 같게 된다. 이 구현부의 중복을 없애야 한다.


해결 방법

인터페이스 구현 시 구현해야 할 함수 중에서 중복되는 함수들을 abstract class에 넣음으로써 곧바로 인터페이스를 구현하려고 할 때 발생할 수 있는 중복 구현을 방지할 수 있다.


간단한 예제를 통해 interface - abstract class - concrete class가 어떻게 쓰이는지 살펴보자.


우선 각종 도형들을 그리는 소프트웨어를 개발한다고 하자. 삼각형 사각형 원 등 다양한 도형이 있는데 이들 도형은 모두 표면(surface)과 라인(line)으로 그려진다고 가정해 보자. 이런 경우 모든 도형의 공통 요소인 표면 색깔 지정, 라인 색깔 지정, 도형 그리기와 같은 API를 생각해 볼 수 있다. 이들은 모든 도형에 공통이므로 공통 인터페이스를 선언하는 것으로 구현을 시작해 보겠다.


interface IShape{

    public void setSurfaceColor(Color surfaceColor);

    public void setLineColor(Color lineColor);

    public void draw();

}



그러면 인터페이스가 정의 되었으니 도형을 구현해 볼 차례이다. 먼저 Rectangle을 만들어 보자.


class Rectangle implements IShape{

    private Color surfaceColor;

    private Color lineColor;

    public void setSurfaceColor(Color surfaceColor){

        this.surfaceColor = surfaceColor;

    }

    public void setLineColor(Color lineColor){

        this.lineColor = lineColor;

    }

    public void draw(){

        System.out.println("draw Rectangle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

}



현재까지는 크게 문제는 없어 보인다. 표면 색깔과 라인 색깔을 지정할 수 있는 인터페이스를 구현했고, 도형을 그리는 draw() 함수도 구현했으니 실제로 잘 그려지게 될 것이다. 이렇게 IShape 인터페이스가 제공하는 모든 API를 구현했으니 이제 다른 도형도 만들어 보겠다. Circle을 만들어 보자.


class Circle implements IShape{

    private Color surfaceColor;

    private Color lineColor;

    public void setSurfaceColor(Color surfaceColor){

        this.surfaceColor = surfaceColor;

    }

    public void setLineColor(Color lineColor){

        this.lineColor = lineColor;

    }

    public void draw(){

        System.out.println("draw Circle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

}


이제 문제점이 눈에 보일 것이다. draw() 함수는 각 도형이 다르겠지만 setSurfaceColor()와 setLineColor()는 서로 동일하다. 하지만 도형이라면 위의 두 인터페이스도 제공해야 하는 것이 맞다. 그러면 계속 중복된 코드들을 만들어 가면서 구현을 완료하는 것이 옳을까?


이런 문제점을 해결할 수 있는 방법이 인터페이스(interfac)와 구체 클래스(concrete class) 중간에 추상 클래스(abstract class)를 하나 두고 공통되는 부분을 모아 두는 것이다. 위의 예제에서 공통된 부분을 추상 클래스로 뽑아 내면 다음과 같아질 것이다.


abstract class Shape implements IShape{

    protected Color surfaceColor;

    protected Color lineColor;

    public void setSurfaceColor(Color surfaceColor){

        this.surfaceColor = surfaceColor;

    }

    public void setLineColor(Color lineColor){

        this.lineColor = lineColor;

    }

} 


우선 우리가 구현하고자 하는 구체 클래스를 외부에서 사용할 때는 IShape 타입이어야 한다. 따라서 일단 추상 클래스가 IShape을 구현하도록 선언한다. 그리고 구체 클래스에서 발생한 중복 코드들을 추상 클래스로 이동시킨다. 주의할 것은 private 변수들을 protected로 바꾸어 주어야 한다는 것이다. 그렇게 해야 구체 클래스들이 이 Shape 추상 클래스를 상속 받았을 때 그 변수들을 사용할 수 있게 된다.


그리고 한가지 주목할 것은 IShape이 제공하는 인터페이스 중에서 void draw() 인터페이스를 구현하지 않았다는 점이다. 추상 클래스의 경우 상속 받은 인터페이스의 일부만 구현해도 컴파일에러가 발생하지 않는다. 그 이유는 인터페이스에서 선언한 API의 타입은 항상 abstract public 타입이기 때문이다. 잠깐 옆길로 새서 interface의 실제 타입을 밝혀보면 다음과 같다.


interface Example{

    void api();

}

abstract class Example{

    abstract public void api();

}


위의 두 선언은 선언적으로는 동등하다. interface는 실체화 할 수 없는 추상 클래스(abstract class)와 같고, api()는 실제로는 abstract public 타입의 함수이다. 다만, 인터페이스는 다중 상속이 가능하지만 추상 클래스는 단 한 개의 클래스만 상속 가능하다는 점에서 실질적으로는 같지 않다. 어쨌든 개념적으로 보면 인터페이스는 추상 클래스의 "특수 케이스"라고 이해할 수 있다.


그러면 이제 본론으로 다시 넘어가서 draw() 함수를 추상 클래스에서 구현하지 않아도 에러가 나지 않은 이유를 알 수 있을 것이다. 추상 클래스는 추상 메소드를 선언할 수 있는 클래스이다. IShape에서 선언된 draw() 함수는 추상 메소드이고, Shape 클래스가 이를 상속 받았으므로 draw() 추상 메소드가 선언된 셈이다. 추상 클래스가 추상 메소드를 선언하는 것은 문법에 위배되지 않기 때문에 구현체가 없어도 전혀 문제가 없는 것이다.


그럼 이제 Rectangle 클래스와 Circle 클래스가 어떻게 바뀌었는지 보자.


class Rectangle extends Shape{

    public void draw(){

        System.out.println("draw Rectangle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

}


class Circle  extends Shape{

    public void draw(){

        System.out.println("draw Circle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

}


자 일단 중복된 부분이 모두 제거되었다. 그 이유는 IShape을 implements 하던 것을 Shape을 extends 하는 것으로 바꿈으로써 setSurfaceColor() 함수와 setLineColor() 함수의 구현부를 상속 받았기 때문이다. 이를 통해서 두 클래스는 서로 다른 부분인 draw() 함수만을 구현하도록 바뀌었다.


그러면 최종적인 모습이 어떤지 한번에 살펴보자.


구현 결과

interface IShape{

    public void setSurfaceColor(Color surfaceColor);

    public void setLineColor(Color lineColor);

    public void draw();

}


abstract class Shape implements IShape{

    protected Color surfaceColor;

    protected Color lineColor;

    public void setSurfaceColor(Color surfaceColor){

        this.surfaceColor = surfaceColor;

    }

    public void setLineColor(Color lineColor){

        this.lineColor = lineColor;

    }

}


class Rectangle extends Shape{

    public void draw(){

        System.out.println("draw Rectangle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

}


class Circle  extends Shape{

    public void draw(){

        System.out.println("draw Circle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

} 


위와 같이 되었다 중복 코드가 없는 깔끔한 모습이다. 그러면 사용 방법에 있어서는 어떨까? Rectangle 클래스와 Circle 클래스를 외부에서는 IShape 타입으로 잘 인식 할 수 있을까? 다음과 같이 테스트를 구현해 보겠다.


테스트 함수

public static void main(String[] args) {

   IShape shape = new Rectangle();

   shape.setSurfaceColor(Color.BLACK);

   shape.setLineColor(Color.WHITE);

   shape.draw();


   shape = new Circle();

   shape.setSurfaceColor(Color.WHITE);

   shape.setLineColor(Color.BLACK);

   shape.draw();

} 


모든 API를 한번씩 호출해보도록 작성했고, 각 구체 클래스들을 IShape 타입으로 지칭하도록 했다. 물론 오류 없이 잘 동작하고 다음과 같은 결과를 출력해 냈다.


출력 결과

draw Rectangle with

java.awt.Color[r=0,g=0,b=0]

java.awt.Color[r=255,g=255,b=255]

draw Circle with

java.awt.Color[r=255,g=255,b=255]

java.awt.Color[r=0,g=0,b=0]


이처럼 아주 잘 동작하는 것을 확인 할 수 있다.


실제로 외부에서 제공된 인터페이스를 이용하여 구현을 하다보면 중복 코드가 자주 발생하게 된다. 같은 인터페이스를 상속 받는다는 것은 상속 받아 구현될 구체 클래스들이 유사점을 많이 가지고 있다는 것을 암시한다. 따라서 구현을 진행하다 보면 자연스럽게 중복된 코드들이 자주 만들어지게 된다.


이런 경우에 이 패턴 처럼 중간에 추상 클래스 하나를 만들어 상속 받도록 하면 중복 코드들을 제거할 수 있다. 중복된 부분들이 제거된 구체 클래스들은 구체 클래스들 간에 서로 다른 부분들만 구현하여 가지고 있게 되므로 코드에 대한 이해 속도도 빨라진다는 장점이 있다.


혹시라도 인터페이스 구현으로 인해 중복이 많이 발생하게 되었다면 이 패턴을 이용해 보자.

'5.디자인패턴' 카테고리의 다른 글

Pluggable Selector 패턴  (1) 2016.08.21
State 패턴  (0) 2016.08.15
Strategy 패턴  (0) 2016.08.15
Telescoping Parameter 패턴  (0) 2016.08.13
Enum Factory Method 패턴  (0) 2016.08.07
Posted by 이세영2
,

Enum Factory Method 패턴은 Factory Method 패턴의 단점을 보완하기 위한 패턴이다.

기본적으로 Factory Method 패턴과 마찬가지로 객체의 생성을 담당하는 메소드를 구현하는 패턴이다. 이와 함께 Factory Method를 구현한 객체를 생성하기 위해 Singleton을 사용해야 하는 문제점을 Enum의 특성을 이용하여 해결한다.


Enum Factory Method 패턴(Java에서만 가능)

public enum EnumFactoryMethod {

    RECTANGLE{

        protected Shape createShape(){return new Rectangle();}

    }

    ,CIRCLE{

        protected Shape createShape(){return new Circle();}

    }

    ;

    public Shape create(Color color){

        Shape shape = createShape();

        shape.setColor(color);

        return shape;

    }

    abstract protected Shape createShape();

    public static void main(String[] args) {

        EnumFactoryMethod.RECTANGLE.create(Color.BLACK);

        EnumFactoryMethod.CIRCLE.create(Color.WHITE);

    }

}



Enum 타입 자체가 public static final 이기 때문에 생성을 위임 받은 객체에 대한 중복 생성이 불가하고, Singleton을 굳이 구현하지 않아도 단일한 객체만 생성됨이 보장된다.


Enum의 이러한 특성은 다른 패턴들에도 응용이 될 수 있는데 이는 이후 포스팅을 통해 살펴보도록 하겠다.

Posted by 이세영2
,