이 포스트는 면접 스터디에서 진행하는 '면접을 위한 CS 전공지식 노트' 책을 기반으로 공부하는 내용을 다룹니다
디자인 패턴은 프로그램을 설계할 때 문제점을 해결할 수 있게 만들어놓은 '약속'이다.

클래스 하나에 오직 하나의 인스턴스만 갖도록 하는 패턴
주로 데이터베이스 연결 모듈처럼, 인스턴스를 여러 개 만들 필요가 없는 곳에 많이 사용된다.
장점: 인스턴스 생성 비용을 줄일 수 있다.
단점: 모듈 간의 의존성이 높아지고, TDD(주로 단위 테스트)를 할 때 매 테스트마다 독립적인 인스턴스를 만들기 어려워 테스트가 까다로워진다.
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
싱글톤으로 인해 높아진 모듈 간의 결합도는 의존성 주입(DI)을 통해 해결할 수 있다.

의존성(종속성)이란, A가 B에 의존하고 있을 때 B의 변경 사항에 따라 A도 함께 변해야 하는 관계를 말한다.
의존성 주입을 하기 전에는 메인 모듈이 자신에게 연결된 수많은 하위 모듈을 직접 신경 써야 했다면, 의존성 주입 이후에는 별도의 의존성 주입자가 이를 대신 관리해주기 때문에 모듈을 쉽게 교체하고, 테스트하고, 마이그레이션할 수 있게 된다.
다만 모듈이 더욱 잘게 분리되면서 복잡성이 증가하고, 런타임 패널티가 생길 수 있다는 단점도 있다.
의존성 주입 원칙 상위 모듈은 하위 모듈로부터 어떤 것도 가져오지 않는다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 하며, 추상화는 세부 사항에 의존하지 않는다.

객체를 "사용"하는 코드에서 "객체 생성" 부분을 추상화한 패턴
객체 생성 로직이 사용하는 코드와 분리되어 있기 때문에 유연성과 유지보수성이 향상된다.
interface Coffee {
String getName();
}
class Latte implements Coffee {
public String getName() { return "Latte"; }
}
class Americano implements Coffee {
public String getName() { return "Americano"; }
}
class CoffeeFactory {
public static Coffee createCoffee(String type) {
switch (type) {
case "latte":
return new Latte();
case "americano":
return new Americano();
default:
throw new IllegalArgumentException("알 수 없는 타입: " + type);
}
}
}
// 사용 예시
Coffee coffee = CoffeeFactory.createCoffee("latte");
System.out.println(coffee.getName()); // Latte

객체의 행위를 바꾸고 싶을 때, "전략"이라는 "캡슐화한 알고리즘"을 이용해 상호 교체가 가능하도록 만든 패턴
interface PaymentStrategy {
void pay(int amount);
}
class CardPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println(amount + "원을 카드로 결제합니다.");
}
}
class CashPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println(amount + "원을 현금으로 결제합니다.");
}
}
class ShoppingCart {
private PaymentStrategy strategy;
public ShoppingCart(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void checkout(int amount) {
strategy.pay(amount);
}
}
// 사용 예시
ShoppingCart cart = new ShoppingCart(new CardPayment());
cart.checkout(10000); // 10000원을 카드로 결제합니다.

주체가 어떤 객체의 상태 변화를 관찰하다가, 변화가 있을 때마다 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴
interface Observer {
void update(String message);
}
class Subscriber implements Observer {
private String name;
public Subscriber(String name) {
this.name = name;
}
public void update(String message) {
System.out.println(name + " 수신: " + message);
}
}
class Subject {
private List<Observer> observers = new ArrayList<>();
public void subscribe(Observer observer) {
observers.add(observer);
}
public void notifyAll(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}
// 사용 예시
Subject subject = new Subject();
subject.subscribe(new Subscriber("Alice"));
subject.subscribe(new Subscriber("Bob"));
subject.notifyAll("새 글이 등록되었습니다."); // 각 구독자에게 알림 전달
옵저버 패턴은 주로 이벤트 기반 시스템에 사용된다.
Vue.js 3.0에서 ref나 reactive로 정의한 값을 변경했을 때 자동으로 DOM에 있는 값이 변경되는데, 이것이 바로 옵저버 패턴을 이용해 구현된 것이다.

객체에 접근하기 전 그 흐름을 가로채, 객체의 인터페이스 역할을 하는 패턴
객체로도 쓰이지만, 프록시 서버로도 많이 활용된다.
프록시 서버는 server와 client 사이에서, client가 자신(프록시 서버)을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해주는 컴퓨터 시스템 혹은 응용 프로그램이다.
Node.js 서버를 운영할 때 앞단에 Nginx를 두거나, Cloudflare를 통해 CDN 서비스를 이용하면서 동시에 공격 보호를 받을 수 있다.
CDN (Content Delivery Network)
각 사용자가 인터넷에 접속하는 곳과 가까운 곳에서 콘텐츠를 캐싱 또는 배포하는 서버 네트워크를 말한다. 이를 통해 사용자가 웹 서버로부터 콘텐츠를 다운로드하는 시간을 줄일 수 있다.
FE 개발 시 FE 서버와 BE 서버끼리 통신할 때 주로 CORS 에러를 마주치는데, 이때도 프록시 서버를 활용하는 경우가 많다.

CORS(Cross-Origin Resource Sharing)는 서버가 웹 브라우저에서 리소스를 로드할 때, 다른 오리진을 통한 로드를 막는 HTTP 헤더 기반 메커니즘이다.
예를 들어 FE는 localhost:3000에서 테스트하는데 BE는 localhost:12010이라면 포트가 달라 CORS 에러가 발생한다. 이때 프록시 서버를 두어 FE에서 보내는 요청의 오리진을 localhost:12010으로 바꿔주는 방식으로 문제를 해결할 수 있다.

이터레이터를 이용해 컬렉션의 요소들에 접근하는 패턴으로, 내부 구조를 노출하지 않고도 순차적으로 요소에 접근할 수 있게 해줍니다.
import java.util.Iterator;
import java.util.ArrayList;
import java.util.List;
public class IteratorExample {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
즉시 실행 함수를 통해 필요한 변수나 함수만 선택적으로 외부에 노출시키는 패턴
const CounterModule = (function () {
let count = 0; // private 변수
function increase() {
count += 1;
}
function getCount() {
return count;
}
// 외부에 노출할 것만 선택적으로 반환
return {
increase,
getCount,
};
})();
CounterModule.increase();
CounterModule.increase();
console.log(CounterModule.getCount()); // 2
console.log(CounterModule.count); // undefined (외부에서 접근 불가)

모델은 애플리케이션의 데이터(DB, 상수, 변수 등)입니다. 뷰에서 데이터를 생성하거나 수정하면, 컨트롤러를 통해 모델을 생성하거나 갱신합니다.
뷰는 사용자가 볼 수 있는 화면입니다. 모델이 가지고 있는 정보를 따로 저장하지 않고 화면에 표시만 해주는 역할을 하며, 변경이 일어나면 컨트롤러에게 이를 전달합니다.
컨트롤러는 모델과 뷰를 잇는 다리 역할을 합니다. 모델이나 뷰로부터 변경 통지를 받으면, 이를 각 구성 요소에 전달합니다.
MVC 패턴의 대표적인 예시로는 React가 있습니다.

MVP 패턴은 MVC에서 컨트롤러가 프레젠터(Presenter)로 교체된 패턴입니다. 뷰와 프레젠터가 1대1 관계를 가지기 때문에 MVC보다 더욱 결합이 강한 패턴입니다.
MVVM 패턴은 MVC에서 컨트롤러가 뷰모델(ViewModel)로 바뀐 패턴입니다.
뷰모델은 뷰를 더 추상화한 계층으로, 뷰와 뷰모델 사이에 양방향 데이터 바인딩을 지원합니다.
데이터 바인딩 화면에 보이는 데이터와 웹 브라우저의 메모리 데이터를 일치시키는 기법으로, 뷰모델을 변경하면 뷰가 자동으로 변경됩니다.
Vue.js와 같은 반응형 프론트엔드 프레임워크가 대표적인 예시입니다.