티스토리 뷰
면접을 준비하면서 느낀건데 CS 지식이 많이 부족한거 같습니다. 그래서 모자른 지식을 채우기 위해 책을 읽고 있는데, 그 내용을 기반으로 정리해 보려고 합니다
그 첫 번째 챕터로 '디자인 패턴과 프로그래밍 패러다임'을 선정했습니다.
방대한 양의 패턴들을 무작정 외우기보다는 각 패턴이 '어떤 문제를 해결하는가?'에 초점을 맞추어 제가 사용하고있는 자바 언어 기반으로 재해석해 보았습니다
1. 싱글톤(Singleton) 패턴
[개념] 애플리케이션 내에서 특정 클래스의 인스턴스가 오직 하나만 생성되도록 보장하고, 이 인스턴스에 접근할 수 있는 전역적인 접촉점을 제공하는 생성 디자인 패턴
[쉬운 풀이] 회사에 있는 '공용 프린터'를 생각해보자. 직원(객체)들이 프린트를 할 때마다 새로운 프린터를 사오는 것이 아니라, 이미 설치된 프린터 1대를 다 같이 공유해서 사용한다.
데이터베이스 연결 모듈처럼 똑같은 객체를 여러 번 만들 필요가 없는 경우, 객체를 매번 새로 생성하면 메모리 낭비가 심해진다. 그래서 "딱 하나만 만들어서 메모리에 올려두고, 다 같이 공유해서 쓰자"는 것이 싱글톤 패턴의 핵심이다
public class Singleton {
// 1. 단 하나뿐인 인스턴스를 저장할 전역(static) 변수
private static Singleton instance;
// 2. 외부에서 새로운 객체를 생성하지 못하도록 차단
private Singleton() {
}
// 3. 외부에서 인스턴스를 가져다 쓸 수 있는 유일한 접근점(메서드)
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 인스턴스가 아예 없을 때만 새로 생성
}
return instance; // 이미 있다면 기존 것을 반환
}
}
[싱글톤 패턴의 트레이드 오프(Trade-off)]
장점: 객체를 한 번만 생성하므로 메모리 낭비를 줄이고, 어디서든 동일한 인스턴스에 접근할 수 있어 데이터 공유가 효율적
단점: 객체 간의 결합도가 높아져서, 나중에 코드를 테스트하거나 변경할 때 까다로워질 수 있다. (의존성 문제)
2. 팩토리(factory) 패턴
[개념] 객체를 사용하는 코드에서 객체 생성 로직을 분리하여 추상화한 패턴. 상위 클래스는 전체적인 뼈대(인터페이스)를 정의하고, 구체적인 객체 생성은 하위 클래스가 담당하도록 한다.
[쉬운 풀이] 우리가 자주 가는 카페를 떠올려보자. 손님(클라이언트)은 "아메리카노 주세요"라고 주문만 할 뿐, 바리스타(팩토리)가 원두를 어떻게 갈고 샷을 어떻게 내리는지 구체적인 생성 과정은 알 필요가 없다. 그저 '아메리카노'라는 조건만 넘겨주면, 바리스타가 알아서 완성된 커피(객체)를 만들어 준다. 프로그래밍에서도 여기저기서 복잡하게 new 키워드로 객체를 만들지 않고, 객체를 전문적으로 찍어내는 '공장' 역할을 하는 클래스에 생성을 맡기는 방식이다.
// 1. 공통 인터페이스 (커피)
interface Coffee {
void drink();
}
// 2. 구체적인 객체들 (아메리카노, 라떼)
class Americano implements Coffee {
public void drink() { System.out.println("아메리카노를 마신다."); }
}
class Latte implements Coffee {
public void drink() { System.out.println("라떼를 마신다."); }
}
// 3. 객체를 대신 생성해주는 팩토리 클래스
class CoffeeFactory {
public static Coffee createCoffee(String type) {
if ("americano".equals(type)) {
return new Americano(); // 아메리카노 생성
} else if ("latte".equals(type)) {
return new Latte(); // 라떼 생성
}
return null;
}
}
[팩토리 패턴의 트레이드 오프(Trade-off)]
장점: 객체 생성 로직이 한곳에 집중되어 유지보수가 쉬움. 클라이언트는 객체 생성 방식을 몰라도 되므로 결합도가 낮아진다. 그리고 객체 생성 로직이 따로 뗴어져 있으므로 리팩토링 과정에서도 한 곳만 고치면 되니 유지 보수성이 증가한다
단점: 새로운 객체를 추가할 때마다 팩토리 클래스의 코드를 수정해야 할 수도 있음. 클래스 개수가 많아져서 구조가 조금 복잡해질 수 있다.
3. 전략(strategy 패턴
[개념] 특정 연산을 수행하는 알고리즘(동작)들을 각각 하나의 클래스로 캡슐화하고, 이들을 상호 교체할 수 있도록 만드는 행위 디자인 패턴이다. 이를 통해 클라이언트 코드를 변경하지 않고도 런타임(프로그램 실행 중)에 알고리즘을 동적으로 변경할 수 있음. 정책 패턴 (policy pattern) 이라고도 한다.
[쉬운 풀이] 스마트폰의 '지도 앱(내비게이션)'을 생각해보자. 목적지는 같지만 '자동차', '대중교통', '도보' 중 어떤 버튼을 누르느냐에 따라 길을 찾는 방식(전략)이 완전히 달라진다. 지도 앱 자체(뼈대)는 그대로 두고, 길 찾기 '전략' 부품만 갈아 끼우는 것. 프로그래밍에서도 어떤 동작을 하는 로직을 독립된 클래스로 분리해두고, 필요할 때마다 부품을 교체하듯이 갈아 끼워 사용하는 방식.
// 1. 전략 인터페이스 (길 찾기 방법)
interface RouteStrategy {
void buildRoute(String start, String end);
}
// 2. 구체적인 전략 클래스들 (자동차, 도보)
class CarStrategy implements RouteStrategy {
public void buildRoute(String start, String end) {
System.out.println(start + "에서 " + end + "까지 자동차 도로로 안내합니다.");
}
}
class WalkStrategy implements RouteStrategy {
public void buildRoute(String start, String end) {
System.out.println(start + "에서 " + end + "까지 좁은 골목길을 포함한 도보 경로로 안내합니다.");
}
}
// 3. 전략을 사용하는 컨텍스트 (내비게이션)
class Navigator {
private RouteStrategy strategy; // 어떤 전략을 사용할지 저장
// 프로그램 실행 중에도 언제든 전략을 교체할 수 있게 해주는 메서드
public void setStrategy(RouteStrategy strategy) {
this.strategy = strategy;
}
public void navigate(String start, String end) {
strategy.buildRoute(start, end); // 현재 설정된 전략 실행
}
}
[전략 패턴 사용 예시] 결제 시스템에 자주 사용된다. 신용카드, 카카오페이, 네이버페이 등 결제 수단을 전략만 바꿔서 결제가 가능하다.
[전략 패턴의 트레이드 오프(Trade-off)]
장점: 알고리즘(전략)을 런타임에 동적으로 교체할 수 있어 유연하다. if-else나 switch로 도배된 복잡한 로직을 별도의 클래스로 분리하여 가독성을 높임
단점: 각 전략을 클래스로 구현해야 하므로 프로젝트 전체의 클래스 개수가 늘어남.
4. 옵저버(observer) 패턴
[개념] 주체가 어떤 객체의 상태 변화를 관찰하다가 상태변화가 있을 때마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴이다
[쉬운 풀이] 유튜브 구독 알림 시스템을 생각하면 잘 맞다 우리가 좋아하는 유튜버의 채널을 구독해두면, 유튜버가 새 영상을 올렸을 때 나의 기기로 "새 영상이 올라왔습니다" 하고 알림이 온다. 우리가 매분 매초 채널에 들어가서 "영상 올렸나?" 하고 확인할 필요가 없음. 여기서 유튜버는 '관찰 대상(Subject)'이고, 구독자는 '관찰자(Observer)'가 됨. 상태 변화(영상 업로드)가 생기면 관찰자들에게 자동으로 알림을 쫙 뿌려주는 것이 옵저버 패턴.
// 1. 관찰자(구독자) 인터페이스
interface Observer {
void update(String videoTitle);
}
// 2. 구체적인 관찰자 (실제 유튜브 사용자)
class Subscriber implements Observer {
private String name;
public Subscriber(String name) { this.name = name; }
public void update(String videoTitle) {
System.out.println(name + "님, 알림: [" + videoTitle + "] 영상이 업로드되었습니다!");
}
}
// 3. 관찰 대상 (유튜브 채널)
class YoutubeChannel {
// 구독자 명단
private List<Observer> subscribers = new ArrayList<>();
// 구독하기
public void subscribe(Observer observer) {
subscribers.add(observer);
}
// 영상 업로드 및 알림 보내기
public void uploadVideo(String title) {
System.out.println("--- 유튜브 채널에 '" + title + "' 업로드 됨 ---");
// 명단에 있는 모든 구독자에게 알림 전송
for (Observer sub : subscribers) {
sub.update(title);
}
}
}
[옵저버 패턴의 트레이드 오프(Trade-off)]
장점:
느슨한 결합(Loose Coupling): 알림을 보내는 쪽(채팅방)은 알림을 받는 쪽(사용자)이 정확히 누구인지 몰라도 된다. 서로 코드가 강하게 얽혀있지 않아서, 새로운 구독자를 추가하거나 빼는 등의 코드 수정이 매우 쉽고 유연.
불필요한 확인 작업 제거: 데이터가 바뀌었는지 매번 주기적으로 물어볼 필요(Polling) 없이, 변화가 생겼을 때만 즉각적으로 대응할 수 있어 시스템이 효율적으로 동작.
단점:
메모리 누수 위험: 알림을 받겠다고 등록(구독)만 해두고, 다 쓴 후에 제대로 해지(구독 취소)하지 않으면 안 쓰는 객체가 메모리에 계속 남아 자원을 낭비하게 됨.
디버깅의 어려움: 알림을 받는 옵저버가 너무 많아지거나 서로 꼬리에 꼬리를 물고 알림을 발생시키면, 코드가 실행되는 순서를 예측하기 어렵고 버그가 났을 때 원인을 추적하기가 까다로워진다..
5. 프록시(proxy) 패턴
[개념] 대상 객체에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 디자인 패턴이다.
이를 통해 객체의 속성, 변환 등을 보완하며 보안, 데이터 검증, 캐싱, 로깅에 사용
[쉬운 풀이] 우리가 상점에서 물건을 살 때 쓰는 '체크카드'를 생각해보자. 결제를 할 때 은행에 있는 내 '계좌(실제 객체)'에서 직접 현금을 꺼내서 주지 않는다. 대신 '체크카드(프록시)'를 점원에게 건넨다. 체크카드는 결제 요청을 받으면 내 계좌에 잔액이 충분한지 먼저 검사(접근 제어)한 뒤에, 조건이 맞을 때만 진짜 은행 계좌에 접근해 돈이 빠져나가게 한다.
// 1. 공통 인터페이스 (결제 수단)
interface Payment {
void pay(int amount);
}
// 2. 실제 객체 (내 은행 계좌 - 실제 돈이 빠져나가는 곳)
class BankAccount implements Payment {
public void pay(int amount) {
System.out.println("은행 계좌에서 " + amount + "원이 출금되었습니다.");
}
}
// 3. 대리인 객체 (체크카드)
class CheckCard implements Payment {
private BankAccount account;
private int balance = 10000; // 현재 잔액
public void pay(int amount) {
System.out.println("[카드 단말기] 결제를 시도하며 잔액을 확인합니다..."); // 부가 기능 추가
// 프록시에서 먼저 조건을 검사 (접근 제어)
if (amount > balance) {
System.out.println("[결제 실패] 잔액이 부족합니다.");
return; // 실제 계좌(BankAccount)에는 접근조차 하지 않음
}
// 조건이 맞을 때만 실제 객체 생성 및 위임
if (account == null) {
account = new BankAccount();
}
account.pay(amount);
balance -= amount; // 잔액 차감
}
}
[프록시 패턴의 트레이드 오프(Trade-off)]
장점:
안전한 접근 제어: 실제 객체(은행 계좌)에 아무나 직접 접근하는 것을 막고, 대리인(체크카드) 선에서 유효성 검사(잔액 확인 등)를 먼저 처리하여 시스템을 안전하게 보호할 수 있음.
관심사의 분리(단일 책임 원칙): 진짜 중요한 로직(실제 출금)과 부가적인 로직(검증, 로깅 등)을 분리할 수 있음. 기존 은행 계좌 클래스의 코드를 수정하지 않고도 체크카드 클래스에 새로운 기능을 얼마든지 추가할 수 있어 유지보수가 편해짐.
단점:
코드 복잡도 증가: 인터페이스를 만들고 대리인 클래스를 추가로 구현해야 하므로, 프로젝트 전체의 클래스 개수가 늘어나 구조가 조금 복잡해짐
6. 이터레이터(iterator) 패턴
[개념] 이터레이터(iterator)를 사용하여 컬렉션(collection)의 요소들에 접근하는 디자인 패턴. 컬렉션(리스트, 큐, 트리 등)이 내부에 데이터를 어떻게 저장하고 있는지 그 복잡한 속사정을 노출하지 않고도, 그 안에 들어있는 모든 항목을 순차적으로 하나씩 꺼내볼 수 있게 해줌
[쉬운 풀이] 우리가 매일 듣는 '음악 앱의 플레이리스트'를 생각해보자. 플레이리스트 안에 노래들이 배열(Array) 형태로 나란히 저장되어 있는지, 아니면 꼬리에 꼬리를 무는 리스트(Linked List) 형태로 흩어져 있는지 우리는 알 필요가 없다. 그저 앱 화면에서 '다음 곡(Next)' 버튼만 누르면 알아서 다음 노래가 흘러나온다. 이처럼 데이터가 어떤 구조로 저장되어 있든 상관없이 hasNext()(다음 곡 있어?)와 next()(그럼 다음 곡 틀어줘)라는 공통된 버튼(인터페이스)만 제공해서, 데이터를 일관된 방식으로 순회할 수 있게 해주는 것이 이터레이터 패턴의 핵심이다.
// 1. 이터레이터(반복자) 인터페이스 - '다음 곡' 버튼 역할
interface Iterator {
boolean hasNext(); // 다음 데이터가 있는지 확인
String next(); // 다음 데이터를 가져옴
}
// 2. 컬렉션 인터페이스 - 이터레이터를 만들어내는 역할
interface Aggregate {
Iterator createIterator();
}
// 3. 구체적인 컬렉션 (음악 플레이리스트)
class MusicPlaylist implements Aggregate {
private String[] songs;
private int count = 0;
public MusicPlaylist(int size) {
songs = new String[size];
}
public void addSong(String song) {
if (count < songs.length) {
songs[count++] = song;
}
}
// 데이터 구조를 순회할 '전용 이터레이터'를 생성해서 반환
@Override
public Iterator createIterator() {
return new PlaylistIterator(this);
}
public String getSongAt(int index) {
return songs[index];
}
public int getCount() {
return count;
}
}
// 4. 구체적인 이터레이터 (플레이리스트 안을 돌아다니는 탐색기)
class PlaylistIterator implements Iterator {
private MusicPlaylist playlist;
private int index = 0; // 현재 탐색 위치를 기억함
public PlaylistIterator(MusicPlaylist playlist) {
this.playlist = playlist;
}
@Override
public boolean hasNext() {
return index < playlist.getCount(); // 노래가 더 남았는지 확인
}
@Override
public String next() {
return playlist.getSongAt(index++); // 현재 노래를 반환하고, 위치를 한 칸 이동
}
}
[이터레이터 패턴의 트레이드 오프(Trade-off)]
장점:
내부 구조 숨김 (캡슐화): 데이터를 사용하는 쪽(클라이언트)에서는 데이터가 배열인지 리스트인지 몰라도 next()만 호출하면 됨. 나중에 데이터 저장 방식을 배열에서 리스트로 완전히 뜯어고쳐도, 밖에서 호출하는 코드는 단 한 줄도 수정할 필요가 없어 매우 유연함.
책임 분리 (단일 책임 원칙): 컬렉션(플레이리스트)은 '데이터를 저장하고 관리하는 일'만 신경 쓰고, 이터레이터는 '데이터를 순서대로 꺼내는 일'만 전담하므로 코드가 훨씬 깔끔해짐.
단점:
구조의 복잡화: 요소가 몇 개 없는 아주 단순한 배열이나 리스트라면, 굳이 인터페이스를 만들고 이터레이터 클래스까지 따로 구현하는 것이 오버엔지니어링이 될 수 있음. 클래스 개수가 늘어나 프로젝트가 다소 복잡해짐.
7. 노출 모듈(reavling module) 패턴
[개념] 즉시 실행 함수를 통해 자바의 private, public 과 같은 접근 제어자를 만드는 디자인 패턴이다
[자바 개발자를 위한 참고 사항]
사실 이 패턴은 자바(Java)보다는 과거의 자바스크립트(JavaScript)에서 아주 중요하게 쓰이던 패턴입니다. 자바에는 변수나 메서드를 숨길 수 있는 private, public 같은 접근 제어자가 처음부터 있었지만, 예전 자바스크립트에는 그런 기능이 없었습니다. 그래서 모든 데이터가 밖으로 새어 나가는 것을 막기 위해 '함수'로 내부를 감싸고, 바깥에서 쓸 기능만 return으로 빼주는 '노출 모듈 패턴'을 억지로 만들어 써야만 했습니다. 즉, "자바의 클래스와 private/public 키워드가 하는 역할을 자바스크립트에서 흉내 낸 것"이라고 이해하시면 가장 정확합니다.
[노출 모듈 패턴의 트레이드 오프(Trade-off)]
장점:
데이터 캡슐화와 보호: 외부에서 알 필요가 없거나 함부로 건드리면 안 되는 중요한 변수를 안전하게 숨겨둘 수 있음.
명확한 가독성: 코드를 읽는 사람 입장에서 파일의 맨 밑(return 부분)만 보면 "아, 외부에서 쓸 수 있는 기능은 이것들뿐이구나" 하고 직관적으로 파악할 수 있어 코드가 깔끔해짐.
단점:
테스트의 어려움: 내부(private)에 꽁꽁 숨겨둔 함수나 변수들은 외부에서 접근할 수 없기 때문에, 단위 테스트(Unit Test)를 꼼꼼하게 작성하기가 매우 까다로움.
내부 확장성 부족: 숨겨진 함수를 나중에 상속받거나 외부에서 수정해서 써야 할 일이 생기면, 구조를 다 뜯어고쳐야 하는 불편함 존재.
8. MVC, MVP, MVVM 패턴
[개념] 애플리케이션을 세 가지 역할로 나누어, 화면에 보이는 시각적 요소(UI)와 뒤에서 돌아가는 비즈니스 로직(데이터 처리)을 분리하는 설계 방식.
[쉬운 풀이] 식당의 운영 시스템을 생각해보자.
- Model(모델): 주방 (식재료, 요리법, 데이터)
- View(뷰): 손님이 보는 테이블과 메뉴판 (사용자 화면, UI)
- Controller / Presenter / ViewModel: 홀 매니저 (손님의 주문을 주방에 전달하고, 음식을 테이블로 가져다주는 중간 역할)
이 세 가지 패턴은 홀 매니저가 주방(Model)과 테이블(View) 사이를 어떤 방식으로 조율하는지에 따라 나뉜다.

MVC (Model - View - Controller)
가장 기본이 되는 패턴. 손님(사용자)이 컨트롤러에게 주문(입력)을 하면, 컨트롤러가 주방(모델)에 요리를 지시하고, 주방에서 요리가 완성되면 테이블(뷰)이 주방의 상태를 직접 확인해서 화면을 갱신한다.
- 특징: 자바의 '스프링(Spring Web MVC)'이 이 패턴을 기반으로 설계되었다.
- 문제점: 뷰와 모델이 서로 쳐다보고 있어(의존성이 있어), 프로젝트가 커지면 컨트롤러가 비대해지고 코드가 스파게티처럼 꼬이기 쉽다.
MVP (Model - View - Presenter)
MVC의 문제를 해결하기 위해 등장. 이제 테이블(뷰)과 주방(모델)은 철저히 남남이 된다. 모든 소통은 오직 홀 매니저(Presenter)를 통해서만 이루어진다.
- 특징: 뷰와 모델 간의 결합도가 완전히 사라져서 코드를 테스트하기가 매우 좋아진다.
- 문제점: 뷰와 프레젠터가 1:1로 강하게 짝지어지기 때문에, 화면이 많아질수록 프레젠터 클래스도 많아지고 결합도도 높아진다는 단점이 있다.
MVVM (Model - View - ViewModel)
MVP에서 한 단계 더 진화한 형태. 매니저(ViewModel)가 주방(모델)에서 가져온 데이터를 '진열장'에 올려두면, 테이블(뷰)이 진열장을 '구독(Data Binding)'하고 있다가 데이터가 바뀌는 순간 스스로 화면을 갱신한다.
- 특징: Vue.js나 React 같은 현대 프론트엔드 프레임워크의 핵심 사상이다. 뷰가 뷰모델을 쳐다볼 뿐, 뷰모델은 뷰가 누군지 신경 쓰지 않아도 되므로 코드가 매우 독립적이고 유연해진다.
마무리: 수많은 패턴, 결국 실무에서는 뭘 제일 많이 쓸까?
지금까지 여러 가지 디자인 패턴을 살펴보았는데 처음 보면 "이걸 언제 다 외우고 언제 다 쓰지?" 싶을 수 있습니다. 결론부터 말씀드리면, 이 모든 패턴의 구조를 달달 외울 필요는 전혀 없다고 합니다
당연한 얘기겠지만, 자바 기반의 스프링 생태계를 다룬다면 그래도 아래의 세가지 패턴 만큼은 그 원리를 꼭 기억해두는게 좋을 것입니다.
싱글톤(Singleton) 패턴: 오늘날 우리가 직접 getInstance()를 만들 일은 거의 없습니다. 하지만 스프링이 객체(Bean)를 관리하는 기본 방식이 바로 싱글톤입니다. 스프링 컨테이너가 알아서 딱 하나의 객체만 만들어 메모리를 효율적으로 관리해 주고 있습니다
프록시(Proxy) 패턴: 스프링의 핵심인 AOP(관점 지향 프로그래밍)나 @Transactional 같은 마법 같은 어노테이션들이 모두 프록시 패턴을 기반으로 돌아갑니다. 진짜 핵심 로직을 건드리지 않고, 대리인이 앞뒤로 부가 기능(로그, 트랜잭션 등)을 붙여주는 역할을 프레임워크가 대신해 주는 것입니다.
전략(Strategy) 패턴: 소셜 로그인이나 결제 수단을 갈아 끼워야 할 때 가장 많이 쓰입니다. 또한 스프링 시큐리티(Spring Security)의 인증 아키텍처 자체가 이 전략 패턴의 집합체라고 볼 수 있습니다.
디자인 패턴은 무조건적인 정답이 아니라 오답노트 느낌이 강한것 같습니다.
무작정 패턴의 틀에 코드를 억지로 끼워 맞추기보다는 현재 내가 어떤 문제를 해결하려고 하는가? 에 대한 고민이 먼저인거 같습니다.
'스터디' 카테고리의 다른 글
| 프로세스와 스레드 (0) | 2026.07.25 |
|---|---|
| HTTP 프로토콜의 발전 (0) | 2026.07.19 |
| 네트워크의 기초 (0) | 2026.07.19 |
| 면접 스터디 내용 정리 (0) | 2026.07.10 |
