📌 들어가며
안녕하세요! 멀티캠퍼스 유레카 3기 백엔드 과정을 수강 중인 개발자 지망생입니다.
코딩을 배우면서 항상 듣는 말이 있어요.
"좋은 코드는 패턴을 따른다. 디자인 패턴을 모르고 개발하면 결국 스파게티 코드가 된다."
처음엔 "패턴? 그냥 코드 짜는 방식 아닌가?"라고 생각했는데, 오늘 수업에서 여러 디자인 패턴을 배우고 실습해보니 왜 선배 개발자들이 패턴에 집착하는지 완전히 이해하게 됐어요! 😊
특히 실무에서 자주 쓰이는 패턴들을 중심으로 배웠는데, 이게 정말 코드 품질과 유지보수성에 큰 영향을 미친다는 걸 체감했습니다.
🎯 Today I Learned
✅ 디자인 패턴이란 무엇인가?
✅ 팩토리 메소드 패턴(Factory Method Pattern)
✅ 싱글톤 패턴(Singleton Pattern)
✅ 어댑터 패턴(Adapter Pattern)
✅ 옵저버 패턴(Observer Pattern)
✅ 스트레티지 패턴(Strategy Pattern)
✅ 템플릿 메소드 패턴(Template Method Pattern)
✅ 컴포짓 패턴(Composite Pattern)
✅ 패턴 선택의 기준
🤔 디자인 패턴, 왜 필요할까?
패턴 없이 개발하면 생기는 문제들
개발을 할 때 우리는 자주 이런 상황을 마주해요:
❌ 코드가 늘어날수록 수정이 어려워짐
한 곳을 고치면 다른 곳이 자꾸 터짐...
❌ 새로운 요구사항에 기존 코드를 자꾸 뜯어야 함
구조 자체가 유연하지 못해서 처음부터 다시 짜야 할 상황 발생
❌ 팀원이 이전 코드를 이해하는 데 시간이 오래 걸림
"이 코드는 왜 이렇게 짜여있는 거지?"
❌ 객체 간의 관계가 너무 복잡해짐
누가 누구에게 의존하는지 파악하기 어려움


바로 이런 문제들을 해결하기 위해 디자인 패턴이 필요해요!
디자인 패턴이란?
디자인 패턴 = 소프트웨어 설계 문제에 대한 '재사용 가능한 솔루션'
즉, 선배 개발자들이 수년간 겪은 문제들을 정리해서
"이 상황에는 이렇게 풀면 잘 풀린다!"고 정리한 것
패턴은 단순한 코드 스니펫이 아니라, 문제 인식 → 해결 방법 → 장단점 까지 포함하고 있어요.
📊 생성 패턴 - 객체를 어떻게 만들 것인가?
1️⃣ 싱글톤 패턴(Singleton Pattern)
"객체를 딱 하나만 만들고 싶다면?"
// ❌ 나쁜 예 - 매번 새로운 객체 생성
public class DatabaseConnection {
public static void main(String[] args) {
DatabaseConnection db1 = new DatabaseConnection();
DatabaseConnection db2 = new DatabaseConnection();
System.out.println(db1 == db2); // false - 다른 객체!
}
}
// ✅ 좋은 예 - 싱글톤 패턴
public class DatabaseConnection {
private static DatabaseConnection instance;
private DatabaseConnection() {
// private 생성자 - 외부에서 생성 불가
}
public static DatabaseConnection getInstance() {
if (instance == null) {
instance = new DatabaseConnection();
}
return instance;
}
}
// 사용
public class Main {
public static void main(String[] args) {
DatabaseConnection db1 = DatabaseConnection.getInstance();
DatabaseConnection db2 = DatabaseConnection.getInstance();
System.out.println(db1 == db2); // true - 같은 객체!
}
}
사용하는 곳:
- 데이터베이스 연결 객체
- 로깅 매니저
- 설정 파일 관리자
장점:
- ✅ 메모리 절약 (객체가 딱 하나)
- ✅ 전역 접근 가능
- ✅ 데이터 일관성 보장
주의할 점:
- ⚠️ 멀티스레드 환경에서는 동기화 필요
- ⚠️ 테스트가 어려워질 수 있음

2️⃣ 팩토리 메소드 패턴(Factory Method Pattern)
"객체 생성 로직을 한곳에 몰아서 관리하고 싶다면?"
// ❌ 나쁜 예 - 객체 생성이 여기저기 흩어짐
public class UserService {
public void createUser(String type) {
if (type.equals("admin")) {
AdminUser user = new AdminUser();
user.setup();
} else if (type.equals("normal")) {
NormalUser user = new NormalUser();
user.setup();
}
}
}
// ✅ 좋은 예 - 팩토리 메소드 패턴
public abstract class User {
public abstract void setup();
}
public class AdminUser extends User {
@Override
public void setup() {
System.out.println("어드민 권한 설정");
}
}
public class NormalUser extends User {
@Override
public void setup() {
System.out.println("일반 사용자 권한 설정");
}
}
// 팩토리 클래스
public class UserFactory {
public static User createUser(String type) {
switch(type) {
case "admin":
return new AdminUser();
case "normal":
return new NormalUser();
default:
throw new IllegalArgumentException("Unknown type: " + type);
}
}
}
// 사용
public class Main {
public static void main(String[] args) {
User admin = UserFactory.createUser("admin");
User normal = UserFactory.createUser("normal");
admin.setup();
normal.setup();
}
}
사용하는 곳:
- 데이터베이스 드라이버 로딩
- GUI 요소 생성
- API 응답에 따른 객체 생성
장점:
- ✅ 생성 로직이 한곳에 모임
- ✅ 새로운 타입 추가가 쉬움
- ✅ 클라이언트 코드가 단순해짐

🔌 구조 패턴 - 객체를 어떻게 조립할 것인가?
3️⃣ 어댑터 패턴(Adapter Pattern)
"기존 코드와 새로운 코드를 연결하고 싶다면?"
실생활 예: USB-C 충전기를 Lightning 단자에 사용하려면 어댑터가 필요하죠!
// 기존 코드 - 레거시 시스템
public interface OldPaymentSystem {
void pay(double amount);
}
public class KGinicsPay implements OldPaymentSystem {
@Override
public void pay(double amount) {
System.out.println("KGinics로 " + amount + "원 결제");
}
}
// 새로운 코드 - 새로운 시스템
public interface NewPaymentSystem {
void processPayment(double amount);
}
public class TosspaymentService implements NewPaymentSystem {
@Override
public void processPayment(double amount) {
System.out.println("Toss로 " + amount + "원 결제");
}
}
// 어댑터 - 기존 코드를 새로운 인터페이스에 맞춰줌
public class PaymentAdapter implements OldPaymentSystem {
private NewPaymentSystem newPaymentSystem;
public PaymentAdapter(NewPaymentSystem newPaymentSystem) {
this.newPaymentSystem = newPaymentSystem;
}
@Override
public void pay(double amount) {
// 기존 인터페이스 호출을 새로운 인터페이스로 변환
newPaymentSystem.processPayment(amount);
}
}
// 사용
public class Main {
public static void main(String[] args) {
NewPaymentSystem tossPay = new TosspaymentService();
// 기존 시스템과 새로운 시스템을 연결
OldPaymentSystem adapter = new PaymentAdapter(tossPay);
adapter.pay(10000); // "Toss로 10000원 결제"
}
}
사용하는 곳:
- 레거시 시스템과 신규 시스템 연결
- 써드파티 라이브러리 통합
- API 응답 형식 변환
장점:
- ✅ 기존 코드 수정 없이 새로운 코드 통합
- ✅ 호환성 문제 해결
- ✅ 점진적 마이그레이션 가능

4️⃣ 컴포짓 패턴(Composite Pattern)
"트리 구조의 복잡한 객체를 간단하게 다루고 싶다면?"
예: 폴더 구조 (폴더 안에 파일과 폴더가 있고, 그 폴더 안에 또 파일과 폴더가...)
// 공통 인터페이스
public interface FileSystemComponent {
void display();
long getSize();
}
// 파일
public class File implements FileSystemComponent {
private String name;
private long size;
public File(String name, long size) {
this.name = name;
this.size = size;
}
@Override
public void display() {
System.out.println("📄 " + name + " (" + size + "KB)");
}
@Override
public long getSize() {
return size;
}
}
// 폴더 (여러 컴포넌트를 담을 수 있음)
public class Folder implements FileSystemComponent {
private String name;
private List<FileSystemComponent> components = new ArrayList<>();
public Folder(String name) {
this.name = name;
}
public void addComponent(FileSystemComponent component) {
components.add(component);
}
@Override
public void display() {
System.out.println("📁 " + name);
for (FileSystemComponent component : components) {
component.display();
}
}
@Override
public long getSize() {
return components.stream()
.mapToLong(FileSystemComponent::getSize)
.sum();
}
}
// 사용
public class Main {
public static void main(String[] args) {
// 파일 생성
File file1 = new File("README.md", 50);
File file2 = new File("main.java", 100);
// 폴더 생성
Folder src = new Folder("src");
src.addComponent(file2);
Folder root = new Folder("프로젝트");
root.addComponent(file1);
root.addComponent(src);
// 전체 구조 출력
root.display();
// 전체 크기 계산
System.out.println("총 크기: " + root.getSize() + "KB");
}
}
출력:
📁 프로젝트
📄 README.md (50KB)
📁 src
📄 main.java (100KB)
총 크기: 150KB
사용하는 곳:
- 파일 시스템
- GUI 컴포넌트 (버튼 안에 레이아웃, 레이아웃 안에 버튼...)
- 조직도
장점:
- ✅ 복잡한 구조를 단순한 인터페이스로 다룸
- ✅ 재귀 구조를 깔끔하게 처리
- ✅ 클라이언트 코드가 단순해짐

🎬 행동 패턴 - 객체 간 상호작용을 어떻게 할 것인가?
5️⃣ 옵저버 패턴(Observer Pattern)
"한 객체의 상태 변화를 여러 객체에게 알리고 싶다면?"
예: 유튜브 구독 - 채널 업로드되면 구독자들에게 알림
// 옵저버 인터페이스
public interface Observer {
void update(String message);
}
// 구독자 (옵저버)
public class Subscriber implements Observer {
private String name;
public Subscriber(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + "님이 알림을 받았습니다: " + message);
}
}
// 채널 (주체)
public class YouTubeChannel {
private String name;
private List<Observer> subscribers = new ArrayList<>();
public YouTubeChannel(String name) {
this.name = name;
}
// 구독자 추가
public void subscribe(Observer observer) {
subscribers.add(observer);
}
// 구독 취소
public void unsubscribe(Observer observer) {
subscribers.remove(observer);
}
// 상태 변화 알림 (영상 업로드)
public void uploadVideo(String videoTitle) {
String message = "새 영상이 업로드됐습니다: " + videoTitle;
notifyObservers(message);
}
private void notifyObservers(String message) {
for (Observer observer : subscribers) {
observer.update(message);
}
}
}
// 사용
public class Main {
public static void main(String[] args) {
YouTubeChannel channel = new YouTubeChannel("코딩채널");
Subscriber sub1 = new Subscriber("김철수");
Subscriber sub2 = new Subscriber("이영희");
channel.subscribe(sub1);
channel.subscribe(sub2);
channel.uploadVideo("디자인 패턴 완벽 이해하기");
// 출력:
// 김철수님이 알림을 받았습니다: 새 영상이 업로드됐습니다: 디자인 패턴 완벽 이해하기
// 이영희님이 알림을 받았습니다: 새 영상이 업로드됐습니다: 디자인 패턴 완벽 이해하기
}
}
사용하는 곳:
- 이벤트 리스너 (버튼 클릭)
- 상태 변화 감지
- 발행-구독 시스템
장점:
- ✅ 느슨한 결합 (주체와 옵저버가 독립적)
- ✅ 동적으로 옵저버 추가/제거 가능
- ✅ 한 주체의 변화를 여러 객체가 반응

6️⃣ 스트레티지 패턴(Strategy Pattern)
"여러 알고리즘 중 상황에 따라 선택하고 싶다면?"
예: 결제 방법 - 신용카드, 계좌이체, 포인트 사용 등
// 전략 인터페이스
public interface PaymentStrategy {
void pay(double amount);
}
// 구체적인 전략들
public class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(double amount) {
System.out.println(cardNumber + "로 " + amount + "원을 신용카드 결제합니다");
}
}
public class BankTransferPayment implements PaymentStrategy {
private String accountNumber;
public BankTransferPayment(String accountNumber) {
this.accountNumber = accountNumber;
}
@Override
public void pay(double amount) {
System.out.println(accountNumber + "에서 " + amount + "원을 계좌이체합니다");
}
}
public class PointPayment implements PaymentStrategy {
private int availablePoints;
public PointPayment(int availablePoints) {
this.availablePoints = availablePoints;
}
@Override
public void pay(double amount) {
if (availablePoints >= amount) {
System.out.println(availablePoints + "포인트로 " + amount + "원을 결제합니다");
availablePoints -= amount;
} else {
System.out.println("포인트가 부족합니다");
}
}
}
// 컨텍스트 (결제 시스템)
public class Order {
private double totalAmount;
private PaymentStrategy paymentStrategy;
public Order(double totalAmount) {
this.totalAmount = totalAmount;
}
// 전략을 선택
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
// 선택된 전략으로 결제 실행
public void checkout() {
if (paymentStrategy == null) {
System.out.println("결제 방법을 선택해주세요");
return;
}
paymentStrategy.pay(totalAmount);
}
}
// 사용
public class Main {
public static void main(String[] args) {
Order order = new Order(50000);
// 신용카드로 결제
order.setPaymentStrategy(new CreditCardPayment("1234-5678-9012-3456"));
order.checkout();
// 계좌이체로 결제
order.setPaymentStrategy(new BankTransferPayment("123-456-789012"));
order.checkout();
// 포인트로 결제
order.setPaymentStrategy(new PointPayment(100000));
order.checkout();
}
}
사용하는 곳:
- 정렬/검색 알고리즘 선택
- 압축 방식 선택 (zip, gzip, bzip2)
- 배송 방법 선택
장점:
- ✅ 런타임에 전략 변경 가능
- ✅ 새로운 전략 추가가 쉬움
- ✅ 조건문 제거 가능

7️⃣ 템플릿 메소드 패턴(Template Method Pattern)
"공통된 절차는 유지하되, 특정 부분만 다르게 하고 싶다면?"
예: 커피와 차를 만드는 과정 - 물 끓이고, 음료 우리고, 잔에 따르고, 설탕 넣기...
// 템플릿 메소드를 정의한 추상 클래스
public abstract class Beverage {
// 템플릿 메소드 - 전체 절차를 정의
public final void makeBeverage() {
boilWater();
brew();
pour();
addCondiments();
}
// 공통 메소드
protected void boilWater() {
System.out.println("물을 끓입니다");
}
protected void pour() {
System.out.println("잔에 따릅니다");
}
// 서브클래스에서 구현해야 할 메소드들
protected abstract void brew();
protected abstract void addCondiments();
}
// 구체적인 구현 - 커피
public class Coffee extends Beverage {
@Override
protected void brew() {
System.out.println("커피 가루를 우립니다");
}
@Override
protected void addCondiments() {
System.out.println("설탕과 우유를 넣습니다");
}
}
// 구체적인 구현 - 차
public class Tea extends Beverage {
@Override
protected void brew() {
System.out.println("찻잎을 우립니다");
}
@Override
protected void addCondiments() {
System.out.println("레몬을 넣습니다");
}
}
// 사용
public class Main {
public static void main(String[] args) {
System.out.println("=== 커피 만들기 ===");
Beverage coffee = new Coffee();
coffee.makeBeverage();
System.out.println("\n=== 차 만들기 ===");
Beverage tea = new Tea();
tea.makeBeverage();
}
}
출력:
=== 커피 만들기 ===
물을 끓입니다
커피 가루를 우립니다
잔에 따릅니다
설탕과 우유를 넣습니다
=== 차 만들기 ===
물을 끓입니다
찻잎을 우립니다
잔에 따릅니다
레몬을 넣습니다
사용하는 곳:
- 데이터베이스 연결 절차
- 파일 처리 (열기, 읽기, 닫기)
- API 요청 처리 (인증, 요청, 응답 처리)
장점:
- ✅ 코드 중복 제거
- ✅ 전체 흐름은 유지하면서 세부 구현만 변경
- ✅ 확장이 용이함

📊 패턴 선택 가이드
상황 추천 패턴 이유
| 객체가 하나만 필요할 때 | Singleton | 메모리 절약, 전역 접근 |
| 생성 로직이 복잡할 때 | Factory Method | 생성 로직 캡슐화 |
| 기존 코드와 새 코드를 연결 | Adapter | 호환성 유지 |
| 한 객체의 변화를 여러 객체에 알려야 할 때 | Observer | 느슨한 결합 |
| 상황에 따라 다른 알고리즘 적용 | Strategy | 런타임 선택 가능 |
| 공통 절차는 유지, 세부만 다를 때 | Template Method | 코드 중복 제거 |
| 트리 구조의 복잡한 객체 처리 | Composite | 계층 구조 단순화 |
💡 오늘 나는 무엇을 배웠는가
1. 패턴은 '정답'이 아니라 '선택지'다
패턴을 배울 때 가장 중요한 깨달음은 "이 패턴이 항상 정답인가?"가 아니라 "이 상황에 어떤 패턴이 적합한가?"를 판단하는 거예요.
같은 코드도 상황에 따라 다른 패턴이 필요할 수 있습니다.
2. 패턴을 알면 코드 리뷰가 쉬워진다
❌ "이 코드 뭔데 이렇게 짜여있어?"
✅ "아, 이건 Strategy 패턴이네. 그럼 이렇게 하면 좋을 것 같은데..."
패턴을 알면 다른 개발자의 의도를 빠르게 파악할 수 있어요!
3. 패턴은 점진적으로 배운다
모든 패턴을 다 알 필요는 없어요. 프로젝트를 하다 보면 "어, 이 상황 봤는데..."하면서 자연스럽게 패턴이 들어가게 됩니다.
4. 과도한 패턴 사용은 독이다
"내가 배운 패턴을 다 써야 해!"라는 마음으로 패턴을 남용하면 오히려 코드가 복잡해져요.
"가장 단순한 코드가 가장 좋은 코드다"
필요할 때만 패턴을 사용하세요!
😊 좋았던 점 & 😅 아쉬웠던 점
좋았던 점
👍 "아, 이게 패턴이었군!"
실무에서 이미 쓰던 코드들이 실은 디자인 패턴이었다는 걸 깨달았어요. 처음 배울 때보다 이해가 훨씬 빠르고 와닿았습니다!
👍 코드 품질의 차이가 명확함
같은 기능을 구현하는데도 패턴을 따르는 것과 안 따르는 것의 차이가 확연히 드러났어요. 특히 유지보수와 확장성 면에서요.
👍 팀 협업이 쉬워질 것 같다
패턴을 공유하는 언어처럼 쓸 수 있다는 게 정말 매력적이에요. "이건 Factory 패턴으로 리팩토링하면 좋을 것 같은데?"라고 말하면 모두가 이해할 거예요.
아쉬웠던 점
😢 실무 규모의 프로젝트 예제 부족
간단한 예제는 많은데, 실제 프로젝트에서는 여러 패턴이 섞여 있을 거 같아요. 그런 통합 예제가 있으면 좋을 것 같았습니다.
😢 각 패턴의 함정 공유 부족
"이 패턴은 언제 쓰면 안 되는가?"에 대한 설명이 더 있으면 좋았을 것 같아요.
😢 패턴 간의 연관성
여러 패턴이 어떻게 함께 작동하는지에 대한 이해가 부족해요.
🎓 나만의 학습 팁
1. 패턴은 '문제'부터 시작한다
❌ "이건 Factory 패턴이다. 그럼 이렇게 짜자"
✅ "이 부분이 계속 바뀌는데, 어떻게 하지? 아, Factory 패턴이 딱 이런 상황을 해결한다!"
먼저 문제를 정의하고, 그 다음에 패턴을 찾으세요.
2. 코드로 직접 구현해보기
이 글의 모든 예제를 직접 IDE에서 타이핑해서 실행해보세요. 책이나 블로그로만 봐서는 절대 습득 안 됩니다!
3. 패턴 간의 비교
Factory vs Singleton:
- Factory는 "생성하는 방식을 정의"
- Singleton은 "생성 개수를 제한"
Strategy vs Template Method:
- Strategy는 "전략을 런타임에 선택"
- Template Method는 "상속으로 구조를 정의"
비슷해 보이지만 다른 패턴들을 비교하면서 공부하세요!
4. 개인 프로젝트에 적용해보기
1. 간단한 프로젝트부터 시작 (계산기, 투두 리스트)
2. Factory 패턴으로 객체 생성 리팩토링
3. Strategy 패턴으로 알고리즘 분리
4. 깃허브에 커밋할 때 "패턴 적용" 정도만 기록
점진적으로 패턴을 녹여내는 경험이 중요해요!
5. GoF 책은 나중에
"Design Patterns: Elements of Reusable Object-Oriented Software" (Gang of Four 책)은 정말 어려워요. 기초를 충분히 다진 다음에 읽으세요!
🚀 다음 학습 목표
📌 단기 목표 (이번 주)
✓ 위의 7가지 패턴을 각각 직접 구현해보기
✓ 패턴별로 사용하는 곳 3가지씩 정리
✓ 간단한 프로젝트에 Factory + Strategy 적용
📌 중기 목표 (이번 달)
✓ 기존 코드를 리팩토링하며 패턴 적용해보기
✓ "이 부분을 어떤 패턴으로 개선할 수 있을까?" 고민 시간 가지기
✓ 팀원들과 "이건 XXX 패턴이 좋을 것 같은데" 토론
📌 장기 목표 (부트캠프 기간)
✓ 포트폴리오 프로젝트에서 4가지 이상 패턴 적용
✓ 코드 리뷰할 때 패턴 관점에서 피드백 주기
✓ 다른 패턴들(State, Proxy, Chain of Responsibility 등) 공부
🎬 마치며
오늘 수업을 듣기 전에는 "디자인 패턴? 어려워 보이는데 꼭 알아야 해?"라고 생각했어요.
하지만 여러 패턴을 배우다 보니 깨달은 게 있습니다.
**디자인 패턴은 "좋은 코드를 짜기 위한 선배 개발자들의 지혜"**입니다.
패턴을 알면:
- ✅ 코드가 왜 이렇게 짜여있는지 이해하기 쉬워져요
- ✅ 새로운 요구사항에 빠르게 대응할 수 있어요
- ✅ 팀원들과의 커뮤니케이션이 훨씬 명확해져요
- ✅ 버그가 줄어들고 유지보수가 쉬워져요
물론 모든 상황에 패턴이 정답은 아니에요. 때로는 단순한 코드가 최고의 디자인 패턴일 수 있습니다.
"필요할 때 쓰되, 과하지 않게. 그게 바로 좋은 개발자의 선택이다."
다음부터 코드를 짤 때는 "이 부분을 어떤 패턴으로 설계하면 좋을까?"라고 한 번만 고민해보세요.
그것이 바로 좋은 개발자로 성장하는 길입니다! 💪