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
,

Immutable(불변) 객체는 수학과 금융 등에서 자주 쓰이는 개념이다.

이 글에서는 켄트 벡의 저서 테스트 주도 개발(TDD)과 켄트 벡의 구현 패턴에 나오는 Money 객체를 예로 들어 설명하도록 하겠다.


정의

Immutable(불변) 객체는 객체 생성 이후 값이 변하지 않는 객체를 말한다. 이는 생성자를 이용한 값 설정 이외의 어떤 갱신 수단도 제공하지 않는다는 것을 의미한다.


특징

정의에서와 마찬가지로 객체 생성 이후에는 값을 변경시킬 수 없다.

또 한가지 특징은 같은 값을 가지는 객체 간에는 동일성(Equality)이 보장된다는 점이다.


이러한 개념이 나오게 된 배경은 바로 실생활에서 사용하는 값에 대한 개념이 바로 그러하기 때문이다. 금융이나 수학에서 자주 쓰이는 개념으로부터 나온 용례를 보면 보다 정확하게 알 수 있다.


용례

1. 100원은 (100원이 계속 존재하는 한) 100원이다. 100원짜리 동전들은 모두 100원이다.

2. PI는 항상 3.141592...이다. PI = PI이다.


Immutable 객체의 정의 및 특징과 마찬가지로 실 생활에서 어떤 값은 항상 그 값으로만 사용된다. 10이 10이었다가 어떤 때에는 20이 되거나 하지 않는다. 그리고 10은 항상 10과 같다. 이러한 동일성은 10이 10인 동안에는 항상 유지된다.

이러한 개념을 구현한 것이 바로 Immutable 객체이다.


실제 구현에 들어가 보자.

켄트 벡의 좋은 예제가 있으므로 그의 자취를 따라가보자. 우리는 Money 객체를 구현할 것이다. Money는 단위(unit)와 값(value)을 갖는다고 가정한다.


Money class 선언

class Money{

    private final String unit;

    private final int value;

    public String getUnit() {

        return unit;

    }

    public int getValue() {

        return value;

    }

}


객체의 외부에서 unit과 value를 확인할 수 있어야 하므로 getter 함수들도 함께 구현해 넣었다.


이제 정의를 만족하도록 더 구현해 넣을 차례다. 우선 생성 이후에는 값이 변경되지 않을 것이므로 생성자를 통해 값을 설정할 수 있도록 해주어야 한다.


class Money{

    public Money(String unit, int value){

        this.unit = unit;

        this.value = value;

    }


이렇게  unit과 value를 매개변수로 받아서 설정할 수 있도록 했다.


그 다음에는 동등성을 확보해야 한다. Java에서 동등성 비교는 equals() 함수를 통해 수행한다. 그런데 Java의 equals() 함수는 객체의 동일성을 비교하는 것이므로 이를 값을 비교하는 것으로 바꾸어 주어야 한다. 그래서 아래와 같이 equals() 함수를 재정의 한다.


class Money{

    @Override

    public boolean equals(Object obj) {

        Money extern = (Money) obj;

        return (extern != null) && unit.equals(extern.unit) && value == extern.value;

    }


여기까지 구현하면 이제 정의를 만족하는 Immutable 객체가 된 것이다. 


하지만 이것만으로 만족하기에는 뭔가 부족하다. 모든 값들은 연산을 할 수 있어야 한다. 5에 5를 더할 수 있어야 하고, 값의 의미에 따라 사칙연산이나 기타 여러 연산을 지원할 수 있어야 한다. 하지만 연산을 수행할 경우 값은 변경된다. 그런데 지금까지 구현한 Immutable 객체에서는 값을 변경할 방법이 없다.(그렇게 하면 값이 변경되므로 Immutable 객체의 정의에 어긋난다.) 그래서 값의 연산을 수행하고 그 결과를 새로운 Immutable 객체 생성을 통해 지원해 주는 함수를 구현해 주어야 한다.


아래 예제는 더하기를 구현한 것이다.


class Money{

    public Money plus(Money added){

        return new Money(unit, value + added.value);

    }


위와 같은 방법을 통해서 하나의 Immutable 객체와 다른 Immutable 객체의 덧셈을 구현한다.(편의상 unit에 대한 동일성 체크는 제외하였다.)


모두 다 구현했다면 아래와 같은 모양이 될 것이다.


최종 구현

class Money{

    private final String unit;

    private final int value;

    public String getUnit() {

        return unit;

    }

    public int getValue() {

        return value;

    }

   

    public Money(String unit, int value){

        this.unit = unit;

        this.value = value;

    }

   

    @Override

    public boolean equals(Object obj) {

        Money extern = (Money) obj;

        return (extern != null) && unit.equals(extern.unit) && value == extern.value;

    }

   

    public Money plus(Money added){

        return new Money(unit, value + added.value);

    }

}


이제 main() 함수를 이용하여 테스트를 해 볼 시간이다.


public static void main(String[] args) {

    Money five = new Money("KRW", 5);

    Money anotherFive = new Money("KRW", 5);

    Money ten = five.plus(five);

    System.out.println(five.equals(anotherFive));

    System.out.println(ten.getValue());

}


5원은 5원과 같아야 하고 5 + 5 = 10원이어야 한다.


용어와 개념은 모르고 있을 때는 전혀 쓸 수 없지만, 알고 있을 때는 매우 유용한 것이다.


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
,