📚 면접을 위한 CS 전공지식 노트 책을 바탕으로 CS 학습과 면접준비를 진행합니다.
디자인 패턴이란 프로그램을 설계할 때 발생했던 문제점들을 객체 간의 상호 관계 등을 이용하여 해결할 수 있도록 하나의 ‘규약’형태로 만들어 놓은 것
크게 생성패턴, 구조패턴, 행동패턴 3가지로 나뉜다.
싱글톤 패턴(singleton pattern)은 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴
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
*/
싱글톤 패턴의 단점인 모듈 간 강한 결합을 의존성 주입으로 느슨하게 만들어 해결할 수 있다.
의존성 주입이란 메인 모듈이 ‘직접’ 다른 하위 모듈에 대한 의존성을 주기보다는 중간에 의존성 주입자가 이 부분을 가로채 메인 모듈이 ‘간접’적으로 의존성을 주입하는 방식이다.
의존성 주입을 할 때는 의존관계역전원칙이 적용된다.
팩토리 패턴(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에 주입한다.
전략 패턴(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();
옵저버 패턴(observer pattern)은 주체가 어떤 객체의 상태 변화를 관찰하다가 상태 변화가 있을 때마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴
아래는 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]
을 "커플"
로 업데이트하게 된다.
이처럼 프록시 객체를 통해 특정 변화를 감시하는 옵저버 패턴을 구현해 볼 수 있다.
프록시 패턴(proxy pattern)은 대상 객체(subject)에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 디자인 패턴
객체의 속성, 변환 등을 보완하며 보안, 데이터 검증, 캐싱, 로깅에 사용된다. 앞서 살펴본 프록시 객체도 마찬가지이다.
프록시 서버(proxy server)는 서버와 클라이언트 사이에서 클라이언트가 자신을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해주는 컴퓨터 시스템이나 응용 프로그램을 가리킴
📌 CDN(Content Delivery Network)
각 사용자가 인터넷에 접속하는 곳과 가까운 곳에서 콘텐츠를 캐싱 또는 배포하는 서버 네트워크를 말한다.
리버스 프록시로 클라이언트가 웹 사이트의 원본 서버에 요청을 보낼 때 리버스 프록시 서버가 네트워크 에지에서 해당 요청을 가로챈다. 이를 통해 어떤 클라이언트도 원본 서버와 직접 통신하지 못 하도록 할 수 있다.
구분 | 정방향 프록시 서버 | 리버스 프록시 서버 |
---|---|---|
위치 | 클라이언트와 외부 서버 사이 | 클라이언트와 내부 서버 사이 |
사용자 | 클라이언트 | 서버 |
요청 대상 | 외부 서버 | 내부 서버 |
익명성 제공 대상 | 클라이언트의 IP를 숨김 | 서버의 IP를 숨김 |
주요 목적 | 클라이언트 보호, 네트워크 우회, 캐싱 | 서버 보호, 로드 밸런싱, SSL 종료, 캐싱 및 압축 |
📌 CORS(Cross-Origin Resource Sharing)
서버가 웹 브라우저에서 리소스를 로드할 때 다른 오리진을 통해 로드하지 못하게 하는 HTTP 헤더 기반 메커니즘
// 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 사용 시 필요
})
);
};
이터레이터 패턴(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
*/
노출모듈 패턴(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
로 명시적으로 노출하는 노출모듈 패턴이다.
MVC 패턴은 Model, View, Controller로 이루어진 디자인 패턴.
MVP 패턴은 MVC 패턴에서 Controller가 Presenter로 교체된 패턴
- Presenter : View에서 요청한 정보로 Model을 가공하여 View에 전달해 주는 부분. View와 Model을 붙여주는 접착제 역할을 한다.
뷰와 프레젠터는 1:1관계이기 때문에 MVC 패턴보다 더 강한 결합을 지닌 디자인 패턴이다. -> MVC 패턴의 뷰와 모델의 사이의 의존성 문제는 해결되었지만 뷰와 프레젠터의 의존성이 생긴다.
MVVM 패턴은 MVC 패턴에서 Controller가 View Model로 교체된 패턴
뷰모델은 뷰를 더 추상화한 계층이며, MVVM 패턴은 MVC 패턴과는 다르게 커맨드와 데이터 바인딩을 가지는 특징을 가지고 있다. 뷰와 뷰모델 사이의 양방향 데이터 바인딩을 지원하며 UI를 별도의 코드 수정 없이 재사용할 수 있고 단위 테스팅이 쉽다는 장점이 있다.