💡 좋은 코드 구조를 반복해서 사용할 수 있게 정리한 설계 방법(템플릿)

예를 들어,

| 분류 | 설명 | 예시 |
|---|---|---|
| 생성(Creational) 패턴 | 객체를 “어떻게 생성할까”에 초점 | Singleton, Factory, Builder, Prototype |
| 구조(Structural) 패턴 | 객체 간의 “구조/관계”를 효율적으로 만드는 패턴 | Adapter, Decorator, Composite, Proxy |
| 행위(Behavioral) 패턴 | 객체 간의 “행동(협력 방식)”을 정리 | Strategy, Observer, Command, State |
한 클래스에 인스턴스가 오직 하나만 존재하도록 보장하고, 그 인스턴스에 전역 접근점을 제공하는 패턴
| 개념 | 설명 |
|---|---|
private 생성자 | 외부에서 new 못하게 막음 |
static 변수 | 객체를 한 번만 저장 |
getInstance() | 같은 인스턴스를 계속 반환 |
== 비교 | 항상 true (같은 객체니까) |
public class Singleton {
// 1️⃣ 자기 자신을 저장할 static 변수
private static Singleton instance;
// 2️⃣ 외부에서 new 못하게 생성자를 private으로 막기
private Singleton() {}
// 3️⃣ 인스턴스 꺼내 쓰는 메서드 (필요할 때만 생성)
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 처음 한 번만 생성
}
return instance; // 항상 같은 객체 반환
}
}
public class Main {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); // true (같은 객체!)
}
}

복잡한 객체를 단계별로 생성할 수 있게 도와주는 패턴
예를 들어,
객체 만들 때 생성자에 너무 많은 파라미터가 있으면 헷갈린다. 👇
User user = new User("정인", 25, "서울", "개발자", true);
이거 보면 서울 이 주소였는지, 개발자 가 직업이었는지 한눈에 안 들어오지 않는다 😅
이럴 때 빌더 패턴을 쓰면 훨씬 보기 좋아진다!
public class User {
private String name;
private int age;
private String email;
// ✅ Builder 내부 클래스
public static class Builder {
private String name;
private int age;
private String email;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder email(String email) {
this.email = email;
return this;
}
public User build() {
return new User(name, age, email);
}
}
private User(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
}
public class Main {
public static void main(String[] args) {
User user = new User.Builder()
.name("정잉이")
.age(25)
.email("abc@gmail.com")
.build();
System.out.println(user);
}
}
@Builder 애노테이션으로 자동 생성 가능! 👇import lombok.Builder;
import lombok.ToString;
@Builder
@ToString
public class User {
private String name;
private int age;
private String address;
private String job;
private boolean active;
}
User user = User.builder()
.name("정잉")
.age(25)
.job("백엔드 개발자")
.build();
기존 객체에 기능을 덧붙이되, 상속 대신 ‘포장(wrapping)’으로 확장하는 패턴
햄버거 예시 👇
햄버거 클래스를 직접 수정하지 않고, “감싸서 기능을 추가”하는 것
// 1️⃣ 기본 컴포넌트 (인터페이스)
interface Coffee {
String make(); // 커피 만드는 기능
}
// 2️⃣ 구체적인 컴포넌트 (기본 커피)
class BasicCoffee implements Coffee {
public String make() {
return "기본 커피";
}
}
// 3️⃣ 데코레이터 추상 클래스 (공통 구조)
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee; // 감쌀 대상
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
public String make() {
return coffee.make();
}
}
// 4️⃣ 구체적인 데코레이터 (기능 확장)
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
public String make() {
return super.make() + " + 우유";
}
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
public String make() {
return super.make() + " + 설탕";
}
}
public class Main {
public static void main(String[] args) {
Coffee coffee = new BasicCoffee(); // 기본 커피
System.out.println(coffee.make()); // "기본 커피"
// ☕ 데코레이터로 감싸기
coffee = new MilkDecorator(coffee);
System.out.println(coffee.make()); // "기본 커피 + 우유"
coffee = new SugarDecorator(coffee);
System.out.println(coffee.make()); // "기본 커피 + 우유 + 설탕"
}
}
| 비교항목 | 상속(Inheritance) | 데코레이터(Decorator) |
|---|---|---|
| 기능 확장 방식 | 부모 클래스를 ‘확장’해서 새 클래스 만듦 | 기존 객체를 ‘감싸서’ 기능 추가 |
| 실행 시점 | 컴파일 시점 (정적) | 런타임 시점 (동적) |
| 조합 가능성 | 고정적 (한 번 상속하면 끝) | 유연함 (여러 개 감쌀 수 있음) |
| 클래스 수 | 많아짐 (조합마다 새 클래스 필요) | 적음 (데코레이터로 조합 가능) |
| 예시 | class MilkCoffee extends Coffee {} | new MilkDecorator(new Coffee()) |
| OCP 원칙 | 위반 가능 (코드 수정 필요) | 준수 (코드 수정 없이 확장 가능) |
class Coffee {
String make() { return "기본 커피"; }
}
class MilkCoffee extends Coffee {
String make() { return super.make() + " + 우유"; }
}
class IceCoffee extends Coffee {
String make() { return super.make() + " + 얼음"; }
}
class MilkIceCoffee extends Coffee { // 둘 다 섞은 버전
String make() { return super.make() + " + 우유 + 얼음"; }
}
👉 이렇게 되면...
MilkCoffeeIceCoffeeMilkIceCoffeeCoffee coffee = new BasicCoffee();
coffee = new MilkDecorator(coffee);
coffee = new IceDecorator(coffee);
System.out.println(coffee.make());
👉 실행 결과: 기본 커피 + 우유 + 얼음
➡️ 새 클래스를 안 만들고, 그냥 감싸는 순서만 바꾸면 조합이 바뀜! 🎯
행동(알고리즘)을 인터페이스로 정의하고, 그 구현체(전략)를 바꿔 끼우면서 다른 동작을 수행하게 하는 패턴
“오리 게임”을 만든다고 해보자!
class Duck {
public void quack() {
System.out.println("꽥꽥!");
}
public void fly() {
System.out.println("훨훨~");
}
}
근데! 고무오리는 날면 안된다!
그래서 아래처럼 하면?
class RubberDuck extends Duck {
@Override
public void fly() {
System.out.println("나는 못 날아요 ㅠㅠ");
}
}
👉 이렇게 되면 오리가 많아질수록 fly() 코드 중복이 심해지고 유지보수도 힘들어진다 😩
1️⃣ 전략 인터페이스 정의
interface FlyBehavior {
void fly();
}
2️⃣ 구체적인 전략(행동) 구현체 만들기
class FlyWithWings implements FlyBehavior {
public void fly() {
System.out.println("훨훨~ 날아요!");
}
}
class FlyNoWay implements FlyBehavior {
public void fly() {
System.out.println("저는 못 날아요 😢");
}
}
3️⃣ 오리 클래스에서 전략을 “조립”하기
class Duck {
private FlyBehavior flyBehavior;
public Duck(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
public void performFly() {
flyBehavior.fly(); // 전략에 따라 다르게 동작!
}
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
}
4️⃣ 실행
public class Main {
public static void main(String[] args) {
Duck mallard = new Duck(new FlyWithWings());
mallard.performFly(); // 훨훨~ 날아요!
Duck rubber = new Duck(new FlyNoWay());
rubber.performFly(); // 저는 못 날아요 😢
}
}
예를 들어,
결제 시스템 → CardPayment, KakaoPay , PaycoPayment
정렬 방식 → AscendingSort , DescendingSort
로그 전략 → ConsoleLogger , FileLogger
이런 것도 전략 패턴으로 바꿔끼는 구조이다 👇
PaymentStrategy pay = new KakaoPay();
pay.pay(10000); // 카카오페이 결제