CS 디자인패턴
업데이트:
카테고리: CS
/태그: MVC, MVP, MVVM, 노출모듈패턴, 싱글톤패턴, 옵저버패턴, 이터레이터 패턴, 전략패턴, 팩토리패턴, 프록시서버, 프록시패턴
싱글톤(Singleton) 패턴
객체의 인스턴스가 오직 1개만 생성되는 패턴
구조
getInstance
를 선언한다. 이 메서드는 자체 클래스의 공유를 반환한다.
장점
- 메모리 이점
- 최초 한번의 new 연산자를 통해서 고정된 메모리 영역을 사용하기에 추후 해당 객체에 접근할 때 메모리 낭비를 방지할 수 있고, 이미 생성된 인스턴스를 활용해서 속도도 빠르다.
- 데이터 공유
- 싱글톤 인스턴스가 전역으로 사용되기에 다른 클래스의 인스턴스가 접근하여 사용할 수 있다.
- 여러 인스턴스에서 한 인스턴스에 동시에 접근하는 것이므로 동시성 문제가 발생할 수 있어 유의해야 한다.
문제
- 구현하는 코드가 많이 필요함
- 정적 팩토리 메서드에서 객체 생성을 확인하고 생성자를 호출하는 경우에 멀티스레딩 환경에서 발생할 수 있는 동시성 문제를 해결하기 위해
syncronize
키워드가 필요하다.
- 정적 팩토리 메서드에서 객체 생성을 확인하고 생성자를 호출하는 경우에 멀티스레딩 환경에서 발생할 수 있는 동시성 문제를 해결하기 위해
- 테스트의 어려움(TDD)
- 한 인스턴스라는 자원을 공유하고 있기 때문에 테스트가 격리된 환경에서 수행되려면 매번 인스턴스의 상태를 초기화해야된다.
- 전역으로 한 인스턴스의 상태를 공유하기 때문에 테스트를 온전하게 수행하기 힘들다.
- 클라이언트가 구체 클래스에 의존
- new 키워드를 직접사용해서 클래스 안에서 객체를 생성하고 있어 SOLID원칙 중 DIP을 위반하고, OCP 원칙 또한 위반할 가능성이 높다.
DIP(Dependency Inversion Principle)이란?
의존 역전 원칙은 “고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다. 저수준 모듈이 고수준 모듈에서 정의한 추상타입에 의존해야 한다.”를 의미한다. “자신보다 변하기 쉬운것에 의존하지 마라”라고 간단하게 얘기할 수 있다.DIP 예제
대표적인 예시로는 자동차와 스노우 타이어
겨울에는 스노우 타이어를 구매해서 자동차에 장착해서 운행해야된다.
즉, 고수준 모듈인 자동차가 저수준 모듈인 스노우 타이어에 의존하는 상태이다.날씨가 따뜻해지면 더 이상 스노우 타이어가 필요없으므로 일반 타이어로 교체해야한다.
하지만, 단순히 타이어만 바꾼다고 끝나는게 아니라 자동차에도 영향을 끼치게 된다.이것은 개방-폐쇄 원칙을 위반하는 것으로 추상화나 다형성을 통해서 문제를 고쳐야 합니다. DIP도 추상화를 이용하여 해당 문제를 해결합니다.
스노우 타이어나 일반 타이어를
타이어
로 추상화한다.
타이어는 저수준 모듈이 아니라 고수준 모듈인 자동차의 입장에서 만들어 지는데, 이것이 고수준 모듈이 저수준 모듈에 의존했던 상황이 역전되어 저수준 모듈이 고수준 모듈에 의존하게 된다고 해서 의존 역전 원칙이라고 한다.이대 소스 코드의 의존은 자동차가 ‘타이어’를 의존하지만, 런타임에서는 타이어가 아니라 하위 타이어 중 하나를 의존한다. 그러므로 DIP는 소스 코드 단계에서의 의존을 역전시킨다는 것을 기억해야된다.
OCP(Open-Closed Principle)이란?
소프트웨어 엔티티(클래스, 모듈, 함수 등)은 확장에 대해서는 열려있어야 되지만 변경에 대해서는 닫혀있어야 한다.
즉, 기존 코드는 변경하지 않으면서 기능을 추가할 수 있도록 설계해야 한다.
이 원칙을 지키기 위해 추상화와 다형성을 활용한다.OCP적용 전
FileStorage에는 2가지 메서드가 존재한다.save_to_ORACLE()
: ORACLE DB에 저장save_to_MySQL()
: MySQL DB에 저장 만약 나중에 새로운 DB를 추가하게 되면 FileStorage 클래스를 수정해야 되므로 OCP를 위반하게 된다.
OCP적용 후
save()
추상 메서드를 가지고 있는 FileStorage를 정의하고 DB에 저장하기 위한 ORACLE, MySQL을 만들어 상속시킨다.
이때 새로운 DB를 추가한다면 클래스를 만들어 추가하면 된다. - 자식클래스를 만들 수 없음
- 내부 상태 변경이 어려움, 유연성이 많이 떨어짐
팩토리 메서드(Factory Method) 패턴
객체를 생성할 때 어떤 클래스의 인스턴스를 만들지 서브 클래스에서 결정한다. 즉, 인스턴스 생성을 서브 클래스에 위임한다.
팩토리 메서드 패턴와 추상 팩토리 패턴이 있다.
Pattern | 공통점 | 차이점 |
---|---|---|
팩토리 메서드 패턴 | 객체의 생성부를 캡슐화하여 결합을 느슨하게 함 | 상속을 통해 서브클래스에서 팩토리 메소드를 오버라이딩하여 객체의 생성부를 구현 |
추상 팩토리 패턴 | 구체적인 타입에 의존하지 않도록 함 | 객체의 집합을 생성하기 위한 정의를 추상체에 위치시키고 하위의 구현체에서 세부적인 집합 생성 과정을 구현 |
팩토리 메서드
부모 클래스에 알려지지 않은 구체 클래스를 생성하는 패턴이며, 자식 클래스가 어떤 객체를 생성할지를 결정하도록 하는 패턴이다.
- Product : 팩토리 메서드로 생성될 객체의 공통 인터페이스
- ConcreteCreator: 구체적으로 객체가 생성되는 클래스
- Creator : 팩토리 메서드를 갖는 클래스
- ConcreteCreator : 팩토리 메서드를 구현하는 클래스로 ConcreteProduct 객체를 생성
왜 쓰는가?
객체가 생성하기 위한 인터페이스를 정의하지만, 어떤 클래스의 인스턴스를 생성할지에 대한 결정은 서브클래스에서 이루어지도록 하여 재정의 가능한 것으로 설계하지만, 복잡해지지 않게 한다.
장점
- 기존 코드를 수정하지 않고 새로운 인스턴스를 다른 방법으로 생성하도록 확장할 수 있다. 의존성을 제거하고 결합도를 낮춘다.
- Product와 Creator간의 결합이 느슨
- 확장에 열려있고 변경에 닫혀있는 원칙(OPC)을 적용했기에 가능함
- 코드가 간결하다
- 병렬적 계층도를 연결하는 역할을 담당할 수 있음
단점
- 클래스가 많아짐
- Product 클래스가 바뀔 때 마다 새로운 서브클래스를 생성해야 됨
- 클라이언트가 creator클래스를 반드시 상속해 Product를 생성해야 함
전략 패턴
객체의 행위를 바꾸고 싶을 때 직접 수정하지 않고 전략이라고 부르는 캡슐화 알고리즘을 컨텍스트 안에서 바꿔주면서 상호 교체가 가능하게 만드는 패턴
strategy
: 인터페이스나 추상 클래스로 외부에서 동일한 방식으로 알고리즘을 호출하는 방법을 명시ConcreateStrategy1,2,3
: strategy에서 명시한 알고리즘을 실제로 구현한 클래스Context
: strategy 패턴을 이용하는 역할 수행, 필요에 따라 동적으로 구체적인 전략을 바꿀 수 있도록 setter 메서드 제공
설계
- 추상 클래스 Robot의 자식 클래스: TaekwonV, Atom
- 추상 클래스 Robot의 메서드 정의: attack(), move()
문제점
새로운 기능 수정의 경우
- 기존 로봇의 공격 또는 이동 방법을 수정하려면 어떻게 해야되는가?
-
새로운 로봇을 만들어 기존의 공격 또는 이동 방법을 추가하거나 수정하려면?
- 변경이 일어나게 되면 모든코드를 수정해야된다.
해결책
- 변화된 것을 찾은 후 캡슐화
- 이동방식: move()
- 공격방식: attack()
- 외부에 구첵적인 이동 방식, 공격 방식을 담은 클래스 은닉화
- 공격, 이동을 위한 인터페이스 생성, 실현할 클래스 생성
옵저버패턴
쥬체가 어떤 객체의 상태 변화를 관찰하다가 상태 변화가 있을 때마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴
대표적으로 활용한 예시는 유튜브가 있다
내가 어떤 사람을 구독 했다면 주체가 동영상을 올렸을 떄 알람이 구독자에게 가야된다.
옵저버 패턴에서는 한 객체의 상태가 바꾸면 그 객체에 의존하는 다른 객체에게 연락이 가고, 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다
또한, 옵저버 패턴은 주로 이벤트 기반의 시스템에 사용하며 MVC(Model-View-Controller)패턴에도 사용된다.
예를 들어 주체라고 볼 수 있는 model에서 변경사항이 생겨 update() 메서드로 옵저버인 view에 알려주고 이를 기반으로 controller가 작동한다.
JavaScript에서의 옵저버 패턴
프록시 객체를 통해서 구현할 수 있다
프록시 객체 어떠한 대상의 기본적인 동작(속성 접근, 할당, 순회, 열거, 함수 호출 등)의 작업을 가로챌 수 있는 객체를 뜻하며, 두 개의 매개변수를 가진다.
target
: 프록시할 대상handler
: 프록시 객체의 target 동작을 가로채서 정의할 동작들이 정해져 있는 함수
프록시 객체 예제
const handler = {
// get : 속성과 함수에 대한 접근을 가로챈다.
get: function(target, name) {
return name === 'name' ? `${target.a} ${target.b}` : target[name]
}
}
const p = new Proxy({a: 'KUNDOL', b: 'IS AUMUMU ZANGIN'}, handler)
console.log(p.name) // KUNDOL IS AUMUMU ZANGIN
프록시를 이용한 옵저버 패턴
function createReactiveObject(target, callback){
const proxy = new Proxy(target, {
// set: 속성에 대한 접근을 가로챈다.
set(obj, prop, value) {
// 해당 객체, 여기서는 b.형규 !== "솔로" 가 아니라면 값을 변경하고
// 변경된 값을 출력해준다.
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.형규 = "솔로"
b.형규 = "커플"
Vue.js 3.0의 옵저버 패턴
ref
나 reactive
로 정의하면 해당 값이 변경되었을 때 자동으로 DOM에 있는 값이 변경된다.
이는 프록시객체를 이용해서 구현한 옵저버 패턴을 활용한 것입니다.
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${string(target)}`)
}
return target
}
if (
target[ReactiveFlags.RAM] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// proxyMap이라는 프록시 객체를 사용함 + get, set메서드를 사용
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers)
proxyMap.set(target, proxy)
return proxy
}
프록시 패턴과 프록시 서버
프록시 객체는 디자인 패턴 중 프록시 패턴이 녹아들어 있는 객체이다.
프록시 패턴
프록시 패턴은 대상 객체에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 디자인 패턴이다.
장점
- 사이즈가 큰 객체가 로딩되기 전에도 프록시를 통해 참조할 수 있음
- 실제 객체의 public, protected 메소드를 숨기고 인터페이스를 통해 노출시킬 수 있음
- 로컬에 있지 않고 떨어져있는 객체를 사용할 수 있음
- 원래 객체에 접근에 대해 사전처리를 할 수 있음
단점
- 객체를 생성할 때 한 단계를 거치게 되므로, 빈번한 객체 생성이 필요한 경우는 성능이 저하될 수 있음
- 프록시 내부에서 객체 생성을 위해 스레드가 생성, 동기화가 구현되어야 하는 경우 성능이 저하될 수 있음
- 로직이 난해해져 가독성이 떨어질 수 있음
프록시 서버
서버와 클라이언트 사이에서 클라이언트가 자신을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해주는 컴퓨터 시스텐이나 응용프로그램이다.
프록시 서버로 쓰는 nginx
nginx는 비동기 이벤트 기반의 구조와 다수의 연결을 효과적으로 처리 가능한 웹 서버, 주로 node.js
서버 앞단의 프록시 서버로 활용된다.
Node.js 창시자
- “Node.js의 버퍼 오버플로우 취약점을 예방하기 위해서는 nginx를 프록시 서버로 앞단에 놓고 Node.js를 뒤쪽에 놓는 것이 좋다.”
이것을 통해 사용자의 직접적인 서버로의 접근을 차단하고 간접적으로 한 단계를 더 거침으로써 보안성을 더욱 강화할 수 있다.
실제 포트를 숨길 수 있고, 정적 자원을 gzip으로 압축하거나, 메인 서버 앞단에서의 로깅을 할 수 있음
버퍼 오버플로우: 버퍼는 데이터가 저장되는 메모리 공간으로, 메모리 공간을 벗어나는 경우를 말한다. 이때 사용되지 않아야 할 영역에 데이터가 덮어씌워져 주소, 값을 바꾸는 공격이 발생하기도 한다.
gzip 압축: 데이터 전송량을 줄일 수 있지만, 압축을 해제했을 때 서버에서의 CPU 오버헤드도 생각해서 gzip 압축 사용 유무를 결정한다.
프록시 서버로 쓰는 CloudFare
- DDOS 공격 방어 DDOS는 짧은 시간동안 네크워크에 많은 요청을 보내 네트워크를 마비시켜 웹 사이트의 가용성을 방해하는 사이버 공격 유형입니다. CloudFare는 시스템을 통해 오는 트래픽을 자동으로 차단해서 DDOS 공격으로부터 보호합니다.
- HTTPS 구축
CORS와 프런트엔드의 프록시 서버
CORS(Cross-Origin Resource)는 서버가 웹 브라우저에서 리소스를 로드할 때 다른 오리진을 통해 로드하지 못하게 하는 HTTP헤더 기반의 메커니즘이다.
프런트 개발시 프런트엔드 서버를 만들어서 백엔드 서버와 통신할 때 주로 CORS 에러를 마주치는데, 이를 해결하기 위해 프런트엔드에서 프록시 서버를 만들기도 한다.
예를 들어 프런트는 127.0.0.1:3000
으로 테스트하는데 백엔드는 127.0.0.1:12010
이라면 포트 번호가 다르기 때문에 CORS 에러가 난다. 그래서 프록시 서버를 두어 프론트엔드 서버에서 요청되는 오리진을 127.0.0.1:12010
으로 바꾸어 준다.
이터레이터 패턴
이터레이터를 사용하여 컬렉션의 요소의 접근하는 디자인 패턴
const mp = new Map()
mp.set('a', 1)
mp.set('b', 2)
mp.set('c', 3)
const st = new Set()
st.add(1)
st.add(2)
st.add(3)
for (let a of mp) console.log(a)
for (let a of st) console.log(a)
⇒ 다른 자료 구조인 set, map 이지만 for a of b
라는 이터레이터 프로토콜을 통해서 순회한다.
이터레이터 프로토콜
이터러블한 객체를 순회할 때 쓰이는 규칙
이터러블한 객체
반복 가능한 객체로 배열을 일반화한 객체
노출모듈 패턴
즉시 실행 함수를 통해 private
, public
같은 접근 제어자를 만드는 패턴
자바스크립트는 접근제어자가 존재하지 않기 때문에 노출모듈 패턴을 통해서 private
와 public
접근 제어자를 구현한다.
const pukuba = (() => {
const a = 1
const b = () => 2
// public 객체만 함수 밖으로 나올 수 있다.
const public = {
c: 2,
d: () => 3
}
return public
})()
console.log(pukuba) // {c: 2, d: [Function: d]}
console.log(pukuba.a) // undefined
public
클래스에 정의된 함수에서 접근 가능하며 자식 클래스와 외부 클래스에서 접근 가능
protected
클래스에 정의된 함수에서 접근 가능, 자식 클래스에서 접근 가능하나 외부 클래스에는 접근 가능
private
클래스에 정의된 함수에서만 접근 가능
즉시 실행 함수
함수를 정의하자마자 바로 호춣하는 함수, 초기화 코드, 라이브러리 내 전역 변수의 충돌 방지에 사용
MVC 패턴
MVC 패턴은 모델(Model), 뷰(View), Controller(컨트롤러)로 이루어진 디자인 패턴
재사용성과 확장성이 용이하나, 애플리케이션이 복잡해질수록 모델과 뷰의 관계가 복잡해진다.
모델
- 애플리케이션 데이터인
데이터베이스
,상수
,변수
등을 뜻함 - 사각형 모양이 박스 안에 글자가 들어있다면, 박스의 위치 정보, 글자 내용, 글자 위치, 글자 포맷에 관한 정보를 모두 가지고 있어야한다.
- 뷰에서 데이터를 생성하거나 수정하면 컨트롤러를 통해 모델을 생성하거나 갱신합니다.
뷰
input
,checkbox
,textarea
등 사용자 인터페이스 요소- 모델을 기반으로 사용자가 볼 수 있는 화면
- 모델이 가지고 있는 정보를 따로 저장하지 않아야 하며 단순히 사각형 모양 등 화면에 표시하는 정보만 가지고 있어야함
컨트롤러
- 하나 이상의 모델과 하나 이상의 뷰를 잇는 다리 역할을 하며 이벤트 등 메인 로직을 담당
- 모델과 뷰의 생명주기 관리
- 모델이나 뷰의 변경 통지를 받으면 이를 해석하여 각각의 구성 요소에 해당 내용을 알린다.
React
유저 인터페이스를 구축하기 위한 라이브러리로 ‘가상 DOM’을 통해서 실제 DOM을 조작하는 것을 추상화해서 성능을 높였다.
대표적인 특성으로는 불변성(immutable)이 있다.
state는 setState를 통해서만 수정이 가능하고, props를 기반으로 만들어진는 컴포넌트인 pureComponent가 있다. 단방향 바인딩이 적용되어 있어, 자유도가 높다.
MVP 패턴
MVC 패턴에서 파생된 패턴으로 C에 해당하는 컨트롤러가 프레젠터(presenter)로 교체된 패턴
뷰와 프레전터는 일대일 관계이기 때문에 MVC패턴보다 더 강한 결합을 지닌 디자인 패턴이다.
MVVM패턴
MVC 패턴에 C 해당하는 컨트롤러가 뷰모델로 바뀐 패턴이다.
뷰 모델은 뷰를 더욱 추상화한 계층이며, 해당 패턴은 커맨드와 데이터 바인딩을 가지는 것이 특징
뷰와 뷰 모델 사이의 양방향 데이터 바인딩을 지원하며 UI를 별도의 코드 수정없이 재사용할 수 있고 단위 테스팅이 쉽다.
커맨드
여러 가지 요소에 대한 처리를 하나의 액션으로 처리할 수 있게 하는 기법
데이터 바인딩
화면에 보이는 데이터와 웹 브라우저의 메모리 데이터를 일치시키는 기법으로, 뷰 모델을 변경하면 뷰가 변경된다.
Vue
Vue는 반응형이 특징인 프레임워크로, watch와 computed로 쉽게 반응형적인 값을 구축할 수 있다.
함수를 사용하지 않고 값 대입(v-model)만으로도 변수가 변경되며 양방향 바인딩, html을 토대로 컴포넌트를 구축할 수 있다는 점이 특징이다.