디자인패턴

디자인 패턴

JUNGKEUNG 2023. 8. 2. 07:21

프레임 워크와 라이브러리


프레임워크

전체적인 흐름을 제어하고 있으며 개발자는 그 안에서 필요한 코드를 넣어서 사용한다. 좀 더 쉽게 설명하면 원하는 기능 구현에 집중하여 개발할 수 있도록 일정한 형태와 필요한 기능을 갖추고 있는 골격, 뼈대를 의미한다.

 

EX) Spring, Django, Android..

 

라이브러리

 개발자가 전체적인 흐름을 만들며 사용한다. 소프트웨어를 개발할 때 컴퓨터 프로그램이 사용하는 비휘발성 자원의 모임. 즉, 특정 기능을 모와둔 콛,. 함수들의 집합이며 코드 작성 시 활용 가능한 도구들을 의미한다.

 

EX) JQuery, STL, npm

 

 

디자인 패턴


프로그램을 설계할 때 발생했던 문제점들을 객체 간의 상호 관계 등을 이용하여 해결할 수 있도록 하나의 '규약' 형태로 만들어 놓은 것을 의미한다.

 

 

싱글톤 패턴


하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴 이다. 하나의 클래스를 기반으로 여러 개의 개별적인 인스턴스를 만들 수 있지만, 그렇게 하지 않고 하나의 클래스를 기반으로 단 하나의 인스턴스를 만들어 이를 기반으로 로직을 만드는데 쓰이며, 보통 데이터베이스 연결 모듈에 많이 이용한다.

class Singleton{
	private static class singleInstanceHolder {
    	private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance(){
    	return singleInstanceHolder.INSTANCE;
    }
}

pbulic class HelloWorld {
	public static void main(String[] args) {
    	singleton a = Singleton.getINstance();
        singleton b = Singleton.getInstance();
        System.out.println(a.hashCode());
        Ststem.out.println(b.hashCode());
        if (a=b) {
        	System.out.println(true);
        }
   }
}

/*
705927765
705927765
true
*/

 

싱글톤 패턴의 단점


TDD(Ttest Drvien Development)를 할 때 문제가 된다. TDD를 할 때 단위 테스트를 주로 하는데, 단위 테스트는 테스트가 서로 독립적이어야 하며 테스트를 어떤 순서로든 실행할 수 있어야 한다. 하지만 싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 구현하는 패턴이므로 각 테스트마다 '독립적인' 인스턴스를 만들기가 어렵다.

 

팩토리 패턴


객체를 사용하는 코드에서 객체 생성 부분을 떼어내 추상화한 패턴이자 상속 관계에 있는 두 클래스에서 사우이 클래스가 중요한 뼈대를 결정하고, 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정하는 패턴이다. 

 상위 클래스와 하위 클래스가 분리되기 때문에 느슨한 결합을 가지며 상위 클래스에서는 인스턴스 생성 방식에 대해 전혀 알 필요가 업식 때문에 더 많은 유연성을 가지게 된다. 

 

abstract class Coffee {
	public abstract int getPrice();
    
    @Override
    public String toString() {
    	return "Hi this coffee is " + this.getPrice();
    }
}

class CoffeeFactory {
	public static Coffee getCoffee(String type, int price) {
    	if ("Latte".equalsIgnoreCase(type)) return new Latte(price);
        else if ("Americano".equalsIgnoreCase(type)) return new Americano(price);
        else {
        	return new DefaultCoffee();
         }
    }
}

class DefaultCoffee extends Coffee {
	private int price;
    
    public DefaultCoffee() {
    	this.price = -1;
    }
    
    @Override
    public int getPrice() {
    	return this.price;
        
   }
}

class Latte extends Coffee {
	private int price;
    
    public Latte(int price) {
    	this.price = price;
    }
    @Override
    public int getPrice() {
    	return this.price;
    }
}

class Americano extends Coffee {
	private int price;
    
    public Americatno(int price) {
    	this.price = price;
    }
    @Override
    public int getPrice() {
    	return this.price;
    }
}

pbulic class HelloWold {
	public static void main(String[]args) {
    	Coffee latte = CoffeeFactory.getCoffee("Latte", 4000);
        Coffee ame = CoffeeFactory.getCoffee("Americano", 3000);
     	System.out.println("Cactory lattee ::"+latte);
        System.out.printIn("Factory ame :: + ame);
    }       
}
/*
Factory lattee : Hi this coffee is 4000
Factory ame :: Hi this coffee is 3000
*/

 

 

전략 패턴


전략패턴은 정채패턴이라고도 하며 객체의 행위를 바꾸고 싶은 경우 '직접'수정하지 않고 전략이라고 부르는 '캡슐화한 알고리즘'을 컨텍스트안에서 바꿔주면서 사옿 교체가 가능하게 만드는 패턴이다.

RandomGeneratable.java

package racingcar.util;

public interface RandomGeneratable {
    int generateNumber(int min, int max);
}

RandomGenerator.java

package racingcar.util;

import java.util.Random;

public class RandomGenerator implements RandomGeneratable {
    private static final Random random = new Random();

    public int generateNumber(int min, int max) {
        return RandomGenerator.random.nextInt(max - min) + min;
    }
}

RandomGeneratorStub.java

package racingcar.util;

public class RandomGeneratorStub implements RandomGeneratable {
    private static final int[] randomNumbers = {2, 3, 4, 5};
    private int sequenceCursor = 0;

    public int generateNumber(int min, int max) {
        int currentNumber = randomNumbers[sequenceCursor % randomNumbers.length];
        sequenceCursor++;

        return currentNumber;
    }
}

전략 패턴 ( Strategy Pattern )

객체들이 할 수 있는 행위 각각에 대해 전략 클래스를 생성하고, 유사한 행위들을 캡슐화 하는 인터페이스를 정의하여,

객체의 행위를 동적으로 바꾸고 싶은 경우 직접 행위를 수정하지 않고 전략을 바꿔주기만 함으로써 행위를 유연하게 확장하는 방법을 말합니다.

 

간단히 말해서 객체가 할 수 있는 행위들 각각을 전략으로 만들어 놓고, 동적으로 행위의 수정이 필요한 경우 전략을 바꾸는 것만으로 행위의 수정이 가능하도록 만든 패턴입니다.

 

  • Strategy: 인터페이스나 추상 클래스로 외부에서 동일한 방식으로 알고리즘을 호출하는 방법을 명시
  • ConcreateStrategy1, 2, 3: 스트래티지 패턴에서 명시한 알고리즘을 실제로 구현한 클래스
  • Context: 스트래티지 패턴을 이용하는 역할 수행. 필요에 따라 동적으로 구체적인 전략을 바꿀 수 있도록 setter 메서드 제공

 

전략 패턴 적용 전

"무궁화 꽃이 피었습니다" 를 실행하는 프로그램이 있다.

 

이 게임은 "무궁화 꽃이" 까지는 파란불이며, "피었습니다." 까지는 빨간불로 구현을 하였고 여기에 Speed 라는 필드를 정의하여 "무궁화 꽃이" 혹은 "피었습니다." 를 말하는 속도를 조절 할 수 있도록 하는 기능을 넣었다.

 

 

BlueLightRedLight

public class BlueLightRedLight {
  private int speed;

  public BlueLightRedLight(int speed) {
    this.speed = speed;
  }
  
  public void blueLight() {
    if (speed == 1) {
      System.out.println("무 궁 화 꽃 이");
    } else if (speed == 2) {
      System.out.println("무궁화 꽃이");
    } else if (speed == 3){
      System.out.println("무광꼬치");
    }
  }
  
  public void redLight() {
    if (speed == 1) {
      System.out.println("피 었 습 니 다.");
    } else if (speed == 2) {
      System.out.println("피었습니다.");
    } else if (speed == 3){
      System.out.println("펴씀다");
    }
  }
}

 

 

문제점

speed 에 따라 분기를 나눠야하고 다르게 행동해야 한다. 

또한 다르게 행동하기 위해 Client 에서 Speed 를 1, 2, 3 과 같은 구체적인 숫자를 바꿔줘야하고 새로운 Speed 4 가 생기면 기존 코드를 수정해야 하는 문제가 발생한다.

 

이를 전략 패턴을 이용하여 해결해보자.

 

 

 

전략 패턴 적용 후


먼저 Strategy 인터페이스를 정의한다.

// Strategy
public interface Speed {
  void blueLight();
  void redLight();
}

 

 

ConcreteStrategy 를 구현하자.

Slow, Normal, Faster 를 정의하여 이전에 방식의 speed 1, 2, 3 에 해당되는 기능을 구현해준다.

public class Slower implements Speed {
  @Override
  public void blueLight() {
    System.out.println("무 궁 화 꽃 이");
  }

  @Override
  public void redLight() {
    System.out.println("피 었 습 니 다.");
  }
}
public class Normal implements Speed {
  @Override
  public void blueLight() {
    System.out.println("무궁화꽃이");
  }

  @Override
  public void redLight() {
    System.out.println("피었습니다.");
  }
}
public class Faster implements Speed {
  @Override
  public void blueLight() {
    System.out.println("무광꼬치");
  }

  @Override
  public void redLight() {
    System.out.println("펴씀다.");
  }

 

 

Context 인 BlueLightRedLight 는 아래와 같다.

// Context
public class BlueLigthRedLight {
  private Speed speed;

  public BlueLigthRedLight(Speed speed) {
    this.speed = speed;
  }
  
  public void blueLight() {
    speed.blueLight();
  }
  
  public void redLight() {
    speed.redLight();
  }
}

생성자에서 Speed 타입 클래스 (Slower, Normal, Faster) 중 하나를 받아 그에 따르는 로직을 처리한다.

 

 

Client 는 아래와 같이 호출 할 수 있다.

public class Client {
  public static void main(String[] args) {
    BlueLigthRedLight blueLigthRedLight = new BlueLigthRedLight(new Slower());
    blueLigthRedLight.blueLight();
    blueLigthRedLight.redLight();
  }
}

 

 

만약, BlueLight 와 RedLight 의 속도를 다르게 하고 싶으면 생성자가 아닌 메소드에서 Speed 타입 클래스를 인자로 받으면 된다.

public class BlueLigthRedLight {

  public void blueLight(Speed speed) {
    speed.blueLight();
  }

  public void redLight(Speed speed) {
    speed.redLight();
  }
}
public class Client {
  public static void main(String[] args) {
    BlueLigthRedLight blueLigthRedLight = new BlueLigthRedLight();
    blueLigthRedLight.blueLight(new Slower());
    blueLigthRedLight.redLight(new Faster());
  }
}

 

 

 

장점과 단점


장점

  • 새로운 전략을 추가하더라도 기존 코드를 변경하지 않는다. (OCP)
  • 상속 대신 위임을 사용할 수 있다.
  • 런타임에 전략을 변경할 수 있다.

 

단점

  • 복잡도가 증가한다.
  • 클라이언트 코드가 구체적인 전략을 알아야 한다

옵저버 패턴이란?


  • 객체의 상태 변화를 관찰하는 관찰자들 즉, 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다.
  • 어떤 객체의 변경 사항이 발생하였을때 이와 연관된 객체들에게 알려주는 디자인 패턴이라고 생각하면 된다.

 

다수의 객체가 특정 객체 상태 변화를 감지하고 알림을 받는 패턴

  • 발행(publish) - 구독 (subscribe) 패턴을 구현할 수 있다

 

옵저버 패턴 사용 예제

공지사항을 알리때를 예제로 공지사항을 전파할때(상태변환) 옵저버와 관련된 객체들(유저들)에게 통지하도록 하는 간단한 예제를 옵저버 패턴으로 만들어보도록 하겠다.

Observer.class

public class Observer {
    public String msg;

    public void receive(String msg){
        System.out.println(this.msg + "에서 메시지를 받음 : " + msg);
    }
}

옵저버 클래스를 정의하여 공지사항을 받았을 경우 알림을 출력하는 receive함수를 만듭니다.

User1.class

public class User1 extends Observer{

    public User1(String msg){
        this.msg = msg;
    }
}

Observer를 상속받는 user1을 만듭니다.

User2.class

public class User2 extends Observer{

    public User2(String msg) {
        this.msg = msg;
    }
}

Observer를 상속받는 user2을 만듭니다.

Observer.class

import java.util.ArrayList;
import java.util.List;

public class Notice {
    private List<Observer> observers = new ArrayList<Observer>();

    // 옵저버에 추가
    public void attach(Observer observer){
        observers.add(observer);
    }

    // 옵저버에서 제거
    public void detach(Observer observer){
        observers.remove(observer);
    }

    // 옵저버들에게 알림
    public void notifyObservers(String msg){
        for (Observer o:observers) {
            o.receive(msg);
        }
    }
}

옵저버에 공지사항을 받을 유저를 추가하고 삭제하며 공지사항을 전파하는 기능을 가진 Notice클래스를 만듭니다.

Main.class

public class Main {
    public static void main(String[] args) {
        Notice notice = new Notice();
        User1 user1 = new User1("유저1");
        User2 user2 = new User2("유저2");

        notice.attach(user1);
        notice.attach(user2);

        String msg = "공지사항입니다~!";
        notice.notifyObservers(msg);

        notice.detach(user1); // user1 공지사항 받는 대상에서 제거
        msg = "안녕하세요~";
        notice.notifyObservers(msg);
    }
}

 

 

장점

  • 실시간으로 한 객체의 변경사항을 다른 객체에 전파할 수 있다.
  • 느슨한 결합으로 시스템이 유연하고 객체간의 의존성을 제거할 수 있다

 

단점

  • 너무 많이 사용하게 되면, 상태 관리가 힘들 수 있다.
  • 데이터 배분에 문제가 생기면 자칫 큰 문제로 이어질 수 있다.