[Java] 디자인 패턴(Design Pattern)

Jeini·2025년 10월 16일
0

☕️  Java

목록 보기
72/72

🧱 디자인 패턴(Design Pattern)

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

  • 즉, “이런 상황에서는 이렇게 짜면 좋더라~” 하는 선배 개발자들의 지혜의 집합이다.

예를 들어,

  • 객체를 새로 만들 때 복잡한 생성 과정을 숨기고 싶다 → Factory 패턴
  • 여러 객체가 한 번에 상태를 바꿔야 한다 → Observer 패턴
  • 객체의 기능을 런타임에 동적으로 추가하고 싶다 → Decorator 패턴

🧩 디자인 패턴의 3가지 분류

분류설명예시
생성(Creational) 패턴객체를 “어떻게 생성할까”에 초점Singleton, Factory, Builder, Prototype
구조(Structural) 패턴객체 간의 “구조/관계”를 효율적으로 만드는 패턴Adapter, Decorator, Composite, Proxy
행위(Behavioral) 패턴객체 간의 “행동(협력 방식)”을 정리Strategy, Observer, Command, State

🔍 싱글톤 패턴(Singleton Pattern)

한 클래스에 인스턴스가 오직 하나만 존재하도록 보장하고, 그 인스턴스에 전역 접근점을 제공하는 패턴

  • 간단히 말하면 “프로그램에서 단 하나의 객체만 필요할 때 쓰는 방법”이야.
    예: 로깅(로그 파일 하나), 설정(Configuration) 객체, 스레드 풀 매니저, 데이터베이스 연결 관리자(DB Manager) 등.

언제 쓰면 좋을까? ✅ / 언제 피할까? ❌

✅ 사용하면 좋은 상황

  • 애플리케이션 전체에서 상태를 공유해야 할 때 (예: 구성값)
  • 인스턴스 생성 비용이 크고 한 번만 만들면 될 때

❌ 피해야 할 상황

  • 테스트하기 어렵게 만들고 싶지 않을 때 (테스트 의존성 문제)
  • 전역 상태가 시스템 설계를 복잡하게 만드는 경우
  • DI(Dependency Injection)를 사용하는 프로젝트에서는 굳이 싱글톤을 직접 만들 필요 없이 컨테이너에 빈으로 등록하는 편이 더 낫다

⚙️ 기본 사용법과 예시

개념설명
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 (같은 객체!)
    }
}

🧩 FileWriter 멀티스레드 환경 예시


🔍 빌더 패턴(Builder Pattern)

복잡한 객체를 단계별로 생성할 수 있게 도와주는 패턴

예를 들어,
객체 만들 때 생성자에 너무 많은 파라미터가 있으면 헷갈린다. 👇

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);
    }
}
  • 인자 순서 헷갈리지 않음
  • 필요한 값만 선택해서 넣을 수 있음
  • 가독성 👍

🚀 실무에서는?

  • 롬복(Lombok)에서 @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();

🔍 데코레이터 패턴(Decorator Pattern)

기존 객체에 기능을 덧붙이되, 상속 대신 ‘포장(wrapping)’으로 확장하는 패턴

  • 즉, 기존 코드를 수정하지 않고 기능을 동적으로 추가하는 방법

🧩 비유로 이해하기 🍔

햄버거 예시 👇

  • 기본 햄버거 🍔 → Burger
  • 치즈 추가 🧀 → CheeseDecorator
  • 베이컨 추가 🥓 → BaconDecorator

햄버거 클래스를 직접 수정하지 않고, “감싸서 기능을 추가”하는 것


⚙️ 기본 사용법과 예시

// 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()); // "기본 커피 + 우유 + 설탕"
    }
}

💡 상속VS 데코레이터 비교

비교항목상속(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() + " + 우유 + 얼음"; }
}

👉 이렇게 되면...

  • 우유만 추가 = MilkCoffee
  • 얼음만 추가 = IceCoffee
  • 우유+얼음 추가 = MilkIceCoffee
    ➡️ 조합이 늘어날 때마다 새 클래스가 계속 생김 😵
    (3개, 4개 되면 조합이 폭발 💣)

② 데코레이터로 확장하는 방식

Coffee coffee = new BasicCoffee();
coffee = new MilkDecorator(coffee);
coffee = new IceDecorator(coffee);
System.out.println(coffee.make());

👉 실행 결과: 기본 커피 + 우유 + 얼음
➡️ 새 클래스를 안 만들고, 그냥 감싸는 순서만 바꾸면 조합이 바뀜! 🎯


🔍 전략 패턴(Strategy Pattern)

행동(알고리즘)을 인터페이스로 정의하고, 그 구현체(전략)를 바꿔 끼우면서 다른 동작을 수행하게 하는 패턴

  • "if문으로 조건 분기하지 말고, 전략 객체를 갈아끼워라!" 라는 개념 😎

⚙️ 기본 사용법과 예시

“오리 게임”을 만든다고 해보자!

🦆 기존 코드 (문제 있는 방식)

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); // 카카오페이 결제
profile
Fill in my own colorful colors🎨

0개의 댓글