[면접을 위한 CS 전공지식 노트] 1.1 디자인 패턴

minzip·2025년 1월 2일
0

CS

목록 보기
1/1

📚 면접을 위한 CS 전공지식 노트 책을 바탕으로 CS 학습과 면접준비를 진행합니다.

1.1 디자인 패턴

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

크게 생성패턴, 구조패턴, 행동패턴 3가지로 나뉜다.

  • 생성패턴: 객체 생성 방법이 들어간 디자인 패턴
    • 싱글톤, 팩토리, 추상팩토리, 빌더 프로토타입 패턴
  • 구조패턴: 객체, 클래스 등으로 큰 구조를 만들 때 유연하고 효율적으로 만드는 방법이 들어간 디자인 패턴
    • 프록시, 어댑터, 브리지, 복합체, 데코레이터, 퍼사드, 플라이웨이트 패턴
  • 행동패턴: 객체나 클래스 간의 알고리즘, 책임 할당에 관한 디자인 패턴
    • 이터레이터, 옵저버, 전략, 책임연쇄, 커맨드, 중재자, 메멘토, 상태, 템플릿메서드, 비지터 패턴

1.1.1 싱글톤 패턴

싱글톤 패턴(singleton pattern)은 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴

  • 장점
    • 하나의 인스턴스를 만들어 놓고 해당 인스턴스를 다른 모듈들이 공유하며 사용하기 때문에, 인스턴스를 생성할 때 드는 비용이 줄어든다.
  • 단점
    • 모듈간 의존성이 높아짐.
    • 특히 TDD의 단위테스트에서 테스트가 서로 독립적이어야 하며 테스트를 어떤 순서로든 실행할 수 있어야한다. but 싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 구현하는 패턴이므로 각 테스트마다 독립적인 인스턴스를 만들기 어렵다.
  • 사용 예시
    • Node.js에서 MongoDB 데이터베이스를 연결할 때 쓰는 mongoose 모듈에서 볼 수 있다.
class Singleton {
    private static class singleInstanceHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return singleInstanceHolder.INSTANCE;
    }
}

public class Main {
        public static void main(String[] args) {

            Singleton a = Singleton.getInstance();
            Singleton b = Singleton.getInstance();

            System.out.println(a.hashCode());
            System.out.println(b.hashCode());

            if (a == b) {
                System.out.println(true);
            }
        }
}

/*
1523554304
1523554304
true
*/

의존성 주입 (DI, Dependency Injection)

싱글톤 패턴의 단점인 모듈 간 강한 결합을 의존성 주입으로 느슨하게 만들어 해결할 수 있다.

  • 의존성? A가 B에 의존한다 = B의 변경 사항에 대해 A 또한 변해야 한다.

의존성 주입이란 메인 모듈이 ‘직접’ 다른 하위 모듈에 대한 의존성을 주기보다는 중간에 의존성 주입자가 이 부분을 가로채 메인 모듈이 ‘간접’적으로 의존성을 주입하는 방식이다.

의존성 주입을 할 때는 의존관계역전원칙이 적용된다.

  • 의존관계역전원칙(DIP, Dependency Inversion Principle)
    • 상위 모듈은 하위 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다.
    • 추상화는 세부사항에 의존해서는 안 된다. 세부사항은 추상화에 따라 달라져야 한다.
  • 장점
    • 모듈들을 쉽게 교체할 수 있는 구조가 된다.
    • 단위 테스팅과 마이그레이션이 쉬워진다.
    • 애플리케이션 의존성 방향이 좀 더 일관되어 코드를 추론하기가 쉬워진다.
  • 단점
    • 결국에는 모듈이 더 생기게 되므로 복잡성이 증가한다.
    • 종속성 주입 자체가 컴파일이 아닌 런타임 때 일어나기 때문에, 컴파일 할 때 종속성 주입에 관한 에러를 잡기가 어려워질 수 있다.

1.1.2 팩토리 패턴

팩토리 패턴(factory pattern)은 객체를 사용하는 코드에서 객체 생성 부분을 떼어내 추상화한 패턴이자 상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고, 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정하는 패턴

  • 장점: 느슨한 결합
    • 상위 클래스에서는 객체 생성 방식에 대해 알 필요가 없어져 유연성을 갖게 된다.
    • 객체 생성 로직이 하위 클래스에서만 관리되기 때문에 유지보수성이 증가된다.
// 상위 클래스가 중요한 뼈대를 결정
class CoffeeFactory { 
  static createCoffee(type) {  // 정적 메서드로 정의해 객체를 만들지 않고도 호출 가능하도록
    const factory = factoryList[type];
    return factory.createCoffee();
  }
}

class Latte {
  constructor() {
    this.name = "latte";
  }
}

class Espresso {
  constructor() {
    this.name = "espresso";
  }
}

// 하위 클래스가 구체적인 내용을 결정
class LatteFactory extends CoffeeFactory {
  static createCoffee() {
    return new Latte();
  }
}
class EspressoFactory extends CoffeeFactory {
  static createCoffee() {
    return new Espresso();
  }
}

const factoryList = { LatteFactory, EspressoFactory };

const main = () => {
  const coffee = CoffeeFactory.createCoffee("LatteFactory");
  console.log(coffee.name);
};

main();

위 예시 역시 의존성 주입으로 볼 수 있다. CoffeFactory에서 LatteFactory의 인스턴스를 생성하는 것이 아닌 LatteFactory에서 생성한 인스턴스를 CoffeFactory에 주입한다.

1.1.3 전략 패턴

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

어떤 일을 수행하는 알고리즘이 여러가지인 경우, 동작들을 미리 정의함을써 쉽게 전략을 교체할 수 있다. 즉 알고리즘 변형이 빈번하게 필요한 경우에 적합하다.

  • 전략 패턴 구조
    • 전략 알고리즘 객체들: 각기 다른 알고리즘(전략)을 구현한 구체적인 클래스들
    • 전략 인터페이스: 모든 전략 구현체에 대한 공용 인터페이스
    • 컨텍스트: 전략 객체를 사용하여 작업을 수행하며, 전략을 동적으로 교체할 수 있는 클래스
    • 클라이언트: 컨텍스트에 적절한 전략 객체를 주입하고 실행을 요청하는 코드
// 전략 인터페이스 정의
class PaymentStrategy {
  processPayment(amount) {
    throw new Error("processPayment method must be implemented");
  }
}

// 전략 알고리즘 객체들
class CreditCardPayment extends PaymentStrategy {
  processPayment(amount) {
    console.log(`Paid $${amount} using Credit Card.`);
  }
}
class PayPalPayment extends PaymentStrategy {
  processPayment(amount) {
    console.log(`Paid $${amount} using PayPal.`);
  }
}
class BitcoinPayment extends PaymentStrategy {
  processPayment(amount) {
    console.log(`Paid $${amount} using Bitcoin.`);
  }
}

// 컨텍스트 클래스
class PaymentProcessor {
  constructor(paymentStrategy) {
    if (!(paymentStrategy instanceof PaymentStrategy)) {
      throw new Error("paymentStrategy must implement PaymentStrategy interface");
    }
    this.paymentStrategy = paymentStrategy; // 초기 전략 설정
  }

  setStrategy(paymentStrategy) {
    if (!(paymentStrategy instanceof PaymentStrategy)) {
      throw new Error("paymentStrategy must implement PaymentStrategy interface");
    }
    this.paymentStrategy = paymentStrategy; // 전략 변경 가능
  }

  process(amount) {
    this.paymentStrategy.processPayment(amount); // 현재 전략 실행
  }
}

// 클라이언트
const main = () => {
  try {
    // 결제 전략 생성
    const creditCardPayment = new CreditCardPayment();
    const paypalPayment = new PayPalPayment();
    const bitcoinPayment = new BitcoinPayment();

    // 컨텍스트 생성 및 초기 전략 설정
    const paymentProcessor = new PaymentProcessor(creditCardPayment);
    paymentProcessor.process(100); // Paid $100 using Credit Card.

    // 전략 변경: PayPal
    paymentProcessor.setStrategy(paypalPayment);
    paymentProcessor.process(200); // Paid $200 using PayPal.

    // 전략 변경: Bitcoin
    paymentProcessor.setStrategy(bitcoinPayment);
    paymentProcessor.process(300); // Paid $300 using Bitcoin.
  } catch (error) {
    console.error(error.message);
  }
};

main();

1.1.4 옵저버 패턴

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

  • 관찰 대상(Subject)와 여러개의 관찰자(Observers)로 일대다 의존성을 가지며, 주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다.
  • Subject에서 관찰자들을 리스트로 모아 합성하여 관리한다.
  • MVC나 PubSub에서도 활용!

아래는 JS의 프록시 객체를 활용해 구현한 옵저버 패턴의 예시이다.

프록시 객체의 기본 구조

JavaScript의 프록시 객체(Proxy)는 객체의 기본 동작(예: 속성 접근, 값 설정, 함수 호출 등)을 가로채고, 이를 수정하거나 추가 동작을 정의할 수 있게 해주는 기능이다.

const target = {}; // 프록시할 대상
const handler = { // target 동작을 가로채고 어떠한 동작을 할 것인지가 설정되어 있는 함수
  get(target, property) {
    // 속성 접근을 가로챔
    return `Property '${property}' was accessed`;
  },
};

const proxy = new Proxy(target, handler);

console.log(proxy.someProperty); // Property 'someProperty' was accessed

특정 속성에 접근할 때 그 부분을 가로채서 등록한 handler의 로직을 강제하고 있다.

📌 핸들러 트랩 메서드
프록시 핸들러는 다양한 작업을 가로채는 트랩(Trap) 메서드를 제공한다.

1. get(target, property, receiver)
    - 속성 접근을 가로챈다.
    - 매개변수:
        - `target`: 원래 객체.
        - `property`: 접근하려는 속성 이름.
        - `receiver`: 프로퍼티 접근을 받은 프록시 객체.
2. set(target, property, value, receiver)
    - 속성 값을 설정할 때 동작을 가로챈다.
3. has(target, property)
    - `in` 연산자를 가로챈다.
4. deleteProperty(target, property)
    - `delete` 연산자를 가로챈다.
5. apply(target, thisArg, args)
    - 함수 호출을 가로챈다.
6. construct(target, args)
    - 생성자 호출(`new`)을 가로챈다.
function createReactiveObject(target, callback) { 
    const proxy = new Proxy(target, {
        set(obj, prop, value){
            if(value !== obj[prop]){
                const prev = obj[prop]
                obj[prop] = value 
                callback(`${prop}가 [${prev}] >> [${value}] 로 변경되었습니다`)
            }
            return true
        }
    })
    return proxy 
} 
const a = {
    "형규" : "솔로"
} 
const b = createReactiveObject(a, console.log)
b.형규 = "솔로"  // set은 속성값을 설정할 때 가로챈다.
b.형규 = "커플"
// 형규가 [솔로] >> [커플] 로 변경되었습니다

b.형규 = "커플"; 이 실행되며 기존 값(obj[prop] = "솔로")과 새 값(value = "커플")이 다르므로 obj[prop]"커플"로 업데이트하게 된다.

이처럼 프록시 객체를 통해 특정 변화를 감시하는 옵저버 패턴을 구현해 볼 수 있다.

1.1.5 프록시 패턴과 프록시 서버

프록시 패턴(proxy pattern)은 대상 객체(subject)에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 디자인 패턴

객체의 속성, 변환 등을 보완하며 보안, 데이터 검증, 캐싱, 로깅에 사용된다. 앞서 살펴본 프록시 객체도 마찬가지이다.

프록시 서버

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

  • nginx: 비동기 이벤트 기반의 구조와 다수의 연결을 효과적으로 처리 가능한 웹서버. 주로 Node.js 서버 앞단의 프록시 서버로 활용된다.
    • 실제 포트를 숨길 수 있고 정적자원을 gzip 압축하거나, 메인 서버 앞단에서의 로깅을 할 수도 있다
  • CloudFlare: 전 세계적으로 분산된 서버가 있고 이를 통해 어떠한 시스템의 콘텐츠 전달을 빠르게 할 수 있는 CDN 서비스
📌 CDN(Content Delivery Network)
각 사용자가 인터넷에 접속하는 곳과 가까운 곳에서 콘텐츠를 캐싱 또는 배포하는 서버 네트워크를 말한다.
  • DDOS 공격 방어HTTPS 구축에 쓰인다. 또한 의심스러운 트래픽인지를 먼저 판단해 캡챠 등을 기반으로 이를 일정 부분 막아주는 역할도 수행해준다.


리버스 프록시로 클라이언트가 웹 사이트의 원본 서버에 요청을 보낼 때 리버스 프록시 서버가 네트워크 에지에서 해당 요청을 가로챈다. 이를 통해 어떤 클라이언트도 원본 서버와 직접 통신하지 못 하도록 할 수 있다.

구분정방향 프록시 서버리버스 프록시 서버
위치클라이언트와 외부 서버 사이클라이언트와 내부 서버 사이
사용자클라이언트서버
요청 대상외부 서버내부 서버
익명성 제공 대상클라이언트의 IP를 숨김서버의 IP를 숨김
주요 목적클라이언트 보호, 네트워크 우회, 캐싱서버 보호, 로드 밸런싱, SSL 종료, 캐싱 및 압축
  • CORS와 프론트엔드의 프록시 서버
📌 CORS(Cross-Origin Resource Sharing)
서버가 웹 브라우저에서 리소스를 로드할 때 다른 오리진을 통해 로드하지 못하게 하는 HTTP 헤더 기반 메커니즘
  • 서버에서 Access-Control-Allow-Origin을 어떻게 설정하느냐에 따라서 서버에 접근 가능한 오리진이 제한된다.
  • 즉 CORS 에러가 난다면 서버에서 허용 도메인을 추가해준다던가, 프론트엔드에서 Proxy Server를 도입해 API 서버와 오리진을 동일하게 맞추는 방식이 있다.

  • 리액트의 경우에는 http-proxy-middleware 모듈을 활용할 수 있다.
// src/setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function(app) {
  app.use(
    '/api', // API 요청을 /api로 시작하는 경로로 프록시합니다.
    createProxyMiddleware({
      target: 'http://localhost:5000', // 백엔드 서버 주소
      changeOrigin: true, // 도메인 변경 여부 (서버가 다른 도메인일 때 필요)
      secure: false, // HTTPS 사용 시 필요
    })
  );
};

1.1.6 이터레이터 패턴

이터레이터 패턴(iterator pattern)은 이터레이터(iterator)를 사용하여 컬렉션(collection)의 요소들에 접근하는 디자인 패턴. 이를 통해 순회할 수 잇는 여러가지 자료형의 구조와는 상관없이 이터레이터라는 하나의 인터페이스로 순회가 가능.

과거 미션에서 Symbol.iterator로 직접 만든 클래스를 순회 가능한 구조(이터러블한 객체)로 선언한 경험.

const mp = new Map() 
mp.set('a', 1)
mp.set('b', 2)
mp.set('cccc', 3) 
const st = new Set() 
st.add(1)
st.add(2)
st.add(3) 
const a = []
for(let i = 0; i < 10; i++)a.push(i)

for(let aa of a) console.log(aa)
for(let a of mp) console.log(a)
for(let a of st) console.log(a) 
/* 
a, b, c 
[ 'a', 1 ]
[ 'b', 2 ]
[ 'c', 3 ]
1
2
3
*/

1.1.7 노출모듈 패턴

노출모듈 패턴(revealing module pattern)은 즉시 실행 함수를 통해 private, public 같은 접근 제어자를 만드는 패턴

const pukuba = (() => {
    const a = 1
    const b = () => 2
    const public = {
        c : 2, 
        d : () => 3
    }
    return public 
})() 
console.log(pukuba)
console.log(pukuba.a)
// { c: 2, d: [Function: d] }
// undefined

JS에는 private이나 public같은 접근제어자가 존재하지 않고 전역 범위에서 스크립트가 실행되기 때문에 노출모듈 패턴을 사용한다.

CommonJS 방식 역시 기본적으로 모듈 내부에 변수를 숨기고, 외부에 필요한 메서드나 값을 module.exports명시적으로 노출하는 노출모듈 패턴이다.

1.1.8 MVC 패턴

MVC 패턴은 Model, View, Controller로 이루어진 디자인 패턴.

  • 구성
    • 모델(Model)애플리케이션의 데이터인 데이터베이스, 상수, 변수 등을 뜻한다.
    • 뷰(View)inputbox, checkbox, textarea 등 사용자 인터페이스 요소를 나타내며 모델을 기반으로 사용자가 볼 수 있는 화면을 뜻한다.
    • 컨트롤러(Controller)하나 이상의 모델과 하나 이상의 뷰를 잇는 다리 역할을 하며 이벤트 등 메인 로직을 담당합니다.모델과 뷰의 생명주기도 관리하며 모델이나 뷰의 변경 통지를 받으면 각각의 구성 요소에 해당 내용에 대해 알려준다.
  • 장점
    • 애플리케이션의 구성 요소를 세 가지 역할로 구분하여 개발 프로세스에서 각각의 구성 요소에만 집중해서 개발할 수 있으며, 재사용성과 확장성이 용이하다.
  • 단점
    • 애플리케이션이 복잡해질수록 모델과 뷰가 높은 의존성을 갖게된다.
  • 사용 예시
    • 리액트 (React.js)리액트는 유저 인터페이스를 구축하기 위한 라이브러리로, '가상 DOM'을 통해 실제 DOM을 조작하는 것을 추상화해서 성능을 높였다.

1.1.9 MVP 패턴

MVP 패턴은 MVC 패턴에서 Controller가 Presenter로 교체된 패턴

  • Presenter : View에서 요청한 정보로 Model을 가공하여 View에 전달해 주는 부분. View와 Model을 붙여주는 접착제 역할을 한다.

뷰와 프레젠터는 1:1관계이기 때문에 MVC 패턴보다 더 강한 결합을 지닌 디자인 패턴이다. -> MVC 패턴의 뷰와 모델의 사이의 의존성 문제는 해결되었지만 뷰와 프레젠터의 의존성이 생긴다.

1.1.10 MVVM 패턴

MVVM 패턴은 MVC 패턴에서 Controller가 View Model로 교체된 패턴

뷰모델은 뷰를 더 추상화한 계층이며, MVVM 패턴은 MVC 패턴과는 다르게 커맨드와 데이터 바인딩을 가지는 특징을 가지고 있다. 뷰와 뷰모델 사이의 양방향 데이터 바인딩을 지원하며 UI를 별도의 코드 수정 없이 재사용할 수 있고 단위 테스팅이 쉽다는 장점이 있다.

  • 사용 예시
    • 뷰 (View.js)함수를 사용하지 않고 값 대입만으로도 변수가 변경되며 양방향 바인딩, html을 토대로 컴포넌트를 구축할 수 있다는 것이 특징이다.

프론트엔드에서 MVC보다 더 많이 쓰이는 패턴은 ?

참고

https://inpa.tistory.com/entry/GOF-💠-전략Strategy-패턴-제대로-배워보자

profile
내일은 더 성장하기

0개의 댓글

관련 채용 정보