디자인 패턴 (2) - 전략, 옵저버, 프록시

2ㅣ2ㅣ·2024년 10월 9일

CS

목록 보기
5/13

3. 전략 패턴

  • 정책 패턴(policy pattern)이라고도 함
  • 객체의 행위를 바꾸고 싶은 경우 직접 수정하지 않고, 전략이라 부르는 ‘캡슐화한 알고리즘’컨텍스트 안에서 바꿔주면서 상호 교체가 가능하게 함

우리가 어떤 것을 살 때 네이버페이, 카카오페이 등 다양한 방법으로 결제하듯이, 어떤 아이템을 살 때 LUNACard로 사는 것과 KAKAOCard로 사는 것을 구현한 예제이다. 결제 방식의 ‘전략’만 바꿔서 두 가지 방식으로 결제하는 것을 구현

  • 코드 구현(Java)
    import java.text.DecimalFormat;
    import java.util.ArrayList;
    import java.util.List;
    // PaymentStrategy는 전략 인터페이스로, 다양한 결제 방법을 제공하기 위한 공통 계약을 정의
    // 각 결제 방식은 이 인터페이스를 구현하면서 서로 다른 결제 로직을 제공함
    interface PaymentStrategy { 
        public void pay(int amount);
    } 
    
    class KAKAOCardStrategy implements PaymentStrategy {
    	// KAKAOCard를 사용하는 결제 전략
        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 {
    	// LUNACard를 사용하는 결제 전략
        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 { 
        private String name;
        private int price; 
        public Item(String name, int cost){
            this.name=name;
            this.price=cost;
        }
    
        public String getName() {
            return name;
        }
    
        public int getPrice() {
            return price;
        }
    } 
    
    class ShoppingCart { 
        List<Item> items;
        
        public ShoppingCart(){
            this.items=new ArrayList<Item>();
        }
        
        public void addItem(Item item){
            this.items.add(item);
        }
        
        public void removeItem(Item item){
            this.items.remove(item);
        }
        
        public int calculateTotal(){
            int sum = 0;
            for(Item item : items){
                sum += item.getPrice();
            }
            return sum;
        }
        
        public void pay(PaymentStrategy paymentMethod){
            int amount = calculateTotal();
            paymentMethod.pay(amount);
        }
    }  
    
    public class HelloWorld{
        public static void main(String []args){
            ShoppingCart cart = new ShoppingCart();
            
            Item A = new Item("kundolA",100);
            Item B = new Item("kundolB",300);
            
            cart.addItem(A);
            cart.addItem(B);
            
            // pay by LUNACard
            cart.pay(new LUNACardStrategy("kundol@example.com", "pukubababo"));
            // pay by KAKAOBank
            cart.pay(new KAKAOCardStrategy("Ju hongchul", "123456789", "123", "12/01"));
        }
    }
    /*
    400 paid using LUNACard.
    400 paid using KAKAOCard.
    */
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.");
    }
} 
  • KAKAOCardStrategyLUNACardStrategy는 각각 결제 방식에 대한 구체적인 전략(알고리즘)을 제공
  • 둘 다 PaymentStrategy 인터페이스를 구현하면서 자신만의 결제 로직을 가지고 있음.
class ShoppingCart { 
    List<Item> items;
    
    public ShoppingCart(){
        this.items=new ArrayList<Item>();
    }
    
    public void addItem(Item item){
        this.items.add(item);
    }
    
    public void removeItem(Item item){
        this.items.remove(item);
    }
    
    public int calculateTotal(){
        int sum = 0;
        for(Item item : items){
            sum += item.getPrice();
        }
        return sum;
    }
    
    public void pay(PaymentStrategy paymentMethod){
        int amount = calculateTotal();
        paymentMethod.pay(amount);
    }
}  
  • ShoppingCart전략을 사용하는 컨텍스트 클래스
  • 여기서 결제 로직을 직접 정의하는 게 아니라, pay 메서드 를 통해 외부에서 주입된 전략을 사용해 결제를 처리
    • 즉, 쇼핑카트는 결제 방식에 대해 전혀 알 필요 없이 전략 객체만 호출하여 결제를 처리

전략 패턴의 장점

  1. 유연성
  • 런타임에 전략을 선택하거나 변경할 수 있기 때문에 다양한 요구 사항에 유연하게 대응할 수 있음
  1. 클래스 분리:
  • 행위(알고리즘)를 전략 객체로 분리하므로 클래스의 책임이 명확해지고, 유지보수와 확장이 쉬워짐
  1. 중복 코드 감소:
  • 여러 유사한 알고리즘을 전략으로 분리하면, 클라이언트 코드 내의 중복된 분기 처리가 줄어들어 코드가 간결해짐

전략 패턴의 단점

  1. 클래스 수 증가:
  • 각 알고리즘(전략)을 별도로 클래스화해야 하기 때문에 클래스 수가 많아질 수 있음
  1. 복잡성 증가:
  • 소규모 프로젝트에서는 전략 패턴이 오히려 복잡성을 증가시킬 수 있음
  • 단순한 조건문으로 해결할 수 있는 문제에 전략 패턴을 사용하는 것은 과할 수 있음

4. 옵저버 패턴

주체가 어떤 객체(subject)상태 변화를 관찰하다가 상태 변화가 있을 때마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴

주요 개념

  1. Subject(주체):
    • 상태 변화를 감지하는 객체
    • 주체는 여러 옵저버들을 등록하고, 상태 변화가 발생했을 때 등록된 옵저버들에게 알림을 보내는 역할을 함.
  2. Observer(옵저버):
    • 주체의 상태 변화를 감지하고, 그에 따라 특정 동작을 수행하는 객체
    • 여러 옵저버들이 주체에 등록될 수 있고, 주체는 옵저버들에게 변경 사항을 알리기만 하면 됨
  3. Notify:
    • 주체는 상태가 변경될 때 옵저버들에게 알림을 보내는 동작을 함
    • 이때 옵저버들은 주체가 제공하는 메서드나 데이터를 사용해 상태 변화를 반영

구현 코드(Java)

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

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 HelloWorld { 
    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!!
*/ 

주체(Subject)

  • Topic 클래스는 주체로서, 상태(message)가 변경되면 notifyObservers() 메서드를 통해 등록된 옵저버들에게 변경을 알린다.
  • register()unregister() 메서드는 옵저버를 주체에 추가하거나 제거하는 메서드
  • 주체는 옵저버들이 어떤 동작을 하는지 전혀 알 필요가 없고, 그저 옵저버를 등록하고 알리기만 하면 됨

옵저버(Observer)

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); 
    } 
}
  • TopicSubscriber 클래스는 옵저버로서, 주체인 Topic의 상태가 변경될 때 update() 메서드를 통해 상태 변화를 알림 받는다.
  • 옵저버는 topic.getUpdate(this)로 주체의 상태를 확인한 후, 그 정보를 사용해 필요한 동작을 수행

상태 알림

public class HelloWorld { 
    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!!");  // 상태 변화 및 알림
    }
}
  • 주체(Topic)가 상태를 업데이트(postMessage())하면 등록된 모든 옵저버(a, b, c)에게 알림이 가고, 각 옵저버는 상태 변화를 반영해 메시지를 출력

옵저버 패턴의 장점

  1. 느슨한 결합:
    • 주체와 옵저버 간의 결합도가 낮음
      • 주체는 옵저버에 대해 구체적으로 알 필요 없이, 옵저버 목록만 관리하고 상태 변화 시 알림을 주기만 하면 됨
      • 반대로, 옵저버는 주체의 상태 변화만 받아서 처리
  2. 유연성:
    • 주체에 새로운 옵저버를 쉽게 추가하거나 제거할 수 있음
    • 옵저버의 동작을 수정하더라도 주체에는 영향을 미치지 않음
  3. 확장성:
    • 새로운 유형의 옵저버를 추가하거나, 주체가 더 많은 상태 변화를 제공할 때도 주체와 옵저버 간의 구조를 수정하지 않고 기능을 확장할 수 있음

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

프록시 패턴

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

  • 이를 통해 객체의 속성, 변환 등을 보완하며, 보안, 데이터 검증, 캐싱, 로깅에 사용됨
  • 이는 앞서 설명한 프록시 객체로 쓰이기도 하지만, 프록시 서버로도 활용됨
💡

프록시 서버에서의 캐싱

  • 캐시 안에 정보를 담아두고, 캐시 안에 있는 정보를 요구하는 요청에 대해 다시 저 멀리 있는 원격 서버에 요청하지 않고 캐시 안에 있는 데이터를 활용하는 것을 말함
  • 이를 통해 불필요하게 외부와 연결하지 않기 때문에, 트래픽을 줄일 수 있다는 장점이 있음

프록시 서버

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

프록시 서버로 쓰는 Nginx

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

주로 Node.js 서버 앞단의 프록시 서버로 활용됨

Node.js 창시자 라이언 달(Ryan Dahl)은 다음과 같이 말했다.

“Node.js의 버퍼 오버플로우 취약점을 예방하기 위해서는 nginx를 프록시 서버로 앞단에 놓고, Node.js를 뒤쪽에 놓는 것이 좋다.”

image.png

  • 익명 사용자가 직접적으로 서버에 접근하는 것을 차단
  • 간접적으로 한 단계를 더 거치게 만드므로 보안을 강화할 수 있음
  • nginx를 프록시 서버로 둬서 실제 포트를 숨길 수 있고, 정적 자원을 gzip 압축하거나, 메인 서버 앞단에서의 로깅을 할 수 있음
💡

버퍼 오버플로우란?

  • 버퍼보통 데이터가 저장되는 메모리 공간으로, 메모리 공간을 벗어나는 경우를 말함.
  • 이 때 사용되지 않아야 할 영역에 데이터가 덮어씌워져 주소, 값을 바꾸는 공격이 발생하기도 함

gzip 압축

  • LZ77과 Huffman 코딩의 조합인 DEFLATE 알고리즘을 기반으로 한 압축 기술
  • gzip 압축을 하면 데이터 전송량을 줄일 수 있지만, 압축을 해제했을 때 서버에서의 CPU 오버헤드도 생각해서 gzip 압축 사용 유무를 결정해야 함

프록시 서버로 쓰는 CloudFlare

CloudFlare는 전 세계적으로 분산된 서버가 있고, 이를 통해 어떠한 시스템의 콘텐츠 전달을 빠르게 할 수 있는 CDN 서비스

💡

CDN(Content Delivery Network)

  • 각 사용자가 인터넷에 접속하는 곳과 가까운 곳에서 콘텐츠를 캐싱 또는 배포하는 서버 네트워크를 말함

  • 이를 통해 사용자가 웹 서버로부터 콘텐츠를 다운로드하는 시간을 줄일 수 있다

  • CloudFlare는 웹 서버 앞단에 프록시 서버로 두어 DDOS 공격 방어HTTPS 구축에 쓰임

  • 또한, 서비스를 배포한 이후에 해외에서 무언가 의심스러운 트래픽이 발생하면 이 때문에 많은 클라우드 서비스 비용이 발생할 수도 있는데, 이 때 CloudFlare가 의심스러운 트래픽인지를 먼저 판단해 CAPTCHA 등을 기반으로 이를 일정 부분 막아주는 역할도 함

  • 위 그림처럼 사용자, 크롤러, 공격자가 자신의 웹 사이트에 접속하게 될 텐데, 이 때 CloudFlare를 통해 공격자로부터 보호할 수 있음

    DDOS 공격 방어

    • DDOS는 짧은 기간동안 네트워크에 많은 요청을 보내 네트워크를 마비시켜 웹 사이트의 가용성을 방해하는 사이버 공격 유형
    • CloudFlare는 의심스러운 트래픽, 특히 사용자가 접속하는 것이 아닌 시스템을 통해 오는 트래픽을 자동으로 차단하여 DDOS 공격으로부터 보호함
    • CloudFlare의 거대한 네트워크 용량과 캐싱 전략으로 소규모 DDOS 공격은 쉽게 막아낼 수 있으며, 이러한 공격에 대한 방화벽 대시보드도 제공

    HTTPS 구축

    • 서버에서 HTTPS를 구축할 때 인증서를 기반으로 구축할 수도 있음
      • 하지만 CloudFlare를 사용하면 별도의 인증서 설치 없이 HTTPS를 구축할 수 있음

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

    프록시 서버 이용 전

    프록시 서버 이용 전

    CORS?

    • 서버가 웹 브라우저에서 리소스를 로드할 때 다른 오리진을 통해 로드하지 못하게 하는 HTTP 헤더 기반 메커니즘
    • FE 개발시 FE 서버를 만들어서 BE 서버와 통신할 때 주로 CORS를 마주치는데, 이를 해결하기 위해 FE에서 프록시 서버를 만들기도 함.
    💡

    오리진?

    프록시 서버 이용 후

    프록시 서버 이용 후

    위 그림처럼, 프론트 서버 앞단에 프록시 서버를 놓아 /api 요청은 users API, /api2 요청은 users API2 에 요청할 수 있음.

    • 예를 들어, FE에서는 127.0.0.1:3000 으로 테스팅 하는데, BE 서버가 127.0.0.1:12010 라면 포트 번호가 다르기 때문에 CORS 에러가 발생
    • 이 때 프록시 서버를 두어 FE 서버에서 요청되는 오리진을 127.0.0.1:12010 으로 바꿈
    💡

    127.0.0.1이란?

    • 127.0.0.1이란 루프백(loopback) IP로, 본인 PC 서버의 IP를 의미
    • localhost와 동일
    profile
    https://sususoo.tistory.com/

    0개의 댓글