검색

검색어를 입력하세요

1.1 디자인 패턴

스터디를 함께 진행했음

라이브러리 : 애플리케이션에서 호출하여 사용하는 재사용 가능한 기능의 집합

프레임워크 : 애플리케이션의 전체 구조와 실행 흐름을 미리 정하고, 개발자가 작성한 코드를 호출하는 틀

라이브러리는 애플리케이션 코드가 필요한 기능을 호출하지만, 프레임워크는 전체 실행 흐름을 제어하며 애플리케이션 코드를 호출한다는 차이가 있다.

싱글톤 패턴 (singleton pattern)

클래스의 인스턴스가 하나만 생성되도록 보장하고, 그 인스턴스에 접근할 수 있는 방법을 제공하는 패턴

  • 장점 : 생성 비용이 큰 객체를 반복해서 만들지 않고 재사용하면 인스턴스 생성 비용을 줄일 수 있다.
  • 단점 : 같은 인스턴스를 사용하는 코드들의 결합도가 높아지고, 공유 상태로 인해 테스트가 서로 영향을 줄 수 있다.

이 단점은 자동화된 테스트에서 걸림돌이 된다. 독립된 테스트를 수행해야 하는데 같은 인스턴스와 상태를 공유하면 테스트가 서로 영향을 줄 수 있기 때문이다. **의존성 주입(Dependency Injection, DI)**을 사용하면 외부에서 의존 객체를 전달하여 테스트마다 다른 인스턴스로 교체하기 쉬워진다. 다만 의존성 주입 자체가 싱글톤의 공유 상태 문제를 직접 해결하는 것은 아니다.

싱글톤의 테스트 문제와 의존성 주입

객체가 필요한 의존성을 내부에서 직접 생성하지 않고 외부에서 전달받는 방식

의존성 주입

  • 장점 :
    • 모듈 교체가 가능한 구조를 만들 수 있다. 테스트나 마이그레이션이 쉬운 구조를 만들 수 있다.
    • 의존 객체를 외부에서 전달받기 때문에 모듈 간 관계가 명확해진다.
  • 단점 :
    • 복잡성이 높아진다.
    • 의존 관계와 객체 생성 설정이 많아지면 전체 구조를 파악하기 어려워질 수 있다.

의존성 역전 원칙(Dependency Inversion Principle, DIP) : 상위 모듈과 하위 모듈 모두 구체적인 구현이 아니라 추상화에 의존해야 한다. DI와 함께 적용할 수 있는 설계 원칙이지만, DI의 필수 조건은 아니다.

싱글톤 활용

DB 클라이언트나 커넥션 풀을 애플리케이션 범위의 단일 인스턴스로 관리하면 싱글톤과 유사한 생명주기를 갖는다. 이를 공유하면 연결을 매번 새로 만드는 비용을 줄일 수 있다.

  • MongoDB 데이터베이스를 연결할 때 쓰는 mongoose 연결이 내부적으로 사용하는 MongoDB 드라이버의 커넥션 풀
  • MySQL 데이터베이스의 커넥션 풀

팩토리 패턴 (factory pattern)

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

팩토리는 입력된 정보와 정해진 규칙에 따라 적절한 객체를 생성한다. 이를 통해 객체 생성에 필요한 세부 규칙을 실제 사용하는 코드에서 분리한다.

장점 :

객체를 사용하는 코드가 구체적인 생성 과정을 직접 알 필요가 없어, 객체 사용 코드와 생성 로직의 결합을 줄일 수 있다.

객체 생성 로직이 따로 떨어져 있기 때문에 리팩토링이 쉬워지고 유지보수 비용을 낮출 수 있다.

단점 :

구현 방식에 따라 여러 팩토리 클래스가 추가되어 클래스의 개수가 많아질 수 있다.

팩토리 패턴의 변형 :

  • Enum Factory Method 패턴 : enum 상수 안에 추상 메서드를 구현하는 패턴
    Shape rectangle = EnumShapeFactory.RECTANGLE.create("red");
  • Dynamic Factory 패턴 : 유형을 동적으로 등록해서 유형에 맞는 객체를 생성할 수 있는 팩토리 클래스를 사용하는 패턴

참고 :

활용

기본값을 설정하거나, 어떤 조건에 따라서 객체를 뚝딱 만들어야 하는 상황에서 활용할 수 있다. 이런 향기가 날 때.

if (type === "a") {
  // A 만들기
} else if (type === "b") {
  // B 만들기
} else if (type === "c") {
  // C 만들기
}
  • API 클라이언트 생성 : 환경에 따라 다른 baseURL을 설정한 인스턴스를 생성해야 할 때
  • 에러 종류에 따른 토스트 생성

전략 패턴 (strategy pattern)

객체의 동작을 바꾸고 싶을 때 전략이라고 부르는 캡슐화된 알고리즘을 객체의 컨텍스트 안에서 교체하는 방식.

내부에서 사용하는 알고리즘을 외부에서 주입받아 교체하는 모양을 하고 있다. 전략은 함수의 인자, 생성자, setter, 의존성 주입 등을 통해 전달할 수 있다.

전략 패턴

Node.js에서 인증 모듈을 구현할 때 사용하는 passport 라이브러리가 전략 패턴을 활용하고 있다. passport.use() 라는 메서드에 인증에 사용할 전략을 매개변수로 전달해서 인증 로직을 수행하는 방식.

장점 :

단점 : 구현 방식에 따라 전략 객체나 클래스가 많아져 관리가 어렵고 코드의 복잡성이 높아질 수 있다.

옵저버 패턴 (observer pattern)

주체가 자신의 상태를 관찰하는 옵저버 목록을 관리하고, 상태에 변경이 생기면 등록된 옵저버들에게 알리는 패턴

전통적인 옵저버 패턴에서는 주체가 곧 관찰 대상이다. 구현에 따라 상태를 가진 객체와 옵저버에게 변경을 알리는 객체를 분리할 수도 있다.

상태 객체와 알림 주체가 분리된 경우 :

상태 객체와 알림 주체가 분리된 경우 다이어그램

상태 객체가 알림 주체를 겸하는 경우 :

상태 객체가 알림 주체를 겸하는 경우 다이어그램

주로 이벤트 기반 시스템에서 쓰인다. MVC에서 모델의 변경을 뷰에 알리는 구조에도 활용할 수 있다.

자바에서 상속과 구현의 차이

  • 클래스가 일반 클래스나 추상 클래스를 상속할 때는 extends를 사용
  • 클래스가 인터페이스를 구현할 때는 implements를 사용
  • 인터페이스가 다른 인터페이스를 상속할 때는 extends를 사용

자바스크립트에서 프록시 객체를 활용한 옵저버 패턴

프록시 객체 : 어떤 대상의 기본적인 작업을 가로채서 재정의할 수 있는 객체

2개의 매개변수를 받는다

  • target : 프록시할 원본 객체
  • handler : 가로채는 작업과 가로채는 작업을 재정의할 방법을 정의하는 객체

handlerset 트랩을 이용하면 원본 객체의 속성이 변경되는 순간을 가로챌 수 있다. 이때 등록된 옵저버들을 호출하도록 구현하면 프록시 객체를 통해 상태 변경을 감지하고 알리는 구조를 만들 수 있다.

Vue.js 3의 반응성 시스템

Vue에서 refreactive로 정의한 값이 변경되면 해당 값에 의존하는 뷰가 업데이트된다. Vue의 반응성 시스템은 값에 접근할 때 의존성을 추적하고, 값이 변경되면 관련된 반응형 효과를 실행하는 방식으로 동작한다. 넓게 보면 상태 변화를 구독자에게 알리는 옵저버 패턴과 유사한 구조를 가진다.

프록시 패턴 (proxy pattern)

대상 객체를 대신하는 대리 객체를 두어 대상 객체에 대한 접근을 제어하는 패턴

대상 객체에 대한 접근 제어, 지연 로딩, 데이터 검증, 캐싱, 로깅 등에 활용한다. 네트워크에서는 프록시 서버를 활용 사례로 볼 수 있다.

프록시 서버

프록시 서버 : 서버와 클라이언트 사이에 위치하는 서버. 클라이언트가 자신을 통해 네트워크 서비스에 간접적으로 접속하게 한다.

프록시 서버의 캐싱 : 최초 요청 시에 프록시 서버에 정보를 담아두고, 이후 요청이 캐시한 정보를 요청하면 원격 서버에 다시 요청하지 않고 캐시한 정보를 이용해 응답하는 것을 말함. 불필요한 요청을 줄일 수 있기 때문에 트래픽이 줄어든다.

  1. nginx : 주로 Node.js 서버 앞단의 프록시 서버로 활용한다.
    • 실제 서버의 포트를 숨길 수 있다.
    • 정적 자원을 gzip 압축할 수 있다. => 데이터 전송량을 줄일 수 있다.
    • 방화벽이나 네트워크 설정을 함께 구성하면 사용자가 원본 서버에 직접 접근하는 것을 차단할 수 있다. => 보안성을 높인다.
  2. Cloudflare : CDN 서비스
    • DDoS 방어 : 악성으로 판단한 트래픽을 탐지하고 필터링하여 원본 서버를 보호
    • HTTPS 구축 : Cloudflare가 HTTPS 인증서를 제공하기 때문에 사용자에게 HTTPS 사이트를 제공할 수 있다. 다만 Flexible 모드에서는 Cloudflare와 원본 서버 사이가 암호화되지 않으므로 민감한 정보를 다루는 서비스에는 적합하지 않다. Full 또는 Full (Strict) 모드를 사용하면 해당 구간도 HTTPS로 통신한다.
      Flexible 모드: 사용자 브라우저 -- HTTPS --> Cloudflare -- HTTP --> 내 서버
  3. CORS 에러 해결을 위한 프런트엔드의 프록시 서버
    • 프런트엔드 개발을 할 때 백엔드와 다른 origin으로 요청하면, 백엔드 서버가 해당 origin을 허용하지 않은 경우 CORS 에러가 발생한다.
    • 백엔드 서버에서 Access-Control-Allow-Origin 에 오리진을 추가해주는 방법도 있지만, 추가가 불가능한 경우에는 프런트엔드 서버 앞단에 프록시 서버를 두는 방법도 있다. (Vite Proxy 등…)
    • 브라우저는 같은 오리진의 프런트엔드 서버에 요청하고, 프록시 서버가 이 요청을 백엔드 서버로 대신 전달한다. 브라우저에서 백엔드 서버로 직접 교차 출처 요청을 보내지 않으므로 CORS 에러를 피할 수 있다.

이터레이터 패턴 (iterator pattern)

이터레이터(iterator)를 이용해서 컬렉션(collection)의 요소들에 접근하는 패턴

map이든 set이든 array든 자료의 구조와는 관계없이 이터레이터라는 하나의 인터페이스로 접근할 수 있게 하는 것.

이터러블 프로토콜 : 객체가 [Symbol.iterator]() 메서드를 구현하여 반복 가능한 동작을 정의하는 규칙. for...of는 이 프로토콜을 이용한다.

이터레이터 프로토콜 : next() 메서드가 다음 값과 순회 완료 여부를 나타내는 { value, done } 객체를 반환하도록 정의한 규칙

이터러블한 객체 : 이터러블 프로토콜을 구현한 반복 가능한 객체

노출모듈 패턴 (revealing module pattern)

자바스크립트에서 즉시 실행 함수(IIFE)와 클로저를 이용해 외부에 공개할 API만 반환하고 내부 상태를 숨기는 패턴

아래 코드 출처 (약간 수정함) : https://github.com/wnghdcjfe/csnote/blob/main/ch1/12.js

const user = (() => {
  let name = "정대만" // private

  const publicApi = {
    greeting: "농구가 하고 싶어요",

    getName: () => name,
    setName: (newName) => {
      name = newName
    },
  }
  
  return publicApi;
})()

console.log(user.greeting) // 농구가 하고 싶어요
console.log(user.name) // undefined
console.log(user.getName()) // 정대만

user.setName("불꽃남자 정대만")
console.log(user.getName()) // 불꽃남자 정대만

user는 즉시 실행 함수의 반환값인 publicApi 객체를 담고 있다. publicApi 객체 안의 getName, setNamename을 계속 참조하고 있기 때문에, name은 즉시 실행 함수가 종료된 이후에도 사라지지 않고 클로저로 남는다. 이 name은 외부에서 직접 접근할 수 없고, publicApi 객체가 노출한 getName, setName을 통해서만 접근할 수 있기 때문에 private 상태처럼 사용할 수 있다.

참고로 ES2022에서 private field(#field) 문법이 추가되었다. (참고 : https://github.com/tc39/proposal-class-fields#private-fields)

MVC 패턴

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

  • 장점 : 각각의 구성요소에 집중해서 개발할 수 있기 때문에 재사용성과 확장성이 좋다.
  • 단점 : 애플리케이션이 복잡해질수록 모델과 뷰의 관계가 복잡해진다.

MVC 패턴

  • 모델
    • 애플리케이션의 데이터, 상태, 비즈니스 로직을 담당
    • 컨트롤러의 요청에 따라 상태를 조회하거나 변경
    • 사용자 인터페이스
    • 모델의 상태를 사용자에게 표시
  • 컨트롤러
    • 사용자의 입력을 받아 처리
    • 입력에 따라 모델을 변경하고 적절한 뷰를 선택하거나 갱신

MVP 패턴

프레젠터(Presenter)가 뷰의 입력을 처리하고 모델과 상호작용하며, 그 결과에 따라 뷰 갱신을 지시하는 패턴

뷰는 사용자 입력을 프레젠터에게 전달하고, 프레젠터는 모델과 상호작용한 결과에 따라 “이 텍스트를 보여줘”, “이 버튼을 비활성화해”, “이 에러 메시지를 표시해”처럼 뷰 갱신을 지시한다. 프레젠터가 뷰와 모델 사이의 상호작용을 중재하는 구조다.

뷰와 프레젠터가 일대일로 대응하도록 구현되는 경우에는 둘 사이의 결합도가 높아질 수 있다. 다만 뷰가 모델에 직접 의존하지 않고 프레젠터를 통해 상호작용하므로 뷰와 모델의 결합은 줄어든다.

MVP 패턴

MVVM 패턴

뷰에 필요한 상태와 동작을 뷰모델(ViewModel)이 제공하고, 뷰와 뷰모델을 데이터 바인딩으로 연결하는 패턴

뷰모델은 뷰를 추상화한 계층이다. 뷰와 뷰모델은 데이터 바인딩을 통해 연결되며, 프레임워크와 구현 방식에 따라 단방향 또는 양방향 데이터 바인딩을 사용할 수 있다.

MVVM 패턴

  • 장점
    • UI와 표현 로직을 분리하기 쉽다. : 뷰는 화면 표시를 담당하고, 뷰모델은 화면에 필요한 상태와 동작을 제공한다. 따라서 각각을 독립적으로 변경하고 유지보수하기 쉬워진다.
    • 단위 테스트가 쉽다 : 뷰모델이 특정 UI 프레임워크나 DOM에 직접 의존하지 않도록 구현하면, 화면을 띄우지 않고 뷰모델의 상태와 동작을 독립적으로 테스트할 수 있다.
  • 단점
    • 구조가 복잡해질 수 있다 : 간단한 화면에도 View, ViewModel, 바인딩 구조를 나누면 오히려 코드가 많아질 수 있다.
    • ViewModel이 비대해질 수 있다 : 화면 로직을 ViewModel에 모으다 보면 ViewModel이 너무 많은 책임을 가질 수 있다.
    • 데이터 바인딩 추적이 어려울 수 있다 : 값이 자동으로 반영되기 때문에, 어디서 변경됐는지 흐름을 파악하기 어려울 수 있다.

Vue.js가 MVVM 패턴의 특징을 가지고 있다. (하지만 최신 공식 문서에서는 직접적으로 MVVM 패턴을 언급하지는 않고 있다.)

데이터 바인딩 : 화면에 보이는 데이터와 애플리케이션의 상태를 연결해 일치시키는 기법

  • 양방향 데이터 바인딩 : View의 변경이 애플리케이션 상태에 반영되고, 애플리케이션 상태의 변경도 View에 반영되는 방식
  • 단방향 데이터 바인딩 : 애플리케이션 상태의 변경이 정해진 한 방향으로 View에 반영되는 방식

커맨드 : ViewModel이 사용자 동작을 실행 가능한 객체나 동작으로 제공하고, View가 이를 바인딩하여 실행하는 기법 예: View의 저장 버튼을 ViewModel의 저장 커맨드에 바인딩한다. 사용자가 버튼을 누르면 커맨드가 입력값을 검증하고 저장을 요청한 뒤 ViewModel의 상태를 변경하며, View는 바인딩된 상태의 변화를 화면에 반영한다.