Computer Science/디자인패턴

[Head First Design Patterns] 01 디자인 패턴 소개

계속지나가기 2020. 10. 18. 16:14
반응형

(본 강의 노트는 한빛 미디어의 <Head First Design Patterns>책을 기반으로 하고 있습니다)

01 디자인 패턴 소개

디자인 패턴의 분류

  • GoF가 디자인 패턴을 23가지로 정리하고 세 가지로 크게 분류( bold 처리 한 부분이 앞으로 자세히 다룰 패턴)

생성 패턴 (Creation Patterns)

  1. 객체의 생성 과정과 연관된 패턴
  • 추상 팩토리 (Abstaact Factory)
  • 빌더 (Builder)
  • 팩토리 메소드 (Factory Method)
  • 프로토 타입 (Prototype)
  • 싱글턴 (Singleton)

구조 패턴 (Structual Patterns)

: 클래스나 객체의 합성/ 집약에 관련된 패턴

  • 어댑터 (Adapter)
  • 브리지 (Bridge)
  • 컴포지트 (Composite)
  • 데코레이터 (Decorator)
  • 퍼사드 (Facade)
  • 플라이웨이트 (Flyweight)
  • 프록시 (Proxy)

행위 패턴(Behavioral Patterns)

: 클래스나 객체들이 상호작용하는 방법과 책임을 분산시키는 방법을 정의하는 패턴

  • 책임 연쇄 (Chain of Responsibility)
  • 커맨드 (Command)
  • 인터프리터 (Interpreter)
  • 반복자 (Iterator)
  • 미디에이터 (Mediator)
  • 메멘토 (Memento)
  • 옵서버 (Observer)
  • 스테이트 (State)
  • 스트래티지 (Strategy)
  • 템플릿 메소드 (Template Method)
  • 비지터 (Visitor)

Strategy Pattern : 스트래티지 패턴

목적

  • 같은 종류의 작업을 하는 알고리즘을 정의, 캡슐화, 또한 서로 바꿔 알고리즘을 사용할 수 있도록 함
  • 알고리즘을 사용하는 클라이언트로부터 독립적으로 알고리즘을 바꿔서 적용시킬 수 있도록 함

Policy Pattern이라고 부르기도 함

  • 여러 정책(policy)이 존재하고, 상황에 따라 적합한 정책을 적용시킴

서로 다른 알고리즘들이 존재하고, 실행 중 적합한 알고리즘을 선택해서 적용

  1. 클라이언트에 모든 알고리즘을 포함시킨다면 코드의 양이 늘어나고 복잡해짐
  • 즉 유지 보수가 어려워짐
  1. 모든 알고리즘이 동시에 사용되는 것이 아니라면 굳이 같이 넣을 이유 없음
  2. 새로운 알고리즘 추가가 어려움. 기존 코드를 수정해야 함

  1. 조리법이 다른 경우
  2. 파일의 압축 방법이 다른 경우
  3. 영화를 보는 방식이 다른 경우
  4. 자바의 정렬
  • Comparator Interface를 이용하는 경우
  • 서로 다른 비교 방법을 구현하고 실행 시점에 적절한 방법을 선택

사례1 - Duck(HFDP Ch.1)

Version 1

  1. simUDuck 이라는 오리 연못 시뮬레이션 게임 개발
  • 헤엄치고 꽥꽥 거리는 소리를 내는 다양한 오리가 있음
  1. Duck 클래스를 구성하고 이로부터 상속받아 다른 클래스를 만듦

코드

class Duck{
 void swim(){
     System.out.println("swimming");
 }
 void quack(){
     System.out.println("quack");
 }
 void display(){
     System.out.println("Duck");
 }
}
class MallardDuck extends class{
 @Override
 void display(){
     System.out.println("MallardDuck");
 }
}
class RedheadDuck extends class{
 @Override
 void display(){
     System.out.println("RedheadDuck");
 }
}
public class Main {
    public static void main(String[] args) {
    // write your code here
        Duck d1 = new Duck();
        Duck d2 = new MallardDuck();
        Duck d3 = new RedheadDuck();
        d1.display();
        d2.display();
        d3.display();
        d1.quack();
        d2.quack();
        d3.quack();
    }
}

Version 2: 오리를 날게 하고 싶음

  1. 상위 클래스인 Duck에 fly() 기능 추가
  • 단 부모 클래스에 기능이 추가되었으므로 부모,자식 클래스 모두 다시 컴파일 과정이 필요

    Duck class code

    class Duck{
     void swim(){
         System.out.println("swimming");
     }
     void quack(){
         System.out.println("quack");
     }
     void display(){
         System.out.println("Duck");
     }
     // version 2. new code : Duck can fly
     void fly(){
         System.out.println("fly");
     }
    }

    장난감 오리 클래스를 추가함 : RubberDuck

    class RubberDuck extends class{
     @Override
     void display(){
         System.out.println("RubberDuc");
     }
     @Override
     void quck(){
         System.out.println("squeck");
     }
    }

    문제점! 원하지 않는 자식 클래스에 fly() 기능이 추가됨.

    해결방법 : fly() 함수를 Rubber Duck클래스내에서 오버라이드 하자

    //version 3. new code : rubberDuck cannot fly
    @Override
    void fly(){
      System.out.println("cannot fly");
    }

Version 3 & 4

  1. 인터페이스를 이용한다면? (혹은 추상클래스)

  • 인터페이스에 코드를 넣을 수 없으므로 같은 코드를 반복해서 구현하는 경우가 발생 할 수 있음
  • 자바8 에서는 디폴트 메소드를 이용하면 일부 해결이 가능하긴 함(Version 4)

스트래티지 패턴 이용하기 : 바뀌는 부분과 그렇지 않은 부분 분리하기

  1. Duck 클래스에서 fly(), quack() 부분이 자주 바뀜
  2. 나머지 코드는 변함없음
  3. 변화하는 부분과 그대로 있는 부분을 분리하려면 두 개의 클래스 집합을 만들어야 함
  • 각 클래스 집합에는 각각의 행동을 구현한 것을 넣을 것
  • 특정 행동을 Duck 클래스에서 구현하는 것이 아니라, 독립적으로 새로운 클래스를 만들어 구현
  • Duck 클래스 또는 서브 클래스에서는 행동을 실제로 구현한 인터페이스를 사용

Version 5

캡슐화된 꽥꽥거리는 행동과 나는 행동
클라이언트

  • ModelDuck의 생성자에서 생성한 FlyBehavior을 사용하고 있다.
    : flyBehavior = new FlyNoWay();
  • 또한 set 함수를 통해서 이후 FlyBehavior를 수정할 수 있다.
    : model.setFlyBehavior(new FlyRocketPowered());

사례2 -라면 조리

조리 방법

  1. 기본 조리
  2. 볶음 라면
  3. 치즈 라면
  4. 식초 라면
  5. 우유 라면

Version 1

  1. 클라이언트에 모든 조리법을 넣고 조건문으로 조리법 선택
  2. 문제점
  • 새로운 조리 방법 추가 어려움
  • 클라이언트 클래스 코드가 너무 복잡해짐
  • Ramen 코드
class Ramen {
    public static enum CookingMode {
        GENERAL,
        WITHOUT_BROTH,
        WITH_CHEESE,
        WITH_VINEGAR,
        WITH_MILK
    }

    private CookingMode mode;

    Ramen() {
        mode = CookingMode.GENERAL;
    }

    public void setCookMode(CookingMode mode) {
        this.mode = mode;
    }
    public void cook() {
      switch (mode) {
          case GENERAL:
              cookWithGeneralRecipe();
              break;
          case WITHOUT_BROTH:
              cookWithoutBroth();
              break;
          case WITH_CHEESE:
              cookWithCheese();
              break;
          case WITH_VINEGAR:
              cookWithVinegar();
              break;
          case WITH_MILK:
              cookWithMilk();
              break;
      }
    private void cookWithGeneralRecipe() {
            System.out.println("일반 조리법으로 끓이기");
        }
        private void cookWithoutBroth() {
            System.out.println("물을 적게 넣고 라면을 익힌 뒤에 라면 스프에 볶듯이 끓임");
        }
        private void cookWithCheese() {
            System.out.println("라면을 끓인 후에 치즈 넣기");
        }
        private void cookWithVinegar() {
            System.out.println("라면을 끓인 후에 식초 약간 넣기");
        }
        private void cookWithMilk() {
            System.out.println("우유를 넣고 끓이기");
        }
    }

}
  • Main 코드
public class Main {
    public static void main(String[] args) {
        Ramen cook = new Ramen();
        cook.cook();     

        cook.setCookMode(
                 Ramen.CookingMode.WITH_CHEESE);
        cook.cook();
    }
}

Version 2

  1. 상속 사용

  1. 문제점
  • 음식 모형을 추가한다면? cook()을 오버라이드해서 실제 요리하지 않도록 해야함
  • 새로운 클래스가 추가될때마다 cook() 함수를 확인해야함

Version 3

  1. 인터페이스를 이용해서 변화하는 부분을 캡슐화시킴

  1. Ramen 클래스에서는 변화하는 부분을 바꿔서 사용할 수 있도록 처리
  2. cook() 멤버 함수에서 Recipe의 cook()함수를 호출
  3. CookRecipe 코드
interface CookRecipe {
    public void cook();
}
  1. Ramen 코드
class Ramen {
    CookRecipe recipe = new GeneralRamenRecipe();
    public void setRecipe(Recipe recipe) {
        this.recipe = recipe;
    }
    public void cook() {
        recipe.cook();
    }
}

 

6. CookRecipe를 구현하는 클래스 코드

class GeneralRamenRecipe implements Recipe {
    public void cook() {
        System.out.println("일반 조리법으로 끓이기");
    }
}
class RamenWithoutBrothRecipe implements Recipe {
    public void cook() {
        System.out.println("물을 적게 넣고 라면을 익힌 뒤에 라면 스프에 볶듯이 끓임");
    }
}
class CheeseRamenRecipe implements Recipe {
    public void cook() {
        System.out.println("라면을 끓인 후에 치즈 넣기");
    }
}
class VinegarRamenRecipe implements Recipe {
    public void cook() {
        System.out.println("라면을 끓인 후에 식초 약간 넣기");
    }
}
class MilkRamenRecipe implements Recipe {
    public void cook() {
        System.out.println("우유를 넣고 끓이기");
    }
}

 

7. Main 코드

public class Main {
    public static void main(String[] args) {
        Ramen cook = new Ramen();
        cook.cook();
        cook.setRecipe(new CheeseRamenRecipe());
        cook.cook();
    }
}

디자인 패턴 요소

패턴이 필요한 경우

  1. 경우에 따라 서로 다른 여러 알고리즘이 존재
  2. 알고리즘이 실행 시점에 결정되어져서 조건문 등을 이용해서 다른 알고리즘을 선택하는 경우

요소

설명

이름

스트래티지(Strategy)

문제

알고리즘의 다른 버전이 존재해서, 중복으로 존재하거나 if문을 이용해서 선택해야 함. OCP 위반

해결방안

중복을 공통화시키고, 실행 시점에 맞는 알고리즘을 호출하도록 함 (상속 또는 인터페이스 활용)

결과

OCP. 수정할 경우 Strategy를 추가하고, 나머지는 변경하지 않아도 됨

Strategy Pattern

Context class

  1. 캡슐화된 알고리즘을 멤버 변수로 포함
  2. 캡슐화된 알고리즘을 교환해서 적용시킬 수 있음

Strategy class(인터페이스)

  1. 컴파일 시점에서 사용하는 캡슐화된 알고리즘을 나타냄
  2. 실제 구현은 하위 Strategy_n 클래스에 위임
  3. 인터페이스 또는 클래스(추상)로 구현 가능

Strategy_n

  1. 실행 시점에 적용될 알고리즘을 캡슐화
  2. Context에서 실행될 알고리즘을 구현
반응형