디자인 패턴

xyzw·2024년 8월 18일
0

CS

목록 보기
1/18

0. 디자인 패턴

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

1. 싱글톤 패턴

싱글톤 패턴하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴이다. 하나의 인스턴스를 만들어 놓고 해당 인스턴스를 다른 모듈들이 공유하며 사용한다.

사례

  • 데이터베이스 연결 모듈: 매번 DB connection을 위한 객체를 생성해서 사용하는 것이 아니라, 초기에 하나를 만들어두고 이를 사용한다.
  • 스레드 풀: 스레드를 생성하는 데 시간이 오래 걸린다. 스레드 풀이 필요할 때 생성하는 것이 아니라 미리 하나를 만들어두고 스레드 풀에서 스레드를 가져다 사용한다.

장단점

  • 장점: 인스턴스를 생성할 때 드는 비용이 줄어든다. 사용하기가 쉽고 실용적이다.
  • 단점: 테스트가 서로 독립적이어야 하는 단위 테스트를 하기 어렵다. 의존성이 높아진다.

의존성 주입

의존성 주입을 통해 모듈 간의 결합을 조금 더 느슨하게 만들 수 있다.

의존성 주입은 메인 모듈이 직접 다른 하위 모듈에 대한 의존성을 주기보다는
중간에 의존성 주입자가 이 부분을 가로채 메인 모듈이 간접적으로 의존성을 주입하는 방식이다.
이를 통해 메인 모듈은 하위 모듈에 대한 의존성이 떨어지게 된다.

의존성 주입 원칙: 상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 한다. 또한, 둘 다 추상화에 의존해야 하며, 이때 추상화는 세부 사항에 의존하지 말아야 한다.

  • 장점: 모듈들을 쉽게 교체할 수 있는 구조가 디어 테스팅하기 쉽고 마이그레이션하기 수월하다.
    구현할 때 추상화 레이어를 넣고 이를 기반으로 구현체를 넣어 주기 때문에 애플리케이션 의존성 방향이 일관되고 모듈 간의 관계가 명확해진다.
  • 단점: 모듈이 더욱 분리되어 클래스 수가 늘어나 더 복잡해질 수 있고 런타임 패널티가 생기기도 한다.

2. 팩토리 패턴

팩토리 패턴은 객체를 사용하는 코드에서 객체를 직접 생성하지 않고, 객체를 생성하는 부분을 떼어내 추상화한 팩토리 객체를 사용하는 패턴이다.

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

  • 장점: 상위 클래스와 하위 클래스가 분리되므로 느슨한 결합을 가진다.
    상위 클래스에서는 인스턴스 생성 방식에 대해 알 필요가 없어 더 많은 유연성을 가진다.
    객체 생성 로직이 분리되므로 코드 리팩토링 시 한 곳만 고칠 수 있어 유지 보수성이 증가한다.

자바 코드

CoffeeFactory 밑에 Coffee 클래스를 놓고, 해당 클래스를 상속하는 Latte, Espresso 클래스를 기반으로 구현한 코드이다.

enum CoffeeType {
	LATTE,
    ESPRESSO
}

abstract class Coffee {
	protected String name;
    
    public String getName() {
    	return name;
    }
}

class Latte extends Coffee {
	public Latte() {
    	name = "latte";
    }
}

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 IllegalArgumentException("Invalid coffee type: " + type);
        }
    }
}

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

3. 전략 패턴

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

자바 코드

어떤 아이템을 살 때 결제 방식의 전략만 바꿔서 두 가지 방식(LUNACard, KAKAOCard)으로 결제하는 것을 구현한 코드이다.

interface PaymentStrategy {
	public 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.date = 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 Item {
	...
}

class ShoppingCart {
    ...
}

public class Main {
	public static void main(String[] args) {
    	ShoppingCart cart = new ShoppingCart();
        ...
        
        // pay by LUNACard
        cart.pay(new LUNACardStrategy("email", "password"));
        
        // pay by KAKAOCard
        cart.pay(new KAKAOCardStrategy("name", "cardNum", "cvv", "date"));
    }
}

4. 옵저버 패턴

옵저버 패턴은 주체(객체의 상태 변화를 보고 있는 관찰자)가 어떤 객체의 상태 변화가 있을 때마다 메서드 등을 통해 옵저버(객체의 상태 변화에 따라 전달되는 메서드 등을 기반으로 추가 변화 사항이 생기는 객체)들에게 변화를 알려주는 디자인 패턴이다.

객체와 주체가 분리될 수도 있고, 합쳐질 수도 있다. 합쳐진 옵저버 패턴에서는 주체와 객체를 따로 두지 않고 상태가 변경되는 객체를 기반으로 구축하기도 한다.

사례

  • 트위터: 팔로우한 주체가 트윗을 올리면 알림이 팔로워에게 간다.
  • MVC 패턴: 주체인 모델에서 변경 사항이 생겨 update() 메서드로 옵저버인 뷰에 알려주고 이를 기반으로 컨트롤러가 작동한다.

자바 코드

interface Subject {
    public void register(Observer obj);
    public void unregister(Observer obj);
    public void notifyObservers();
    public Object getUpdate(Observer obj);
}

interface Observer {
    public void update(); 
}

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();
    }
}

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 TopicSubscriber("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!!
*/ 

topic(주체이자 객체)을 기반으로 구현한 옵저버 패턴이다.

5. 프록시 패턴과 프록시 서버

프록시 패턴대상 객체에 접근하기 전, 그 접근에 대한 흐름을 가로채 해당 접근을 필터링하거나 수정하는 등의 역할을 하는 계층이 있는 디자인 패턴이다.

객체의 속성, 변환 등을 보완하여 보안, 데이터 검증, 캐싱, 로깅에 사용한다.

프록시 객체

어떠한 대상의 기본적인 동작의 작업을 가로챌 수 있는 객체이다.

프록시 서버

프록시 서버는 서버와 클라이언트 사이에서 클라이언트가 자신을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해주는 컴퓨터 시스템이나 응용 프로그램이다.

  • 프록시 서버에서의 캐싱: 캐시 안에 정보를 담아두고, 캐시 안에 있는 정보를 요구하는 요청에 대해 다시 저 멀리 있는 원격 서버에 요청하지 않고 캐시 안에 있는 데이터를 활용하는 것이다. 트래픽을 줄일 수 있다.

nginx

비동기 이벤트 기반의 구조와 다수의 연결을 효과적으로 처리 가능한 웹서버이다.

  • nginx를 프록시 서버로 둬서 실제 포트를 숨길 수 있고 메인 서버 앞단에서의 로깅을 할 수도 있다.
  • 주로 Node.js 서버 앞단의 프록시 서버로 활용된다. -> 익명 사용자가 직접적으로 서버에 접근하는 것을 차단하고, 간접적으로 한 단계를 더 거치게 만들어서 보안을 강화할 수 있다.

CloudFlare

전세계적으로 분산된 서버가 있고 이를 통해 어떠한 시스템의 콘텐츠 전달을 빠르게 할 수 있는 CDN 서비스이다.
(CDN - Content Delivery Network: 각 사용자가 인터넷에 접속하는 곳과 가까운 곳에서 콘텐츠를 캐싱 또는 배포하는 서버 네트워크)

웹 서버 앞단에 프록시 서버로 두어 DDOS 공격 방어나 HTTPS 구축에 사용된다.

  • DDOS 공격 방어: 의심스러운 트래픽(특히 사용자가 접속하는 것이 아닌 시스템을 통해 오는 트래픽)을 자동으로 차단해서, 짧은 기간 동안 네트워크에 많은 요청을 보내 네트워크를 마비시키는 DDOS로부터 보호한다.
  • HTTPS 구축: 인증서를 기반으로 HTTPS를 구축할 수도 있지만, CloudFlare를 사용하여 별도의 인증서 설치 없이 더 손쉽게 구축할 수 있다.

CORS와 프론트엔드의 프록시 서버

CORS 에러를 해결하기 위해 프론트엔드에서 프록시 서버를 만들기도 한다.

프론트엔드에서 127.0.0.1:3000으로 테스팅을 하는데, 백엔드 서버는 127.0.0.1:8080이라면 포트 번호가 다르기 때문에 CORS 에러가 발생한다.
이때 프론트엔드 서버 앞단에 프록시 서버를 둬서 오리진을 127.0.0.1:8080으로 바꾸는 것이다.

6. 이터레이터 패턴

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

이를 통해 자료형의 구조와는 상관 없이 이터레이터라는 하나의 인터페이스로 순회가 가능하다.

7. 노출모듈 패턴

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

자바스크립트는 private나 public 같은 접근 제어자가 존재하지 않고 전역 범위에서 스크립트가 실행되기 때문에 노출모듈 패턴을 통해 private와 public 접근 제어자를 구현하기도 한다.

8. MVC 패턴

MVC 패턴모델, 뷰, 컨트롤러로 이루어진 디자인 패턴이다.

애플리케이션의 구성 요소를 세 가지 역할로 구분하여 개발 프로세스에서 각각의 구성 요소에만 집중해서 개발할 수 있다.

  • 장점: 재사용성과 확장성이 용이하다.
  • 단점: 애플리케이션이 복잡해질수록 모델과 뷰의 관계가 복잡해진다.

모델

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

사용자 인터페이스 요소를 나타낸다. 즉, 사용자가 볼 수 있는 화면이다.
모델이 가지고 있는 정보를 따로 저장하지 않아야 하며 단순히 화면에 표시하는 정보만 가지고 있어야 한다.
또한, 변경이 일어나면 컨트롤러에 이를 전달해야 한다.

컨트롤러

모델과 뷰를 잇는 다리 역할을 하며, 이벤트 등 메인 로직을 담당한다.
또한, 모델과 뷰의 생명주기도 관리하며, 모델이나 뷰의 변경 통지를 받으면 이를 해석하여 각각의 구성 요소에 해당 내용에 대해 알려준다.

사례

스프링: Spring의 Web MVC는 웹서비스를 구축하는 데 편리한 기능을 제공한다. @RequestParam, @RequestHeader, @PathVariable 등의 애너테이션을 기반으로 사용자의 요청값을 쉽게 분석할 수 있고, 그것이 유효한지 알 수 있다.

9. MVP 패턴

MVP 패턴은 MVC 패턴으로부터 파생되었으며 P는 프레젠터이다.

모델이 뷰를 직접 컨트롤할 수 없다.

뷰와 프레젠터는 일대일 관계이기 때문에 MVC 패턴보다 더 강한 결합을 가진다.

10. MVVM 패턴

MVVM 패턴은 MVC의 C가 뷰모델로 바뀐 패턴이다.

뷰모델은 뷰를 더 추상화한 계층이다.

MVVM 패턴은 MVC 패턴과 다르게 커맨드(여러 요소에 대한 처리를 하나의 액션으로 처리할 수 있게 하는 기법)와 데이터 바인딩(화면에 보이는 데이터와 웹 브라우저의 메모리 데이터를 일치시키는 기법)을 가진다.
뷰와 뷰모델 사이의 양방향 데이터 바인딩을 지원하며 UI를 별도의 코드 수정 없이 재사용할 수 있고 단위 테스팅하기 쉽다.

사례

Vue.js: 반응형이 특징인 프론트엔드 프레임워크로, 함수를 사용하지 않고 값 대입만으로 변수가 변경되며 양방향 바인딩, html을 토대로 컴포넌트를 구축할 수 있다.

11. 어댑터 패턴

어댑터 패턴어댑터를 사용하여 호환되지 않는 인터페이스를 호환되도록 하는 패턴이다.
향후 인터페이스가 변경되더라도, 변경된 내용이 어댑터 안에 캡슐화되므로 수정할 필요가 없다.

12. 템플릿 메서드 패턴

상속을 통해 부모 클래스의 기능을 확장할 때 사용하는 패턴이다.
부모 클래스에서 변하지 않는 기능을 구현해두고 자식 클래스에서 확장할 기능을 구현한다.

0개의 댓글