JAVA 언어로 배우는 디자인 패턴 입문, 헤드퍼스트 디자인 패턴을 통해 학습 중이며 두 책의 예제 위주로 정리.

 

생성패턴(Creational Pattern)

1. 싱글톤(Singleton)

2. 빌더(Builder)

3. 팩토리 메소드(Factory Method)

4. 추상 팩토리(Abstract Factory)

5. 프로토타입(Prototype)

 

구조패턴(Structural Pattern)

1. 어댑터(Adapter)

2. 브릿지(Bridge)

3. 컴포지트(Composite)

4. 데코레이터(Decorator)

5. 퍼사드(Facade)

6. 플라이웨이트(flyweight)

7. 프록시(Proxy)

 

행동(행위) 패턴(Behavioral Pattern)

1. 책임 연쇄(Chain of Responsibility)

2. 커맨드(Command)

3. 인터프리터(Interpreter)

4. 반복자(Iterator)

5. 중재자(Mediator)

6. 메멘토(Memento)

7. 옵저버(Observer)

8. 상태(State)

9. 전략(Strategy)

10. 템플릿 메소드(Template Method)

11. 방문자(Visitor)


템플릿 메소드 패턴은 행동(행위) 패턴에 속하는 디자인 패턴으로 상위 클래스에서 처리의 뼈대를 결정하고 하위 클래스에서 구체적인 내용을 결정하는 패턴이다.

알고리즘의 구조를 변경하지 않고 알고리즘의 특정 단계들을 다시 정의할 수 있게 해준다.

 

예제

문자 또는 문자열을 출력하는 프로그램.

Character 타입의 경우 << >> 안에 해당 문자가 5번 반복이 되며, String 타입의 경우 +---+ +---+ 사이에 | 문자열 | 형태로 개행이 되며 5번 반복이 된다.

 

구조로는 추상클래스인 AbstractDisplay 에는 open, print, close라는 세개의 추상 메소드, open, 반복문으로 5번 수행하는 print, close 순서로 호출하고 있는 display 메소드가 존재한다.

구현체인 CharDisplay와 StringDisplay는 각각 출력할 문자 혹은 문자열 필드를 갖고 있으며, AbstractDisplay를 상속받아 추상메소드를 구현하고 있다.

이 구조에서 Template Method는 display가 된다.

 

//예제 출처 - JAVA 언어로 배우는 디자인 패턴 입문

public abstract class AbstractDisplay {
    public abstract void open();
    
    public abstract void print();
    
    public abstract void close();
    
    public final void display() {
        open();
        for(int i = 0; i < 5; i++)
            print();
        
        close();
    }
}
public class CharDisplay extends AbstractDisplay {
    
    private char ch;
    
    public CharDisplay(char ch) {
        this.ch = ch;
    }
    
    @Override
    public void open() {
        System.out.println("<<");
    }
    
    @Override
    public void print() {
         System.out.print(ch);
    }
    
    @Override
    public void close() {
        System.out.println(">>");
    }
}
public class StringDisplay extends AbstractDisplay {
    private String string;
    
    private int width;
    
    public StringDisplay(String string) {
        this.string = string;
        this.width = string.length();
    }
    
    @Overrdie
    public void open() {
        printLine();
    }
    
    @Overrdie
    public void print() {
        System.out.println("|" + string + "|");
    }
    
    @Override
    public void close() {
        printLine();
    }
    
    private void printLine() {
        System.out.print("+");
        for(int i = 0; i < width; i++)
            System.out.print("-");
        System.out.println("+");
    }
}
public class Main {
    public static void main(String[] args) {
        AbstractDisplay charH = new CharDisplay('H');
        
        AbstractDisplay stringHello = new StringDisplay("Hello World");
        
        charH.display();
        stringHello.display();
    }
}

 

실행결과

<<HHHHH>>

+----------+

|Hello World|

|Hello World|

|Hello World|

|Hello World|

|Hello World|

+----------+

 

예제 작성 전에 Template Method는 display() 메소드라고 했다.

display() 메소드는 동일하게 open() -> print() * 5 -> close() 를 처리하고 있다.

즉, 처리과정에 대한 뼈대가 된다.

그럼 상위 클래스인 AbstractDisplay에서 뼈대를 결정한 것이라고 볼 수 있다.

그리고 하위 클래스인 CharDisplay와 StringDisplay에서는 AbstractDisplay를 상속받아 메소드를 구현하고 있다.

각 클래스에서는 open(), print(), close()를 어떻게 처리할지 구체적인 구현을 담당하고 있다.

처음 템플릿 메소드의 설명에 빗대어 본다면,

상위 클래스(AbstractDisplay)에서 처리의 뼈대(display())를 결정하고 하위 클래스(CharDisplay, StringDisplay)에서 그 구체적인 내용(open(), print(), close())을 결정하는 패턴이다.

 

코드를 보며 재정리.

 

AbstractDisplay에는 구현해야 할 추상메소드(open(), print(), close())와 템플릿 메소드(display())가 존재한다.

템플릿 메소드는 추상 메소드들을 호출하게 된다.

실제 사용한다고 하면 여러 알고리즘 + 추상 메소드의 구조가 될 것이다.

즉, 템플릿 메소드는 해당 처리에 대한 정형화된 틀이 된다.

그리고 이 틀을 만들어 두게 됨으로써 다른 타입을 받거나 다른 결과를 내야 하는 경우 중복되는 처리 과정을 거치는 여러 메소드들을 생성하지 않고 재사용할 수 있다.

 

즉, CharDisplay와 StringDisplay의 공통점으로는 open() -> print() * 5 -> close()로 처리되고 있는데, 

이걸 각 클래스의 display() 메소드로 처리한다고 한다면 동일한 코드가 반복되거나 혹은 전혀 분리되어 있지 않은 코드가 생길것이다.

public class CharDisplay {
    private char ch;
    
    public CharDisplay(char ch) {
        this.ch = ch;
    }
    
    public void display() {
        System.out.print("<<");
        
        for(int i = 0; i < 5; i++)
            System.out.print(ch);
        
        System.out.println(">>");
    }
}



public class StringDisplay {
    private String string;
    
    private int width;
    
    public StringDisplay(String string) {
        this.string = string;
        this.width = string.length();
    }
    
    private void open() {
        printLine();
    }
    
    private void print() {
        for(int i = 0; i < 5; i++)
            System.out.println("|" + string + "|");
    }
    
    private void close() {
        printLine();
    }
    
    public void display() {
        open();
        print();
        close();
    }
    
    private void printLine() {
        System.out.print("+");
        
        for(int i = 0; i < width; i++)
            System.out.print("-");
            
        System.out.println("+");
    }
    
    
}

 

이러한 코드 구조라면 틀만 봤을때는 같은 처리과정을 갖고 있는데, 각 클래스에서 구현하게 됨으로써 중복이 발생한다.

그리고 만약 이 문자 또는 문자열을 10번 출력하도록 수정해야 한다면 모든 클래스를 체크하면서 5번을 10으로 수정해줘야 한다.

또한 재사용성이라고는 전혀 찾아볼 수도 없다.

그래서 이 처리 과정들을 추상메소드로 구체적인 것은 하위 클래스에서 맡겨놓고 처리하는 틀은 템플릿 메소드로 처리하게 되면 코드도 간결해져 코드가 보다 간결해 질 수 있고, 틀을 수정해야 하더라도 각 하위 클래스에서 수정할 것은 없고 템플릿 메소드에서만 수정이 발생하게 된다.

 

이렇게 상위 클래스 타입의 변수에 생성한 인스턴스 중 어느것을 대입하더라도 제대로 동작할 수 있도록 하는 원칙을 리스코프 치환법칙(LSP, Liskov Substitution Principle)이라고 한다.

 

템플릿 메소드 패턴을 하위 클래스 관점에서 본다면

  • 상위 클래스에서 정의 된 메소드를 하위 클래스에서 이용할 수 있다.
  • 하위 클래스에서 약간의 메소드를 기술하는 것만으로 새로운 기능을 추가할 수 있다.
  • 하위 클래스에서 메소드를 오버라이드 하게 되면 동작을 변경할 수 있다.

상위 클래스 관점에서 본다면

  • 하위 클래스에서 그 메소드를 구현하기를 기대한다.
  • 하위 클래스에 메소드 구현을 요청한다.

즉, 하위 클래스에서는 상위 클래스에서 선언한 추상 메소드를 구현할 책임이 있다고 할 수 있는데 이것을 subclass responsibility(하위 클래스의 책임)이라고 한다.

 

JAVA 언어로 배우는 디자인 패턴 입문, 헤드퍼스트 디자인 패턴을 통해 학습 중이며 두 책의 예제 위주로 정리.

 

생성패턴(Creational Pattern)

1. 싱글톤(Singleton)

2. 빌더(Builder)

3. 팩토리 메소드(Factory Method)

4. 추상 팩토리(Abstract Factory)

5. 프로토타입(Prototype)

 

구조패턴(Structural Pattern)

1. 어댑터(Adapter)

2. 브릿지(Bridge)

3. 컴포지트(Composite)

4. 데코레이터(Decorator)

5. 퍼사드(Facade)

6. 플라이웨이트(flyweight)

7. 프록시(Proxy)

 

행동(행위) 패턴(Behavioral Pattern)

1. 책임 연쇄(Chain of Responsibility)

2. 커맨드(Command)

3. 인터프리터(Interpreter)

4. 반복자(Iterator)

5. 중재자(Mediator)

6. 메멘토(Memento)

7. 옵저버(Observer)

8. 상태(State)

9. 전략(Strategy)

10. 템플릿 메소드(Template Method)

11. 방문자(Visitor)


전략 패턴(Strategy Pattern)은 행위패턴(Behavioral Pattern)에 속하는 디자인 패턴이다.

전략패턴은 스위치를 전환하듯 알고리즘(전략)을 바꿔서 같은 문제를 다른 방법으로 해결하기 쉽게 만들어주는 패턴이다.

특정한 계열의 알고리즘들을 정의하고 각 알고리즘을 캡슐화하여 이 알고리즘들을 해당 계열 안에서 상호 교체가 가능하게 만든다.

 

예제

자바 언어로 배우는 디자인패턴 입문 책에서의 예제를 먼저 본다.

예제 프로그램은 가위바위보를 하는 프로그램이다.

이 프로그램에서는 두가지의 전략을 갖는다.

1. 이기면 다음에도 같은 손을 내는 다소 어리석은 방법(WinningStrategy)

2. 직전 손에서 다음 손을 확률적으로 계산하는 방법(ProbStrategy)

 

//예제 출처 - JAVA 언어로 배우는 디자인패턴 입문

public enum Hand {
    
    ROCK("바위", 0)
    , SCISSORS("가위", 1)
    , PAPER("보", 2);
    
    private String name;   //가위바위보 손 이름
    private int handValue; //가위바위보 손의 값
    
    private static Hand[] hands = {
        ROCK, SCISSORS, PAPER
    };
    
    // Constructor
    private Hand(String name, int handValue) {
        this.name = name;
        this.handValue = handValue;
    }
    
    //손의 값으로 enum 상수를 가져온다
    public static Hand getHand(int handValue) {
        return hands[handValue];
    }
    
    //this가 h 보다 강하다면 true
    public boolean isStrongerThan(Hand h) {
        return fight(h) == 1;
    }
    
    //this가 h보다 약하다면 true
    public boolean isWeakerThan(Hand h) {
        return fight(h) == -1;
    }
    
    //무승부는 0, this가 이기면 1, h가 이기면 -1
    private int fight(Hand h) {
        if(this == h)
            return 0;
        else if((this.handValue + 1) % 3 == h.handValue)
            return 1;
        else
            return -1;
    }
    
    @Override
    public String toString() {
        return name;
    }
}
public interface Strategy {
    public abstract Hand nextHand();
    
    public abstract void study(boolean win);
}
/**
* 이기면 다음에도 같은 손을 내는 전략
*/

public class WinningStrategy implements Strategy{
    private Random random;
    
    private boolean won = false;
    
    private Hand prevHand;
    
    public WinningStrategy(int seed) {
        random = new Random(seed);
    }
    
    @Override
    public Hand nextHand() {
        if(!won)
            prevHand = Hand.getHand(random.nextInt(3));
        
        return prevHand;
    }
    
    @Override
    public void study(boolean win) {
        won = win;
    }
}
/**
* 확률을 계산해 다음 손을 결정하는 전력
* 
* history[직전에 낸 손][이번에 낸 손]
* 배열의 value는 과거의 승리 수를 의미
*/
public class ProbStrategy implements Strategy{
    
    private Random random;
    
    private int prevHandValue = 0;
    
    private int currentHandValue = 0;
    
    private int[][] history = {
        { 1, 1, 1 },
        { 1, 1, 1 },
        { 1, 1, 1 }
    };
    
    public ProbStrategy(int seed) {
        random = new Random(seed);
    }
    
    @Override
    public Hand nextHand() {
        int bet = random.nextInt(getSum(currentHandValue));
        int handValue = 0;
        
        if(bet < history[currentHandValue][0])
            handValue = 0;
        else if(bet < history[currentHandValue][0] + history[currentHandValue][1])
            handValue = 1;
        else
            handValue = 2;
        
        prevHandValue = currentHandValue;
        currentHandValue = handValue;
        
        return Hand.getHand(handValue);
    }
    
    private int getSum(int handValue) {
        int sum = 0;
        
        for(int i = 0; i < 3; i++)
            sum += history[handValue][i];
        
        return sum;
    }
    
    @Override
    public void study(boolean win) {
        if(win)
            history[prevHandValue][currentHandValue]++;
        else{
            history[prevHandValue][(currentHandValue + 1) % 3]++;
            history[prevHandValue][(currentHandValue + 2) % 3]++;
        }
    }
}
public class Player {
    private String name;
    
    private Strategy strategy;
    
    private int winCount;
    
    private int loseCount;
    
    private int gameCount;
    
    //이름과 전략을 받아 플레이어를 생성
    public Player(String name, Strategy strategy) {
        this.name = name;
        this.strategy = strategy;
    }
    
    //전략에 따라 다음 손을 결정
    public Hand nextHand() {
        return strategy.nextHand();
    }
    
    //승리
    public void win() {
        strategy.study(true);
        winCount++;
        gameCount++;
    }
    
    //패배
    public void lose() {
        strategy.study(false);
        loseCount++;
        gameCount++;
    }
    
    //무승부
    public void even() {
        gameCount++;
    }
    
    @Override
    public String toString() {
        return "["
                + name + " : "
                + gameCount + " games, "
                + winCount + " win, "
                + loseCount + " lose]";
    }
}
public class Main{
    public static void main(String[] args) {
        int seed1 = 314;
        int seed2 = 15;
        
        Player player1 = new Player("Kim", new WinningStrategy(seed1));
        Player player2 = new Player("Lee", new ProbStrategy(seed2));
        
        for(int i = 0; i < 10000; i++) {
            Hand nextHand1 = player1.nextHand();
            Hand nextHand2 = player2.nextHand();
            
            if(nextHand1.isStrongerThan(nextHand2)) {
                System.out.println("Winner : " + player1);
                player1.win();
                player2.lose();
            }else if(nextHand2.isStrongerThan(nextHand1)) {
                System.out.println("Winner : " + player2);
                player1.lose();
                player2.win();
            }else{
                System.out.println("Even...");
                player1.even();
                player2.even();
            }
        }
        
        System.out.println("Total result : ");
        System.out.println(player1);
        System.out.println(player2);
    }
}

 

구조

Hand 가위바위보에서 손을 나타내는 클래스
Strategy 가위바위보의 '전략'을 나타내는 인터페이스
WinningStrategy 이기면 다음에도 같은 손을 내는 전략의 클래스.
Strategy의 구현체
ProbStrategy 직전 손에서 다음 손을 확률적으로 계산하는 전략의 클래스.
Strategy의 구현체
Player 가위바위보를 하는 플레이어 클래스

 

메인메소드 실행 시 10,000번의 게임을 진행하고 가장 마지막에 A와 B의 승리, 패배 횟수가 출력된다.

Hand는 enum 클래스로 가위, 바위, 보 라는 세개의 enum 상수를 갖고 있으며, 필드로 name, handValue를 갖고 있다.

또한 isStrongerThan() 메소드를 통해 A와 B의 결과를 반환 할 수 있다.

각 전략에 대한 인터페이스인 Strategy가 존재한다.

그리고 전략에 대한 구체화를 표현한 WinningStrategy, ProbStrategy가 있다.

각 플레이어는 이름과 전략을 갖고 생성된다.

그래서 게임이 진행될때마다 각 플레이어는 자신의 전략에 따라 다음에 낼 손을 결정하게 된다.

 

Strategy 패턴의 구조는 아래와 같다.

1. Strategy - 전략을 이용하기 위한 인터페이스. 예제에서 인터페이스로 처리했지만 추상 클래스로 처리하는 것도 가능하다.

2. ConcreteStrategy - Strategy의 구현체. 여러개의 ConcreteStrategy로 다양한 전략이 가능하다.

3. Context - Strategy를 이용하는 주체. ConcreteStrategy의 인스턴스를 갖고 있다가 필요에 따라 이용한다. 예제에서의 Player 클래스.

 

마무리 정리는 조금 뒤로 미루고 헤드퍼스트 디자인 패턴 책에서의 예제도 정리한다.

예제 프로그램은 오리 시뮬레이터이다.

객체로는 일반 오리와 모형오리 두가지가 존재.

 

코드 먼저 정리한다.

이 예제에서는 패키지가 나눠져 있으니 패키지 구조도 확인.

//예제 출처 - 헤드퍼스트 디자인 패턴

package strategyPattern.headfirst.fly;

public interface FlyBehavioral {
    public void fly();
}
package strategyPattern.headfist.fly;

public class FlyNoWay implements FlyBehavioral {
    
    @Override
    public void fly() {
        System.out.println("날지 못해요");
    }
}
package strategyPattern.headfist.fly;

public class FlyRocketPowered implements FlyBehavioral {
    
    @Override
    public void fly() {
        System.out.println("로켓 추진!!!!!!!!!!!");
    }
}
package strategyPattern.headfist.fly;

public class FlyWithWings implements FlyBehavioral {
    
    @Override
    public void fly() {
        System.out.println("날고 있어요");
    }
}
package strategyPattern.headfist.quack;

public interface QuackBehavioral {
    
    public void quack();
}
package strategyPattern.headfist.quack;

public class MuteQuack implements QuackBehavioral {
    
    @Override
    public void quack() {
        System.out.println("---");
    }
}
package strategyPattern.headfist.quack;

public class Quack implements QuackBehavioral {
    
    @Override
    public void quack() {
        System.out.println("꽥!");
    }
}
package strategyPattern.headfist.quack;

public class Squeak implements QuackBehavioral {
    
    @Override
    public void quack() {
        System.out.println("삑!");
    }
}
package strategyPattern.headfirst;

public abstract class Duck {
    
    FlyBehavioral flyBehavioral;
    
    QuackBehavioral quackBehavioral;
    
    public Duck() {
    
    }
    
    public void swim() {
        System.out.println("모든 오리는 물에 뜹니다.");
    }
    
    public abstract void display();
    
    public void perfomFly() {
        flyBehavioral.fly();
    }
    
    public void performQuack() {
        quackBehavioral.quack();
    }
    
    public void setFlyBehavioral(FlyBehavioral fb) {
        flyBehavioral = fb;
    }
    
    public void setQuackBehavioral(QuackBehavioral qb) {
        quackBehavioral = qb;
    }
}
package strategyPattern.headfirst;

public class MallardDuck extends Duck {
    
    @Override
    public void display() {
        System.out.println("물오리!");
    }
    
    public MallardDuck() {
        quackBehavioral = new Quack();
        flyBehavioral = new FlyWithWings();
    }
}
package strategyPattern.headfirst;

public class ModelDuck extends Duck {
    
    @Override
    public void display() {
        System.out.println("모형오리!");
    }
    
    public ModelDuck() {
        flyBehavioral = new FlyNoWay();
        quackBehavioral = new MuteQuack();
    }
}
package strategyPattern.headfirst;

public class DuckSimulator {
    public static void main(String[] args) {
        // 물오리
        Duck mallardDuck = new MallardDuck();
        mallardDuck.display();
        mallardDuck.performFly();
        mallardDuck.performQuack();
        
        System.out.println("------------");
        
        Duck modelDuck = new ModelDuck();
        modelDuck.display();
        modelDuck.performFly();
        modelDuck.setFlyBehavioral(new FlyRocketPowered());
        modelDuck.performFly();
        modelDuck.performQuack();
    }
}

메인 메소드를 실행해보면

물오리!

날고 있어요

꽥!

----------

모형오리!

날지 못해요

로켓 추진!!!!!!!

---

 

이렇게 출력된다.

그럼 책에서의 예시를 정리해보자.

다양한 종류의 오리의 모습을 보여주고, 헤엄치고 울 수 있는 시뮬레이터를 만들었다.

 

그래서 Duck 이라는 클래스에 quack(), swim(), display() 메소드를 만들었다.

모든 오리는 꽥꽥 소리를 내고 헤엄을 칠 수 있기 때문에 display() 메소드만 추상 메소드로 만들었다.

그래서 각 오리 객체는 Duck 클래스를 상속받고, display() 메소드를 각각 구현하고 있다.

 

그런데 시뮬레이터를 서비스하다가 날아가는 것도 표현되었으면 좋겠다는 사용자 의견에 따라 날아 가는 기능을 추가하게 되었다.

그래서 Duck 클래스에 fly() 메소드를 추가하게 되었다.

시간이 조금 지나서 좀 더 다양한 객체를 추가하려고 러버덕을 추가하기로 했다.

러버덕 객체 역시 다른 오리 객체들과 마찬가지로 Duck을 상속받는 형태로 작성했다.

그랬더니??

날 수 없는 러버덕이 날아다녔다.

또한 러버덕은 꽥꽥 소리가 아닌 삑삑 이라는 소리가 나야 하는데 똑같이 꽥꽥 소리를 내며 울고 있었다.

 

그래서 러버덕 객체에서는 quack() 메소드와 fly() 메소드를 모두 오버라이드해 처리하는 방법을 생각했지만, 그 후 객체를 계속 추가하게 된다면 이것 역시 비효율적이라는 생각을 하게 된다.

 

그래서 분리를 생각하게 되었고, 공통적으로 모든 객체가 수행하는 기능인 swim()과 display() 메소드는 Duck 클래스에 남겨두고, fly()와 quack()은 각각의 인터페이스로 분리한 뒤 필요로 하는 클래스에서 인터페이스들을 확장해 구현하도록 처리한다.

 

여기까지가 책에서의 예시이다.

그럼 위와 같이 처리했을 때의 문제점으로는 무엇이 있을까?

바로 코드의 중복이다.

디자인패턴의 장점으로 유지보수성과 재사용성이 있다.

인터페이스로 분리하는것까지는 좋았지만, 인터페이스를 확장해 구현하는 모든 객체에서 동일한 기능을 하는 객체가 단 하나도 없다면 이러한 분리도 괜찮을 수 있을지도 모르겠다.

하지만, 가장 먼저 시뮬레이터에 생성한 객체인 실제 오리들의 날아가는 행동이 '날고 있어요' 라는 기능으로 동일하다면?

모든 해당 객체들에서는 인터페이스 구현으로 인해 '날고 있어요' 라는 기능의 코드가 작성되어 있을 것이다.

그럼 코드의 중복이 발생한다는 것이다. 중복이 발생한다는 것은 동일한 코드를 사용하고 있음에도 재사용을 하지 않고 새로운 코드를 사용한다고 할 수 있다.

또한, 이 '날고 있어요' 라는 기능을 '날고 있네요??' 라고 수정해야 하는 상황이 왔다면??

그리고 여기에 해당하는 객체들이 몇십 몇백개라면???

모든 객체들의 구현 코드를 수정해야 하는 상황이 발생하게 된다.

 

이 문제의 해결책이 예제이다.

분리까지는 좋은 방법이니 swim, display는 Duck에 그대로 두고 Quack과 Fly를 인터페이스로 만들어준다.

단, 이 인터페이스들을 각 객체에서 구현하는 것이 아닌 클래스 집합으로 문제를 해결한다.

예제 구조를 보면 FlyBehavioral 인터페이스를 구현하는 클래스는 FlyNoWay(날지 못해요), FlyRocketPowered(로켓 추진!!!!), FlyWithWings(날고 있어요) 이렇게 존재한다.

그리고 각 오리 객체에서는 이를 구현하지 않고 있으며, Duck 클래스에서는 각 인터페이스변수를 갖고 있게 된다.

각 객체는 Duck 클래스를 상속받고 있기 때문에 생성자를 통해 각 인터페이스 변수에 행동 클래스 인스턴스를 대입하게 된다.

그 코드가 각 객체 생성자에 있는 flyBehavioral = new FlyWithWings(); 코드이다.

그럼 객체가 생성되고 행동 처리를 하는 메소드를 호출하게 된다면 행동 클래스 인스턴스가 대입되어 있는 인터페이스 변수를 통해 기능을 수행하게 되는 것이다.

 

이렇게 수행했을때를 한번 살펴보면,

일단, 오리 객체들이 직접 fly()를 구현하고 있지 않기 때문에 코드의 중복이 발생하지 않는다.

동일하게 날아가는 모습을 표현하는 오리 객체가 200개가 존재한다고 했을 때, 

이 객체들은 생성자를 통해 flyBehavioral에 행동 클래스 인스턴스를 담아주기만 하면 될 뿐이지 직접 '날고 있어요' 라는 기능의 코드를 작성할 필요가 없게 된다.

그럼 FlyWithWings()의 구현체는 재사용이 처리되고 있는 코드라고 볼 수 있고, 그로인해 중복이 제거되며 재사용성이 높아졌다고 볼 수 있게 된다.

 

다음은 유지보수 관점에서다.

각 객체에서 구현하는 경우 '날고 있어요'를 '날고 있네요??' 라고 수정하기 위해서는 200개의 객체를 모두 수정해야 했다.

하지만 이렇게 구현을 해두고 나면 FlyWithWing() 의 구현체만 수정한다면 문제가 해결된다.

그럼 200개의 메소드를 수정할 필요없이 단 한개의 메소드 수정으로 문제가 해결되는 것이다.

 

또한, 클래스 집합에 대한 디자인은 최대한 유연하게 만드는 것이 좋다.

메인 메소드를 보면 모형오리는 날지 못하는 것이 생성자를 통한 기본값이지만, setter를 통해 FlyRocketPowered 클래스 인스턴스를 담도록 처리한다.

그리고 모형오리를 날도록 해보면 로켓을 달아놓은 모형오리가 된다.

이렇게 유연하게 만들면 다양한 기능을 사용할 수 있도록 처리할 수 있다.

 

위 정리에서의 중점은 나는 행동과 우는 행동을 Duck(슈퍼 클래스)나 그 서브클래스에서 정의한 메소드를 사용해 구현하는 것이 아닌 다른 클래스에 '위임'한다는 것이다.

 

 

정리

두 책에서의 예제는 완전 다른 스타일의 예제였다.

JAVA 언어로 배우는 디자인패턴 입문 책의 예제는 전략 패턴 그 자체에 집중한 예제였다.

가위바위보를 수행하는 프로그램을 만들것이지만, 승리했을 때 이전에 냈던 손을 그대로 낸다는 전략(WinningStrategy)와 직전에 낸 손에서 다음 손을 확률적으로 계산해 낸다는 전략(ProbStrategy)가 존재하며 플레이어는 생성될 때 어떤 전략을 사용할 것인지를 결정하게 되고, 이 전략은 Strategy 라는 인터페이스를 통해 같은 메소드의 구현체로 전략을 구현한다.

그리고 플레이어는 생성될 때 전략을 받아 Strategy 인터페이스 변수에 전략을 담아두고 게임에 임하는 내내 해당 전략을 구사한다.

 

이렇게 구현한다면 각 전략을 플레이어 객체에서 구현하느라 여러 플레이어 객체를 생성해 구현할 필요가 없고 전략 인터페이스를 구현하는 집합체로 구현 클래스를 통한 전략 설계를 하면 된다.

 

헤드퍼스트 디자인 패턴에서는 전략 패턴은 캡슐화가 중요하다! 라는 느낌이었다.

아무래도 좀 더 다양한 기능에 대한 예제라 그렇지 않나 싶다.

시뮬레이터라는 프로그램에서의 기능을 전략이라고 볼 수 있다.

직관적으로 이게 전략이다! 라는 느낌은 없었지만 이렇게 캡슐화를 해야 코드의 재사용성과 유지보수성을 높일 수 있다 라는 것에 대해 이해하기는 더 좋았다.

 

두 책에서의 공통적인 중점.

전략패턴은 알고리즘을 캡슐화 해 각각의 알고리즘 수정에 다른 클래스를 수정해야 하는 경우가 발생하지 않도록 해야 한다.

구현체인 ConcreteStrategy를 수정하거나 추가로 생성하는 형태로 처리할 수 있으며, 위임이라는 약한 결합으로 알고리즘 수정이 용이하다.

 

 

 

 

 

JAVA 언어로 배우는 디자인 패턴 입문, 헤드퍼스트 디자인 패턴을 통해 학습 중이며 두 책의 예제 위주로 정리.

 

생성패턴(Creational Pattern)

1. 싱글톤(Singleton)

2. 빌더(Builder)

3. 팩토리 메소드(Factory Method)

4. 추상 팩토리(Abstract Factory)

5. 프로토타입(Prototype)

 

구조패턴(Structural Pattern)

1. 어댑터(Adapter)

2. 브릿지(Bridge)

3. 컴포지트(Composite)

4. 데코레이터(Decorator)

5. 퍼사드(Facade)

6. 플라이웨이트(flyweight)

7. 프록시(Proxy)

 

행동(행위) 패턴(Behavioral Pattern)

1. 책임 연쇄(Chain of Responsibility)

2. 커맨드(Command)

3. 인터프리터(Interpreter)

4. 반복자(Iterator)

5. 중재자(Mediator)

6. 메멘토(Memento)

7. 옵저버(Observer)

8. 상태(State)

9. 전략(Strategy)

10. 템플릿 메소드(Template Method)

11. 방문자(Visitor)


어댑터 패턴(Adapter Pattern)은 구조패턴(Structural Pattern)에 속하는 패턴이다.

현실에서 어댑터라는 것은 보통 전류 변환을 해주는 용도로 사용이 된다.

제공되는 것과 필요한 것 사이에서 그 사이를 채워주는 역할을 하는 것이 어댑터이다.

책에서는 이렇게 예시를 들고 있다.

노트북에 필요한 전류는 12볼트이지만 콘센트에서는 100볼트로 들어온다면, 그것을 변환해줘야 하는 것이 필요하다.

100볼트로 들어온 전류를 12볼트로 변환해주는 역할을 어댑터가 담당한다.

 

프로그램에서도 이미 제공된 코드를 그대로 사용할 수 없을 때, 필요한 형태로 변환 후 이용하는 경우가 발생했을 때 어댑터 패턴을 사용한다.

어댑터 패턴은 Wrapper 패턴이라고 불리기도 한다.

감싼다는 의미인데 무엇인가를 포장해서 다른 용도로 사용할 수 있도록 변환해주는것이 Wrapper이자 어댑터이다.

 

어댑터 패턴에는 두가지 종류가 있다.

1. 클래스에 의한 어댑터(상속을 사용한 패턴)

2. 인스턴스에 의한 어댑터(위임을 사용한 패턴)

 

예제

 

예제 프로그램은 주어진 문자열을 ( ) 안에 넣어 출력하거나 * * 안에 넣어 출력하도록 하는 프로그램이다.

 

1. 클래스에 의한 어댑터

메인 클래스를 제외하고 2개의 클래스와 1개의 인터페이스로 작성한다.

//예제 출처 - JAVA 언어로 배우는 디자인 패턴 입문

public class Banner {
    
    private String string;
    
    public Banner(String string) {
        this.string = string;
    }
    
    public void showWithParen() {
        System.out.println("(" + string + ")");
    }
    
    public void showWithAster() {
        System.out.println("*" + string + "*");
    }
}
public interface Print {
    
    public abstract void printWeak();
    
    public abstract void printStrong();
}
public class PrintBanner extends Banner implements Print {
    
    public PrintBanner(String string) {
        super(string);
    }
    
    @Override
    public void printWeak() {
        showWithParen();
    }
    
    @Override
    public void printStrong() {
        showWithAster();
    }
}
public class AdapterMain {
    
    public static void main(String[] args) {
        Print p = new PrintBanner("Hello");
        
        p.printWeak();
        p.printStrong();
    }
}

처리 과정을 먼저 보면 PrintBanner 생성자의 super를 통해 Banner 의 생성자에 접근하게 된다.

그리고 생성된 인스턴스는 Print를 구현하고 있는 구현체이기 때문에 Print 타입의 p 변수에 담을 수 있게 된다.

그리고 여기서 printWeak과 printStrong을 호출하면 PrintBanner 클래스에서는 Banner를 상속받고 있기 때문에

Banner의 showWithParen()과 showWithAster()를 바로 호출해 사용할 수 있게 된다.

그래서 (Hello)와 *Hello*가 출력되게 된다.

 

여기서 Banner 클래스는 문자열을 괄호로 묶는 메소드와 * * 로 묶는 메소드가 준비되어 있다.

이 Banner 클래스를 위 전류 예시 중 콘센트에서 제공되는 100볼트라고 가정한다.

 

Print 인터페이스에서는 printWeak과 printStrong이 선언되어 있다.

이 인터페이스를 12볼트가 필요한 노트북이라고 가정한다.

 

마지막으로 PrintBanner 클래스는 Banner 클래스를 상속받으며 Print 인터페이스를 구현하고 있다.

PrintBanner에서는 부모클래스인 Banner의 showWithParen()으로 printWeak을 구현하고 있고, showWithAster()로 printStrong을 구현하고 있다.

이 PrintBanner 클래스가 어댑터의 역할을 한다.

 

정리해보자면 아래와 같다.

- 제공되는 것(100볼트) -  Banner(showWithParen, showWithAster)

- 전류 변환장치(어댑터) -  PrintBanner

- 필요한 것(12볼트)       -  Print 인터페이스(printWeak, printStrong)

 

그럼 메인메소드 코드를 이렇게 볼 수 있다.

PrintBanner 인스턴스를 Print 타입으로 생성해준다.

Print에서 사용하고자 한 printWeak, printStrong을 호출하게 되면

어댑터인 PrintBanner는 제공되는 Banner의 showWithParen, showWithAster를 Print()가 사용할 수 있게 해준다.

 

그럼 메인 메소드에서는 Print 인터페이스를 통해 요청을 처리하고 있게 되는 것이기 때문에 Print 인터페이스에만 관심이 있을 뿐, Banner클래스와 PrintBanner 클래스가 어떻게 구현되어있는지는 관심이 없게 된다.

즉, Banner와 PrintBanner에서 어떻게 처리하는지는 알 수 없고, Print 인터페이스에 printWeak(), printStrong()이 선언되어 있다는것만 알고 있는 것이다.

마치 노트북에서 12볼트로 동작을 하지만 어댑터에 들어오는 전류가 100볼트이던 200볼트이던 상관이 없다는 것이다.

이렇게 구현하게 되면 수정이 필요하게 되더라도 메인메소드의 수정이 발생하지 않고 Banner 클래스나 PrintBanner 클래스에서의 수정만 발생하게 된다.

 

 

2. 인스턴스에 의한 어댑터

클래스에 의한 어댑터와 다른 점으로는 상속이 아닌 위임을 사용한다는 점이다.

자바에서 위임은 어떤 메소드의 실제 처리를 다른 인스턴스의 메소드에 맡기는 것을 말한다.

예제는 Banner 클래스는 수정이 없고, Print 인터페이스를 추상클래스로, 그리고 PrintBanner 클래스에서 Banner 클래스를 상속받지 않도록 수정한다.

public abstract class Print2 {
    public abstract void printWeak();
    
    public abstract void printStrong();
}
public class PrintBanner2 extends Print2 {
    
    private Banner banner;
    
    public PrintBanner2(String string) {
        this.banner = new Banner(string);
    }
    
    @Override
    public void printWeak() {
        banner.showWithParen();
    }
    
    @Override
    public void printStrong() {
        banner.showWithAster();
    }
}
public class AdapterMain {
    public static void main(String[] args) {
        Print2 p2 = new PrintBanner2("Hi");
        
        p2.printWeak();
        p2.printStrong();
    }
}

1번과 다른점을 보자면 Print 인터페이스가 추상클래스인 Print2로 바뀌었다는 점.

PrintBanner2에서는 Banner를 필드로 갖고 생성자에서는 상위 클래스가 아니기 때문에 new 키워드를 통한 인스턴스를 생성해 필드에 담아주게 된다.

그리고 구현메소드 역시 상속에서 벗어났기 때문에 그냥 사용할 수 없어 banner 필드를 통해 호출한다.

 

자바에서는 다중상속이 불가능하기 때문에 Print2 클래스를 상속받은 구현체라면 Banner 클래스를 또 상속받을 수 없다.

그래서 Banner 필드를 갖게 되는 것이고 이 필드를 통한 호출로 인해 위임이 발생하게 된다.

즉, PrintBanner2에서 printWeak() 메소드가 호출되었을 때 자신이 처리하지 않고, 다른 인스턴스인 banner의 showWithParen()으로 위임하게 되는 것이다.

 

 

처음에는 이게 너무 이해가 어려웠다.

PrintBanner 클래스가 어댑터라는데 이게 왜 어댑터인지, 중간에서 뭘 변환해준다는 것인지 이해하기가 어려웠다.

그래서 헤드퍼스트 디자인 패턴에서의 예제를 봤는데 이게 좀 더 이해가 쉬웠다.

 

예제 프로그램은 아래와 같다.

Duck, Terkey 라는 객체가 존재하고, 

Duck은 꽥! 이라는 소리를 내고 날아간다.

Terkey는 골골 이라는 소리를 내고 5번을 짧게 날아간다.

 

위 예제에서와는 다르게 다른 처리과정이 존재하는 예제라고 볼 수 있다.

구조는 인터페이스로 Duck, Terkey를 갖게 되고, 클래스로 MallardDuck, WildTurkey, TurkeyAdapter가 존재한다.

그리고 메인 클래스로 DuckMain 클래스가 있다.

//예제 출처 - 헤드퍼스트 디자인 패턴

public interface Duck {
    public void quack();
    
    public void fly();
}
public class MallardDuck implements Duck {
    
    @Override
    public void quack() {
        System.out.println("꽥!");
    }
    
    @Override
    public void fly() {
        System.out.println("날고 있어요!");
    }
}
public interface Turkey {
    public void gobble();
    
    public void fly();
}
public class WildTurkey implements Turkey {
    
    @Override
    public void gobble() {
        System.out.println("골골");
    }
    
    @Overrdie
    public void fly() {
        System.out.println("짧게 나는중!");
    }
}
public class TurkeyAdapter implements Duck {
    private Turkey turkey;
    
    public TurkeyAdapter(Turkey turkey) {
        this.turkey = turkey;
    }
    
    @Override
    public void quack() {
        turkey.gobble();
    }
    
    @Override
    public void fly() {
        for(int i = 0; i < 5; i++)
            turkey.fly();
    }
}
public class DuckMain {
    public static void main(String[] args) {
        Turkey turkey = new WildTurkey();
        
        System.out.println("칠면조가");
        turkey.gobble();
        turkey.fly();
        
        Duck duck = new MallardDuck();
        
        System.out.println();
        System.out.println("오리가");
        testDuci(duck);
        
        Duck turkeyAdapter = new TurkeyAdapter(turkey);
        
        System.out.println();
        System.out.println("칠면조 어댑터가");
        testDuck(turkeyAdapter);
    }
    
    static void testDuck(Duck duck) {
        duck.quack();
        duck.fly();
    }
}

실행해보면

칠면조가 한번 울고 한번 날고, 오리가 한번 울고 한번 날고, 칠면조 어댑터가 한번 울고 5번을 나는것을 볼 수 있다.

 

Duck은 quack()과 fly()메소드를, Turkey는 gobble(), fly() 메소드를 갖고 있고

각각 MallardDuck과 WildTurkey라는 구현체를 갖고 있다.

각 구현체에서는 울음소리와 날아가는 것을 한번씩 출력하도록 되어있다.

 

그리고 칠면조 어댑터가 존재한다.

이 어댑터의 역할은 칠면조를 Duck과 같이 사용하도록 하는 것이다.

메인 클래스의 testDuck 메소드에서는 Duck 타입의 매개변수를 받아 quack()과 fly()를 호출하고 있다.

이 testDuck 메소드는 들어오는 매개변수가 오리인지 칠면조인지 전혀 구분하지 못하지만 다른 결과를 출력하게 된다.

그럼 이 결과로 gobble()과 fly()를 갖고 있는 Turkey 인터페이스가 어댑터를 통해 Duck과 같은 quack(), fly()로 변환되어 동작하고 있다고 볼 수 있고, testDuck 메소드는 타깃 인터페이스인 Duck를 통해 어댑터에게 요청을 보낼 뿐 어댑터에 연결된 반대쪽 객체가 무엇인지에 대해서는 알 수 없다.

 

정리.

어댑터 패턴은 새로운 객체를 추가하여 연결해주는 목적이라기 보다 호환이 되지 않는 서로 다른 인터페이스(오리와 칠면조)를 어댑터를 통해 연결하여 사용할 수 있도록 하는 패턴이다.

JAVA 언어로 배우는 디자인 패턴 입문, 헤드퍼스트 디자인 패턴을 통해 학습 중이며 두 책의 예제 위주로 정리.

 

생성패턴(Creational Pattern)

1. 싱글톤(Singleton)

2. 빌더(Builder)

3. 팩토리 메소드(Factory Method)

4. 추상 팩토리(Abstract Factory)

5. 프로토타입(Prototype)

 

구조패턴(Structural Pattern)

1. 어댑터(Adapter)

2. 브릿지(Bridge)

3. 컴포지트(Composite)

4. 데코레이터(Decorator)

5. 퍼사드(Facade)

6. 플라이웨이트(flyweight)

7. 프록시(Proxy)

 

행동(행위) 패턴(Behavioral Pattern)

1. 책임 연쇄(Chain of Responsibility)

2. 커맨드(Command)

3. 인터프리터(Interpreter)

4. 반복자(Iterator)

5. 중재자(Mediator)

6. 메멘토(Memento)

7. 옵저버(Observer)

8. 상태(State)

9. 전략(Strategy)

10. 템플릿 메소드(Template Method)

11. 방문자(Visitor)


 

반복자(Iterator)는 행위패턴(Behavioral Pattern)에 속하는 디자인 패턴이다.

자바에서 배열의 모든 요소를 표현하기 위해서는 반복문을 사용해 처리하는 경우가 많다.

for문을 통해 사용하게 되는 경우가 많고 이 경우 i 라는 인덱스 변수를 사용해 i를 증가시켜가며 검색하게 되는데

이 인덱스 변수인 i 의 기능을 추상화 하여 일반화 한 것을 디자인 패턴에서는 Iterator 패턴이라고 한다.

무엇인가 많이 모여있을 때 이를 순서대로 가리키며 전체를 검색하고 처리를 반복하는 것이다.

Iterate의 의미는 '반복하다'라는 의미이기 때문에 Iterator를 반복자라고 부른다.

 

예제

예제 프로그램으로는 책장(BookShelf) 안에 책(Book)을 넣고 책 이름을 차례대로 표시하는 프로그램이다.

 

//예제 출처 - JAVA 언어로 배우는 디자인 패턴 입문

public class Book {
    private String name;
    
    public Book(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
}
import java.util.Iterator;

public BookShelf implements Iterable<Book> {
    private Book[] books;
    
    private int last = 0;
    
    public BookShelf(int maxSize) {
        this.books = new Book[maxSize];
    }
    
    public Book getBookAt(int index) {
        return books[index];
    }
    
    public void appendBook(Book book) {
        this.books[last] = book;
        last++;
    }
    
    public int getLength() {
        return last;
    }
    
    @Override
    public Iterator<Book> iterator() {
        return new BookShelfIterator(this);
    }
}
import java.util.Iterator;
import java.util.NoSuchElementException;

public class BookShelfIterator implements Iterator<Book> {
    private BookShelf bookShelf;
    
    private int index;
    
    public BookShelfIterator(BookShelf bookShelf) {
        this.bookShelf = bookShelf;
        this.index = 0;
    }
    
    @Override
    public boolean hasNext() {
        if(index < bookShelf.getLength())
            return true;
        else
            return false;
    }
    
    @Override
    public Book next() {
        if(!hasNext())
            throw new NoSuchElementException();
        
        Book book = bookShelf.getBookAt(index);
        
        index++;
        
        return book;
    }
}
import java.util.Iterator

public class BookMain {
    
    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(4);
        
        bookShelf.appendBook(new Book("Around the world in 80 Days"));
        bookShelf.appendBook(new Book("Bible"));
        bookShelf.appendBook(new Book("Cinderella"));
        bookShelf.appendBook(new Book("Daddy-Long-Legs"));
        
        //명시적으로 Iterator를 사용하는 방법
        //Iterator 인스턴스를 생성
        Iterator<Book> it = bookShelf.iterator();
        
        //hasNext()가 false가 될때까지.
        //즉, 다음 객체가 존재하지 않을때까지
        //book에 next()로 값을 받아오고 책 이름을 출력한다.
        while(it.hasNext()) {
            Book book = it.next();
            System.out.println(book.getName());
        }
        
        System.out.println();
        
        //확장 for문을 사용하는 경우
        for(Book book : bookShelf)
            System.out.println(book.getName());
    }
}

 

 

 

구조

BookShelf

- 책장을 나타내는 클래스. Iterable<book> 인터페이스를 구현한다.

- books라는 Book 타입 배열이 존재하며, 크기는 BookShelf 인스턴스 생성 시 생성자 인수를 통해 결정된다.

- last는 appendBook() 메소드를 통해 books에 데이터가 추가될때마다 하나씩 증가하게 되며 배열의 인덱스를 담당한다.

- 그 외 메소드로는 배열의 가장 끝에 있는 데이터를 반환하는 getBookAt()과 인덱스를 반환하는 getLength()가 있다.

- 마지막으로 Iterable<Book> 인터페이스를 구현한 iterator() 메소드가 존재한다.

- 이 메소드는 BookShelf 클래스에 대응하는 Iterator로서, BookShelfIterator 클래스의 인스턴스를 생성하여 반환한다.

- 책장에 꽂혀있는 책을 반복해서 처리하고자 할 때 이 iterator() 메소드를 호출.

 

BookShelfIterator

- BookShelf 클래스의 검색을 실행하는 클래스이다.

- 생성자에서는 BookShelf 인스턴스를 인수로 받아 bookShelf 필드에 담아주고 index 필드는 0으로 초기화 한다.

- Iterator<Book> 인터페이스를 구현한다.

- hasNext() 메소든느 다음 책이 있는지 확인하고 다음 책이 있다면 true, 없다면 false를 리턴하도록 한다.

- next() 메소드는 다음 책이 존재하지 않는다면 NoSuchElementException을 발생시키고 존재한다면 해당 객체를 반환한다.

- 반환 전 index 필드 값을 하나 미리 증가시켜 다음 next() 를 준비하도록 한다.

 

Iterable<E>

- 처리를 반복할 대상을 나타내는 것으로 java.lang에 선언되어 있다.

- 이 인터페이스의 구현체는 배열같은 집합체가 된다. 예제에서는 Book을 모은 인터페이스를 사용하기 때문에 Iterable<Book>으로 선언한다.

- Iterable 인터페이스에는 iterator 메소드가 선언되어있는데 집합체에 대응하는 Iterator<E>를 만들도록 하기 위함이다.

- 집합체에 포함된 요소를 하나하나 처리해나가고자 할 때는 iterator 메소드를 사용해 Iterator<E> 인터페이스를 구현한 클래스의 인스턴스를 하나 만든다.

 

Iterator<E>

- 하나하나의 요소 처리를 반복하기 위한 것으로 루프 변수(i) 와 같은 역할을 한다.

- 선언되는 메소드로는 hasNext()와 next()가 있다.

- hasNext()는 다음 요소가 존재하는지 여부에 대한 것이고 next()는 다음 요소를 가져오는 것이다.

- next()의 경우는 보이지 않는 곳에서 다음에 다시 호출되었을 때 다음 요소를 반환할 수 있도록 사전에 미리 준비를 하게 된다.

 

 

예제 프로그램에서의 역할들

Iterator(반복자) 역 - Iterator<Book>

- 요소를 순서대로 검색하는 인터페이스를 결정한다.

- 다음 요소의 존재여부를 파악하는 hasNext()와 다음 요소를 가져오는 next()를 결정한다.

 

ConcreteIterator(구체적인 반복자) 역 - BookShelfIterator

- Iterator 인터페이스를 구현하는 구현체 역할이다.

- 검색에 필요한 정보를 가지고 잇어야 하기 때문에 BookShelf 클래스의 인스턴스를 bookShelfIterator 클래스의 bookShelf 필드에서 기억하고, 검색중인 위치를 index 필드에서 기억한다.

 

Aggregate(집합체) 역 - Iterable<Book>

- Iterator를 만들어내는 인터페이스를 결정한다.

- 이 인터페이스는 내가 가진 요소를 차례대로 검색해 주는 것을 만들어내는 메소드이다.

 

ConcreteAggregate(구체적인 집합체) 역 - BookShelf

- Aggregate가 결정한 Iterable 인터페이스를 실제로 구현하는 구현체 역할이다.

- 구체적인 Iterator 역할인 ConcreateIterator의 인스턴스를 만들어 낸다.

 

 

for 문 대신 Iterator를 사용하는 이유

Iterator도 for문처럼 반복을 통해 다음 값을 가져오게 되는데 굳이 Iterator를 써야 하는 이유가 무엇일까.

이유는 구현과 분리해서 사용할 수 있기 때문이다.

Iterator의 hasNext()와 next()는 Iterator의 메소드일 뿐이지 실질적인 BookShelf의 구현에 사용되는 것은 아니다.

또한 Iterator 인터페이스는 BookShelf의 메소드에 접근해야할 이유도 없다.

 

기존 예제는 배열을 사용하고 있지만 만약 리스트로 수정하고자 한다면?

Iterator를 사용하는 경우에는 BookShelf에서 Book[] 을 List<Book>으로 수정하고 appendBook에서 this.books.add로 수정만 해주면 된다.

import java.util.Iterator;

public BookShelf implements Iterable<Book> {
    //private Book[] books;
    private List<Book> books;
    
    private int last = 0;
    
    public BookShelf(int maxSize) {
        this.books = new Book[maxSize];
    }
    
    public Book getBookAt(int index) {
        return books[index];
    }
    
    public void appendBook(Book book) {
        //this.books[last] = book;
        this.books.add(book);
        last++;
    }
    
    public int getLength() {
        return last;
    }
    
    @Override
    public Iterator<Book> iterator() {
        return new BookShelfIterator(this);
    }
}

이렇게 BookShelf 하나만 수정하면 메인클래스 포함 다른 클래스에서는 수정이 필요없어진다.

반면, Iterator를 사용하지 않는다면 메인 메소드에서는 books[i].getName() 형태로 출력하게 되었을 것이고,

그럼 이렇게 처리되고 있는 모든 것들에 대해 books.get(i) 형태로 수정해야 했을 것이다.

Iterator를 사용하게 되면서 코드 재사용성이 높아졌다고 볼 수 있다.

또한, 자바에서는 사용하지 않는 인스턴스에 대해 GarbegeCollection을 통해 자동으로 삭제되기 때문에 iterate에 대응하는 deleteIterator 메소드는 불필요하다.

 

그리고 메인메소드에서 추가로 하단에 확장 for문을 사용한 것을 볼 수 있다.

확장 for문의 구조를 보면 for(Book book : bookShelf) 로 처리하여 간단하게 Iterator를 통한 처리와 마찬가지로 book.getName()으로 출력할 수 있는것을 볼 수 있다.

이 코드로 알 수 있는 점으로는 확장 for문은 Iterator와 동일한 처리를 하고 있는 것을 알 수 있다.

물론 bookShelf에 Book 배열이 들어가있는 것이기 때문에 동일하게 Iterator가 구현이 되어있어야 이렇게 사용할 수 있게 된다.

단지 hasNext와 next를 굳이 명시하지 않아도 된다는 편의성이 제공되는 것이다.

디자인패턴은 개발하면서 공통적으로 발생하는 문제들을 어떻게 해결 할 것인지에 대한 해결 방안으로 다양한 해결책 중에서 많은 사람들이 인정한 모범사례이다.

바로 전환될 수 있는 완성된 디자인은 아니며, 다른 상황에 맞게 사용될 수 있는 문제들을 해결하는데에 쓰이는 서술이나 템플릿이다.

디자인패턴은 객체지향 4대 특성(캡슐화, 상속, 추상화, 다형성)과 설계원칙(SOLID)을 기반으로 구현되어 있다.

 

디자인패턴은 재사용성, 가독성, 유지보수성, 확장성, 안정성과 신뢰성이라는 장점이 있다.

재사용성은 반복적인 문제에 대한 일반적인 해결책을 제공하는 것으로 이를 재사용하여 유사한 상황에서 코드를 더 쉽게 작성할 수 있다.

가독성은 명확하게 작성하여 개발자가 코드를 이해하고 유지보수하기 쉽게 만드는 것이다.

유지보수성은 코드를 쉽게 모듈화 할 수 있으며, 변경이 필요한 경우 해당 모듈만 수정하는 것으로 유지보수가 가능하도록 하는 것이다.

확장성은 새로운 기능을 추가하거나 변경할 때 기존코드를 변경하지 않고도 새로운 기능을 통합할 수 있는 것이다.

안정성과 신뢰성은 수많은 사람들이 인정한 모범사례로 검증된 솔루션이기 때문에 안정적이고 신뢰성있는 패턴이라고 할 수 있다.

 

 

디자인 패턴의 종류

디자인 패턴으로 가장 유명한 GoF 디자인 패턴에서는  크게 생성패턴(Creational Pattern), 구조패턴(Structural Pattern), 행동(행위)패턴(Behavioral Pattern)으로 분류되며 각각 5개, 7개, 11개의 패턴으로 총 23개의 패턴이 존재한다. 

 

생성패턴(Creational Pattern)

1. 싱글톤(Singleton)

2. 빌더(Builder)

3. 팩토리 메소드(Factory Method)

4. 추상 팩토리(Abstract Factory)

5. 프로토타입(Prototype)

 

구조패턴(Structural Pattern)

1. 어댑터(Adapter)

2. 브릿지(Bridge)

3. 컴포지트(Composite)

4. 데코레이터(Decorator)

5. 퍼사드(Facade)

6. 플라이웨이트(flyweight)

7. 프록시(Proxy)

 

행동(행위) 패턴(Behavioral Pattern)

1. 책임 연쇄(Chain of Responsibility)

2. 커맨드(Command)

3. 인터프리터(Interpreter)

4. 반복자(Iterator)

5. 중재자(Mediator)

6. 메멘토(Memento)

7. 옵저버(Observer)

8. 상태(State)

9. 전략(Strategy)

10. 템플릿 메소드(Template Method)

11. 방문자(Visitor)

 

 

계속해서 디자인패턴을 학습할 계획이며, 포스팅 할 때마다 위 패턴 목록에 링크를 추가할 계획이다.

 

Reference

 

[OOP] 디자인 패턴(Design Pattern)이란? - 장점 및 종류

디자인 패턴(Design Pattern)이란? 디자인 패턴은 개발하면서 발생하는 반복적인 문제들을 어떻게 해결할 것인지에 대한 해결 방안으로 실제 현업에서 비즈니스 요구 사항을 프로그래밍으로 처리하

ittrue.tistory.com

 

소프트웨어 디자인 패턴 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 소프트웨어 디자인 패턴(software design pattern)은 소프트웨어 공학의 소프트웨어 디자인에서 특정 문맥에서 공통적으로 발생하는 문제에 대해 재사용 가능한 해결

ko.wikipedia.org

 

+ Recent posts