디자인패턴에 대해 알아보자

profile image 스이연 2025. 2. 9. 03:28

CS 공부를 해오면서 종종 언급되어 오던 디자인 패턴에 대해서 공부해보려 한다.
그동안 디자인 패턴 공부를 아주 짧게 하거나 거의 소홀히 해왔었는데 코드를 작성하다 보면 좀 더 깔끔하고 효율적인 코드 형식을 요구하는 때가 종종 생겨 디자인 패턴을 공부해보고 싶어졌다!

디자인 패턴

다자인 패턴이란, 프로그램을 설계할 때 발생했던 문제점들을 객체 간의 상호 관계등을 이용하여 해결할 수 있도록 하나의 규약 형태로 만들어 둔 것

왜 사용하나요

  1. 개발자들 간의 소통이 원활해진다.
    무언가를 설명할 때 빠르고 정확한 소통이 가능해진다.
    예를 들어,
    '오리들의 행동들을 쉽게 확장하거나 변경할 수 있는 클래스들의 집합으로 캡슐화 되어 있습니다.'
    = '오리들의 다양한 행동들을 전략 패턴으로 전략 패턴으로 구현하고 있습니다.'
    이렇게 본인의 코드를 상대방에게 명확하고 빠르게 설명할 수 있다는 장점이 있다.
  2. 재사용성을 높이고 변경이 쉬워진다.
    처음 보는 문제에 대해서도 단점을 최소화 시킬 수 있고 변화에 유연하게 대응이 가능하다.

싱글톤 패턴 (Singleton Pattern)

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

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

public class HelloWorld {
	public static void main(String[] args){
		// Singleton 이라는 하나의 인스턴스를 기반으로 a, b 모듈을 생성
		Singleton a = Singleton.getInstance();
		Singleton b = Singleton.getInstance();
		System.out.println(a.hashCode()); // 1234
		System.out.println(b.hashCode()); // 1234
		if(a==b){
			System.out.println(true); // true
		}
	}
}

단점

  • TDD (Test Driven Development) 를 할 때 좋지 않다.
💡TDD란
     테스트 주도 개발
     작은 단위의 테스트 케이스(단위 테스트)를 작성하고 이를 통과하는 코드를 추가하는 단계를 반복하여 구현한다.

 

TDD를 할 때 단위 테스트를 주로 하게 되는데 단위 테스트는 서로 독립적이어야 하며 순서 상관 없이 테스트가 실행되어야 한다.

싱글톤 패턴 특성상 미리 생성된 하나의 인스턴스를 기반으로 구현하는 특징이 있기 때문에 각 테스트마다 각각의 독립적인 인스턴스를 만들기 힘들다!

 

  • 모듈 간의 결합을 강하게 만들 수 있다.

해결하기 위해서는 모듈간의 결합을 느슨하게 만드는 작업이 필요! → 의존성 주입 (DI: Dependency Injection)

💡의존성 주입이란
     메인 모듈이 직접 다른 하위 모듈에 의존성을 주기 보다 중간에 의존성 주입자가 이 부분을 대신해서
     메인 모듈이 간접적으로 의존성을 주입하는 방식.
  • 의존성 주입의 장점
    • 테스팅하기가 쉽다.
    • 마이그레이션하기가 수월하다.
    • 모듈간의 관계들이 조금 더 명확해진다.
  • 의존성 주입의 단점
    • 모듈들이 더욱 분리되기 때문에 클래스 수가 늘어나 복잡성이 증가 될 수 있음
  • 의존성 주입 원칙
    • 상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 한다.
    • 상위, 하위 둘 다 추상화에 의존해야하며 추상화는 세부사항에 의존하지 말아야 한다.

팩토리 패턴 (Factory Pattern)

객체를 사용하는 코드에서 객체 생성 부분을 떼어내서 추상화한 패턴

상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대 역할, 하위 클래스에서 객체 생성 관련 구체적인 내용을 결정한다.

 

즉, 상위 하위 클래스가 분리 된다.

→ 두 클래스는 느슨한 결합을 가진다.

→ 상위 클래스에서는 인스턴스 생성 방식에 대해 알 필요가 없어져 더 많은 유연성을 가진다.

 

객체 생성 로직이 따로 떼어져 있기 때문에 코드를 리팩토링 하더라도 한 곳만 고칠 수 있게 되어 유지 보수성이 증가한다.

enum CoffeeType {
	LATTE,
	ESPRESSO
}

// 상위 클래스
abstract class Coffee {
	protected String name;
	
	public String getName() {
		return name;
	}
}

// Coffee 클래스 상속 (하위 클래스)
class Latte extends Coffee {
	public Latte() {
		name = "latte";
	}
}

// Coffee 클래스 상속 (하위 클래스)
class Espresso extends Coffee {
	public Espresso() {
		name = "espresso";
	}
}

class CoffeeFactory {
	public static Coffee createCoffee(CoffeeType type){
		switch(type) {
			case LATTE:
				return new Latte();
			case ESPRESSO:
				return new Espresso();
			default:
				throw new IllegalArgumentExceptioin("Invalid coffee type: " + type);
		}
	}
}

public class Main {
	public static void main(String[] agrs){
		Coffee coffee = CoffeeFactory.createCoffee(CoffeeType.LATTE);
		System.out.println(coffee.getName());
	}
}

전략 패턴 (Strategy Pattern)

객체의 행위를 바꾸고 싶을 경우 직접 수정하지 않고 전략이라고 부르는 캡슐화한 알고리즘을 컨텍스트 안에서 바꿔주며 상호 교체

가능하게 만드는 패턴이다.

💡 컨텍스트란
      상황, 문맥, 맥락을 의미하며, 개발자가 어떠한 작업을 완료하는데 필요한 모든 정보를 의미.
// 전략 - 추상화된 알고리즘
interface PaymentStrategy {
    void pay(int amount);
}

class KAKAOCardStrategy implements PaymentStrategy {
    private String name;
    private String cardNumber;
    private String cvv;
    private String dateOfExpiry;

    public KAKAOCardStrategy(String nm, String ccNum, String cvv, String expiryDate) {
        this.name = nm;
        this.cardNumber = ccNum;
        this.cvv = cvv;
        this.dateOfExpiry = expiryDate;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + "원 paid using KAKAOCard.");
    }
}

class LUNACardStrategy implements PaymentStrategy {
    private String emailId;
    private String password;

    public LUNACardStrategy(String email, String pwd) {
        this.emailId = email;
        this.password = pwd;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + "원 paid using LUNACard.");
    }
}
// 컨텍스트 - 전략을 등록하고 실행
class ShoppingCart {
    List<Item> items;

    public ShoppingCart() {
        this.items = new ArrayList<Item>();
    }

    public void addItem(Item item) {
        this.items.add(item);
    }
	
    // 전략을 매개변수로 받아서 바로바로 전략을 실행
    public void pay(PaymentStrategy paymentMethod) {
        int amount = 0;
        for (Item item : items) {
            amount += item.price;
        }
        paymentMethod.pay(amount);
    }
}
class Item {
    public String name;
    public int price;

    public Item(String name, int cost) {
        this.name = name;
        this.price = cost;
    }
}

// 클라이언트 - 전략 제공/설정
class User {
    public static void main(String[] args) {
        // 쇼핑카트 전략 컨텍스트 등록
        ShoppingCart cart = new ShoppingCart();

        // 쇼핑 물품
        Item A = new Item("맥북 프로", 10000);
        Item B = new Item("플레이스테이션", 30000);
        cart.addItem(A);
        cart.addItem(B);

        // LUNACard로 결제 전략 실행
        cart.pay(new LUNACardStrategy("kundol@example.com", "pukubababo")); // 4000원 paid using LUNACard.

        // KAKAOBank로 결제 전략 실행
        cart.pay(new KAKAOCardStrategy("Ju hongchul", "123456789", "123", "12/01")); // 4000원 paid using KAKAOCard.
    }
}

옵저버 패턴 (Observer Pattern)

주체가 어떤 객체의 상태 변화를 관찰하다가 상태 변화가 있을 때마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게

변화를 알려주는 디자인 패턴

  • 주체: 객체의 상태 변화를 보고 있는 관찰자
  • 옵저버들: 객체의 상태 변화에 따라 전달되는 메서드들을 기반으로 추가 변화 사항이 생기는 객체들

예를 들어 트위터 안에서 내가 누군가(주체)를 팔로우 했을 경우 그 누군가가 포스팅을 올렸을 경우(상태의 변화) 알림이 나에게 오는 것과 같다.

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

interface Subject {
	public void register(Observer obj); // 구독 추가
	public void unregister(Observer obj); // 구독 해제
	public void notifyObservers();// Subject 객체 상태 변화시 모든 옵저버들에게 알림
	public Object getUpdate(Observer obj);
}

class Topic implements Subject{
	private List<Observer> observers;
	private String message;
	
	public Topic(){
		this.observers = new ArrayList<>(); // 구독자들을 담아 관리하는 리스트
		this.message = "";
	}
	
	@Override
	public void register(Observer obj){
		if(!observers.contains(obj)) observers.add(obj);
	}
	
	@Override
	public void unregister(Observer obj){
		observers.remove(obj);
	}
	
	@Override
	public void notifyObservers(){
		this.observers.forEach(Observer::update);
	}
	
	@Override
	public Object getUpdate(Observer obj){
		return this.message;
	}
	
	public void postMessage(String msg){
		System.out.println("Message sended to Topic: " + msg);
		this.message = msg;
		notifyObservers();
	}
}

interface Observer{
	public void update();
}

class TopicSubscriber implements Observer {
	private String name;
	private Subject topic;
	
	public TopicSubscriber(String name, Subject topic){
		this.name = name;
		this.topic = topic;
	}
	
	@Override
	public void update(){
		String msg = (String)topic.getUpdate(this);
		System.out.println(name + ":: got message >> " + msg);
	}
}
public class Main {
	public static void main(String[] args){
		Topic topic = new Topic();
		Observer a = new TopicSubscriber("a", topic);
		Observer b = new TopicSubscriber("b", topic);
		Observer c = new TopiceSubscriber("c", topic);
		
		topic.register(a);
		topic.register(b);
		topic.register(c);
		
		topic.postMessage("amumu is op champion!");
	}
}

// 결과
// Message sended to Topic: amumu is op champion!
// a:: got message >> amumu is op champion!
// b:: got message >> amumu is op champion!
// c:: got message >> amumu is op champion!

프록시 패턴 (Proxy Pattern)

대상 객체에 접근하기 전 그 접근에 대한 흐름을 가로채서 해당 접근을 필터링하거나 수정하는 등의 역할을 하는 계층이 있는

디자인 패턴이다.

객체의 속성, 변환 등을 보완 할 수 있고 보안, 데이터 검증, 캐싱, 로깅에 사용된다.


이터레이터 패턴 (Iterator Pattern)

이터레이터 (Iterator)를 사용해서 컬렉션의 요소들에 접근하는 디자인 패턴이다.

순회할 수 있는 여러가지 자료형의 구조와는 상관 없이 이터레이터라는 하나의 인터페이스로 순회가 가능하다.


노출 모듈 패턴 (Revealing Module Pattern)

즉시 실행 함수를 통해 private, public 과 같은 접근 제어자를 만드는 패턴이다.

자바스크립트는 접근제어자가 존재하지 않기 때문에 노출 모듈 패턴을 통해 구현한다.


MVC 패턴

모델(Model), 뷰(View), 컨트롤러(Controller)로 이루어진 디자인 패턴이다.

  • 애플리케이션의 구성요소를 세가지 역할로 구분하여 개발 프로세스에서 각각의 구성요소에만 집중 개발이 가능하다.
  • 재사용성과 확장성이 용이하다.
  • 애플리케이션이 복잡해질수록 모델과 뷰의 관계가 복잡해진다.

모델

애플리케이션의 데이터인 데이터베이스, 상수, 변수 등을 뜻한다.

사용자 인터페이스 요소를 나타낸다.

즉 모델을 기반으로 사용자가 볼 수 있는 화면이다.

컨트롤러

하나 이상의 모델과 하나 이상의 뷰를 잇는 다리 역할을 한다.

이벤트 등록 등과 같은 메인 로직을 담당한다.

모델과 뷰의 생명 주기를 관리하고 모델이나 뷰의 변경 통지를 받으면 이를 해석해서 각각의 구성 요소에

해당 내용을 알려준다.


MVP 패턴

MVC 패턴으로부터 파생되었으며 MVC에 해당하는 컨트롤러가 프레젠터로 교체되었다.

뷰와 프레젠터는 일대일 관계이기 때문에 더욱 더 강한 결합을 지닌 디자인 패턴이다.


MVVM 패턴

MVC에 해당하는 컨트롤러가 뷰 모델로 바뀐 패턴이다.

뷰 모델은 뷰를 더 추상화한 계층이며 MVC 패턴과는 다르게 커맨드와 데이터 바인딩을 가진다.