카테고리 없음

디자인 패턴 (Design Pattern) 정리

JUNGKEUNG 2025. 1. 5. 11:36
반응형

디자인 패턴이란?

  • 소프트웨어를 설계할 때 특정 맥락에서 자주 발생하는 고질적인 문제들이 또 발생했을 떄 재사용할 수 있는 해결책
  • 이미 만들어져서 잘 되는 것을 처음부터 다시 만들 필요가 없다는 의미이다.
Gof(Gang of Four)
사인방(Gang of Four, 줄여 Gof) 불리는 에리히 감마(Erich Gamma), 리처드 헬름(Richard Helm), 랄프 존슨(Ralph Johnson), 존 블리시데스 (John Vissides)가 같이 썻다

 

싱글톤 패턴

  • 하나의 클래스에 오직 하나의 인스턴만을 가지는 패턴

 

장점과 단점

장점 : 하나의 인스턴스를 기반으로 다른 모듈들은 해당 인스턴스를 공유하여 사용하기 때문에 인스턴스 생성에 드는 비용 절감

 

단점 : 의존성/종속성 향상 (인스턴스가 변경되면 그 이후에 사용되는 인스턴스도 변경되어야 함) -> 의존선 주입으로 해결

 

 

의존성 주입

  • 모듈 간의 결합을 느슨하게 만들어 디커플링 (의존성 낮춤)
  • 메인 모듈이 직접 다른 하위 모듈에 의존성을 주기보다 중간에 의존성 주입자를 만들어 간접적으로 의존성을 주입하여 클라이언트가 서비스 구성 방법을 알 필요가 없게 한다.

 


팩토리 패턴

  • 객체 생성 처리를 서브 클래스로 분리 해 처리하는 캡슐화 하는 패턴
    • 객체의 생성 코드를 별도의 클래스/메서드로 분리함으로써 객체 생성의 변화에 대비하는데 유용
    • 특정 기능의 구현은 개별 클래스를 통해 제공되는 것이 바람직한 설계
      • 기능의 변경이나 상황에 따른 기능의 선택은 해당 객체를 생성하는 코드의 변경을 초래한다.
      • 상황에 따라 적절한 객체를 생성하는 코드는 자주 중복될 수 있다.
      • 객체 생성 방식의 변화는 해다되는 모든 코드 부분을 변경해야 하는 문제가 발생 

 

// Step 1. Product 인터페이스 생성
interface Animal {
    void speak();
}

// Step 2. Product 구현 클래스 생성
class Dog implements Animal {
    @Override
    public void speak() {
        System.out.println("멍멍!");
    }
}

class Cat implements Animal {
    @Override
    public void speak() {
        System.out.println("야옹!");
    }
}

class Duck implements Animal {
    @Override
    public void speak() {
        System.out.println("꽥꽥!");
    }
}

// Step 3. 팩토리 클래스 생성
class AnimalFactory {
    // 팩토리 메서드
    public static Animal createAnimal(String type) {
        if (type.equalsIgnoreCase("dog")) {
            return new Dog();
        } else if (type.equalsIgnoreCase("cat")) {
            return new Cat();
        } else if (type.equalsIgnoreCase("duck")) {
            return new Duck();
        } else {
            throw new IllegalArgumentException("알 수 없는 동물 타입: " + type);
        }
    }
}

// Step 4. 팩토리 패턴 테스트
public class FactoryPatternExample {
    public static void main(String[] args) {
        // 팩토리를 통해 객체 생성
        Animal dog = AnimalFactory.createAnimal("dog");
        Animal cat = AnimalFactory.createAnimal("cat");
        Animal duck = AnimalFactory.createAnimal("duck");

        // 동물 소리 출력
        dog.speak();  // 멍멍!
        cat.speak();  // 야옹!
        duck.speak(); // 꽥꽥!
    }
}

 

 

탬플릿 메소드 패턴

  • 여러 클래스에서 공통으로 사용하는 메서드를 템플릿화 하여 상위 클래스에서 정의하고, 하위 클래스마다 세부 동작 사항을 다르게 구현하는 패턴
  • 변하지 않는 기능(템플릿)은 상위 클래스에 만들어두고 자주 변경되며 확장할 기능은 하위 클래스에서 만들도록 하고, 상위 메소드 실행 동작 순서는 고정하면서 세부 실행 내용은 다양화 될 수 있는 경우에 사용된다.
  • 템플릿 메서드 패턴은 상속이라는 기술을 극대화하여, 알고리즘 뼈대를 맞추는 것에 초점을 둔다. 이미 수많은 프레임워크에서 많은 부분에 템플릿 메소드 패턴 코드가 우리도 모르게 적용되어 있다.

  • AbstractClass(추상 클래스) : 템플릿 메소드를 구현하고, 템플릿 메소드에서 돌아가는 추상 메서드를 선언한다. 이 추상 메서드는 하위 클래스인 ConcreteClass 역할에 의해 구현된다.
  • ConcreteClass(구현 클래스) : AbstractClass를 상속하고 추상메서드를 구체적으로 구현한다. ConcreteClass에서 구현한 메소드는 AbstactClass의 템플릿 메소드에서 호출 된다.
// Step 1. 추상 클래스 생성 (템플릿 메서드 정의)
abstract class Beverage {
    // 템플릿 메서드 (final로 선언하여 변경 불가능)
    public final void prepareRecipe() {
        boilWater();      // 공통된 부분
        brew();           // 서브클래스가 구현
        pourInCup();      // 공통된 부분
        addCondiments();  // 서브클래스가 구현
    }

    // 공통된 부분 구현
    private void boilWater() {
        System.out.println("물을 끓이는 중...");
    }

    private void pourInCup() {
        System.out.println("컵에 따르는 중...");
    }

    // 서브클래스에서 구현할 메서드
    protected abstract void brew();           // 음료 준비
    protected abstract void addCondiments();  // 첨가물 추가
}

// Step 2. 구체 클래스 생성
class Coffee extends Beverage {
    @Override
    protected void brew() {
        System.out.println("커피를 내리는 중...");
    }

    @Override
    protected void addCondiments() {
        System.out.println("설탕과 우유를 추가하는 중...");
    }
}

class Tea extends Beverage {
    @Override
    protected void brew() {
        System.out.println("차를 우려내는 중...");
    }

    @Override
    protected void addCondiments() {
        System.out.println("레몬을 추가하는 중...");
    }
}

// Step 3. 템플릿 메소드 패턴 테스트
public class TemplateMethodExample {
    public static void main(String[] args) {
        // 커피 준비
        System.out.println("== 커피 준비 ==");
        Beverage coffee = new Coffee();
        coffee.prepareRecipe();

        System.out.println();

        // 차 준비
        System.out.println("== 차 준비 ==");
        Beverage tea = new Tea();
        tea.prepareRecipe();
    }
}

 


전략 패턴 ( = 정책 패턴)

  • 객체의 행위를 바꾸고 싶은 경우 직접 수정하지 않고 전략이라고 부르는 갭슐화한 알고리즘을 컨텍스트 안에서 바꿔주면서 상호 교체가 가능하게 만드는 패턴
    • 같은 문제를 해결하는 여러 알고리즘이 클래스별로 캡슐화되어 있고 이들이 필요할 때 교체할 수 있어 동일한 문제를 다른 알고리즘으로 해결할 수 있게 하는 디자인 패턴
    • 즉) 전략을 쉽게 바꿀 수 있도록 해주는 디자인 패턴
    • 특히 게임 프로그래밍에서 게임 캐릭터가 자신이 처한 상황에 따라 공격이나 행동하는 방식을 바꾸고 싶을 때 스트래티지 패턴은 매우 유용하다

// Step 1. 전략 인터페이스 정의
interface WeaponBehavior {
    void useWeapon(); // 무기 사용 메서드
}

// Step 2. 구체적인 전략 클래스 구현
class SwordBehavior implements WeaponBehavior {
    @Override
    public void useWeapon() {
        System.out.println("검을 휘두릅니다!");
    }
}

class BowBehavior implements WeaponBehavior {
    @Override
    public void useWeapon() {
        System.out.println("활을 쏩니다!");
    }
}

class KnifeBehavior implements WeaponBehavior {
    @Override
    public void useWeapon() {
        System.out.println("칼을 던집니다!");
    }
}

class AxeBehavior implements WeaponBehavior {
    @Override
    public void useWeapon() {
        System.out.println("도끼를 휘두릅니다!");
    }
}

// Step 3. 컨텍스트 클래스 (캐릭터)
abstract class Character {
    protected WeaponBehavior weaponBehavior; // 무기 행동을 설정

    public void setWeapon(WeaponBehavior weaponBehavior) {
        this.weaponBehavior = weaponBehavior; // 무기 행동을 변경 가능
    }

    public void performAttack() {
        if (weaponBehavior != null) {
            weaponBehavior.useWeapon(); // 무기 행동 실행
        } else {
            System.out.println("무기가 없습니다!");
        }
    }

    // 캐릭터별 고유 행동
    public abstract void display();
}

// Step 4. 구체적인 캐릭터 클래스
class King extends Character {
    @Override
    public void display() {
        System.out.println("나는 왕입니다.");
    }
}

class Queen extends Character {
    @Override
    public void display() {
        System.out.println("나는 여왕입니다.");
    }
}

class Knight extends Character {
    @Override
    public void display() {
        System.out.println("나는 기사입니다.");
    }
}

class Peasant extends Character {
    @Override
    public void display() {
        System.out.println("나는 농부입니다.");
    }
}

// Step 5. 전략 패턴 테스트
public class StrategyPatternExample {
    public static void main(String[] args) {
        // 캐릭터 생성
        Character king = new King();
        Character queen = new Queen();
        Character knight = new Knight();
        Character peasant = new Peasant();

        // 무기 설정
        king.setWeapon(new SwordBehavior());
        queen.setWeapon(new BowBehavior());
        knight.setWeapon(new AxeBehavior());
        peasant.setWeapon(new KnifeBehavior());

        // 캐릭터 행동 테스트
        king.display();
        king.performAttack(); // 검을 휘두릅니다!

        queen.display();
        queen.performAttack(); // 활을 쏩니다!

        knight.display();
        knight.performAttack(); // 도끼를 휘두릅니다!

        peasant.display();
        peasant.performAttack(); // 칼을 던집니다!

        // 무기 변경 테스트
        System.out.println("\n== 무기 변경: 여왕이 칼을 사용합니다 ==");
        queen.setWeapon(new KnifeBehavior());
        queen.performAttack(); // 칼을 던집니다!
    }
}

옵저버 패턴

  • 한 객체의 상태변화에 따라 다른 객체의 상태도 연동되도록 일대다 객체 의존 관계를 구성 하는 패턴
    • 데이터의 변경이 발생했을 경우 상대 클래스나 객체에 의존하지 않으며너 데이터 변경을 통보하고자 할 떄 유용

 

  • 옵저버 패턴은통보 대상 객체의 관리를 Subject(구독자) 클래스와 Obaserver 인터페이스로 일반화 한다. (ConcreteObserver)에 대한 의존성을 없앨 수 있다.
  • 결과적으로 통보 대상 클래스나 대상 객체의 변경에도 통보하는 클래스 (ConcreteSubject)를 수정 없이 그대로 사용할 수 있다.

프록시 패턴

  • 대상 객체에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 패턴
  • 이를 통해 객체의 속성, 변환 등을 보완하며 보안, 데이터 검증, 캐싱, 로깅에 사용
  • 프록시 서버 : 서버와 클라이언트 사이에서 클라이언트가 자신을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해주는 컴퓨터 시스템이나 응용 프로그램
  • AOP는 프록시 패턴으로 되어있다.

 

// Step 1. 공통 인터페이스 정의
interface Image {
    void display(); // 이미지를 표시하는 메서드
}

// Step 2. 실제 객체 (RealSubject) 정의
class RealImage implements Image {
    private String fileName;

    public RealImage(String fileName) {
        this.fileName = fileName;
        loadImageFromDisk(); // 생성 시 이미지 로드
    }

    private void loadImageFromDisk() {
        System.out.println("디스크에서 " + fileName + " 로드 중...");
        try {
            Thread.sleep(2000); // 로드 시간 시뮬레이션
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void display() {
        System.out.println(fileName + " 이미지 표시");
    }
}

// Step 3. 프록시 객체 (Proxy) 정의
class ProxyImage implements Image {
    private String fileName;
    private RealImage realImage; // 실제 객체 참조

    public ProxyImage(String fileName) {
        this.fileName = fileName;
    }

    @Override
    public void display() {
        if (realImage == null) {
            // 실제 객체를 처음 사용할 때 생성
            realImage = new RealImage(fileName);
        }
        realImage.display(); // 실제 객체에 작업 위임
    }
}

// Step 4. 프록시 패턴 테스트
public class ProxyPatternExample {
    public static void main(String[] args) {
        System.out.println("== 프록시를 사용한 이미지 로딩 ==");
        
        // 프록시 객체 생성
        Image image1 = new ProxyImage("image1.jpg");
        Image image2 = new ProxyImage("image2.jpg");

        // 이미지 표시 (지연 로딩)
        System.out.println("\nimage1 표시 요청");
        image1.display(); // 실제 객체 생성 후 표시

        System.out.println("\nimage1 다시 표시 요청");
        image1.display(); // 이미 로드된 실제 객체 사용

        System.out.println("\nimage2 표시 요청");
        image2.display(); // 실제 객체 생성 후 표시
    }
}

MVC 패턴

  • Model, View, Controller 로 이루어진 디자인 패턴
  • 구성 요소를 세 가지 역할로 구분하여 개발 프로세스에서 각각의 구성 요소에만 집중해서 개발 할 수 있다.

 

장점과 단점

장점 : 재사용성과 확장성이 용이하다

 

단점 : 복잡해질수록 모델과 뷰의 관계가 복잡해진다.