반응형
디자인 패턴이란?
- 소프트웨어를 설계할 때 특정 맥락에서 자주 발생하는 고질적인 문제들이 또 발생했을 떄 재사용할 수 있는 해결책
- 이미 만들어져서 잘 되는 것을 처음부터 다시 만들 필요가 없다는 의미이다.
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 로 이루어진 디자인 패턴
- 구성 요소를 세 가지 역할로 구분하여 개발 프로세스에서 각각의 구성 요소에만 집중해서 개발 할 수 있다.
장점과 단점
장점 : 재사용성과 확장성이 용이하다
단점 : 복잡해질수록 모델과 뷰의 관계가 복잡해진다.