Spring Boot/레이어트 아키텍처, 디자인 패턴

[Spring boot] 디자인 패턴

재윤 2025. 7. 21. 16:21
반응형
  • 소프트웨어를 설계할 때 자주 발생하는 문제들을 해결하기 위해 고안된 해결책이다.
  • 디자인 패턴에서 ‘패턴’이라는 단어는 애플리케이션 개발에서 발생하는 문제는 유사한 경우가 많고 해결책도 동일하게 적용할 수 있다는 의미를 내표한다.
  • but 디자인 패턴이 모든 문제의 정답은 아니며, 상황에 맞는 최적 패턴을 결정해서 사용

디자인 패턴의 종류

  • 대표적 분류 방식인 ‘GoF 디장니 패턴’ == ‘Gang for Four’의 줄임말
    • 구체화하고 체계화해서 분류한 4명의 인물 의미
  • Gof 디자인 패턴은 생성 패턴, 구조 패턴, 행위 패턴의 총 3가지로 구분됨

생성 패턴 → 객체 생성에 사용되는 패턴, 객체를 수정해도 호출부가 영향 받지 않는다.

구조 패턴 → 객체를 조합해서 더 큰 구조를 만드는 패턴

행위 패턴 → 객체 간의 알고리즘이나 책임 분배에 관한 패턴, 객체 하나로는 수행할 수 없는 작업을 여러 객체를 이용해 작업을 분배. 결합도 최소화를 고려할 필요가 있다.

 

생성 패턴

1. 싱글턴(Singleton)

하나의 인스턴스만 생성되도록 보장하는 패턴

  • 전역적으로 접근 가능
  • 스프링의 기본 Bean Scope

특징

  • 인스턴스 1개만 유지
  • new로 새로 만들 수 없음
public class Singleton {
    private static final Singleton instance = new Singleton();
    private Singleton() {}  // 외부에서 생성 금지

    public static Singleton getInstance() {
        return instance;
    }
}

 

2. 팩토리 메서드(Factory Method)

객체 생성 로직을 서브 클래스에 위임하여 코드 변경 없이 다른 객체 생성 가능

특징

  • 상위 클래스에서 객체 생성 메서드 정의
  • 하위 클래스
abstract class Dialog {
    public void render() {
        Button btn = createButton(); // 팩토리 메서드
        btn.onClick();
    }

    protected abstract Button createButton();  // 추상
}

class WindowsDialog extends Dialog {
    protected Button createButton() {
        return new WindowsButton();
    }
}

 

 

3. 추상 팩토리(Abstract Factory)

관련된 객체들을 통일된 인터페이스로 묶어 생성하는 패턴

  • 팩토리 묶음
  • 서로 호환되는 객체군 생성

특징

  • 관련 객체들을 한 번에 생성
  • 플랫폼 변경 쉽게 가능 (windows ↔ mac)
interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

class MacFactory implements GUIFactory {
    public Button createButton() { return new MacButton(); }
    public Checkbox createCheckbox() { return new MacCheckbox(); }
}

class Application {
    private Button button;
    private Checkbox checkbox;

    public Application(GUIFactory factory) {
        this.button = factory.createButton();
        this.checkbox = factory.createCheckbox();
    }
}

 

4. 빌더(Builder)

객체를 단계별로 생성하도록 분리

  • 복잡한 객체 생성 시 유용
  • 가독성과 재사용성 ↑

특징

  • 생성자 파라미터 많을 때 적합
  • 선택적 매개변수 가능
public class User {
    private String name;
    private int age;

    public static class Builder {
        private String name;
        private int age;

        public Builder name(String name) { this.name = name; return this; }
        public Builder age(int age) { this.age = age; return this; }
        public User build() { return new User(name, age); }
    }

    private User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

// 사용
User user = new User.Builder().name("Kim").age(20).build();

 

5. 프로토타입(Prototype)

기존 객체를 복제(clone)하여 새 객체 생성

특징

  • 객체 복사로 생성
  • DB 로딩 비용이 큰 경우 유용
public class Sheep implements Cloneable {
    private String name;

    public Sheep(String name) {
        this.name = name;
    }

    public Sheep clone() throws CloneNotSupportedException {
        return (Sheep) super.clone();
    }
}

// 사용
Sheep original = new Sheep("Dolly");
Sheep copy = original.clone();

 

구조 패턴

1. 어댑터(Adapter)

기존 인터페이스를 호환되지 않는 다른 인터페이스에 맞게 바꿔줌 → “끼워 넣기” 용도

사용 시기

  • 기존 코드 변경 없이 새 클래스를 끼워넣고 싶을 때
// 기존 코드 (클라이언트가 기대하는 인터페이스)
interface MediaPlayer {
    void play(String fileName);
}

// 새 클래스 (호환되지 않음)
class VLCPlayer {
    void playVLC(String fileName) {
        System.out.println("Playing VLC: " + fileName);
    }
}

// 어댑터
class MediaAdapter implements MediaPlayer {
    private VLCPlayer vlc = new VLCPlayer();

    public void play(String fileName) {
        vlc.playVLC(fileName);  // 호환시켜줌
    }
}

 

2. 브리지(Bridge)

기능 계층과 구현 계층을 분리해서 독립적으로 확장 가능

사용 시기

  • 플랫폼과 기능을 독립적으로 바꾸고 싶을 때
interface Device {
    void turnOn();
    void turnOff();
}

abstract class Remote {
    protected Device device;

    public Remote(Device device) {
        this.device = device;
    }

    abstract void power();
}

class TV implements Device {
    public void turnOn() { System.out.println("TV on"); }
    public void turnOff() { System.out.println("TV off"); }
}

class BasicRemote extends Remote {
    public BasicRemote(Device device) { super(device); }

    void power() { device.turnOn(); }
}

 

 

3. 컴포지트(Composite)

부분-전체 구조를 트리 형태로 표현

사용 시기

  • 폴더/파일처럼 계층 구조를 동일 인터페이스로 다루고 싶을 때
interface Component {
    void show();
}

class FileLeaf implements Component {
    private String name;
    public FileLeaf(String name) { this.name = name; }

    public void show() {
        System.out.println("File: " + name);
    }
}

class FolderComposite implements Component {
    private List<Component> children = new ArrayList<>();

    public void add(Component c) {
        children.add(c);
    }

    public void show() {
        for (Component c : children) c.show();
    }
}

 

 

4. 데코레이터(Decorater)

기능을 동적으로 추가할 수 있게 하는 패턴(상속 대신 조합)

사용 시기

  • 실행 중 기능을 유연하게 붙이고 싶을 때(Spring의 Filter도 유사)
interface Coffee {
    String getDescription();
    int cost();
}

class BasicCoffee implements Coffee {
    public String getDescription() { return "Basic"; }
    public int cost() { return 5; }
}

class MilkDecorator implements Coffee {
    private Coffee coffee;
    public MilkDecorator(Coffee c) { this.coffee = c; }

    public String getDescription() { return coffee.getDescription() + ", Milk"; }
    public int cost() { return coffee.cost() + 2; }
}

 

5. 퍼사드(Facade)

복잡한 서브 시스템에 대해 단순한 인터페이스 제공

사용 시기

  • 복잡한 시스템을 간단하게 감싸서 제공하고 싶을 때 (ex : JdbcTemplate)
class CPU {
    void freeze() {}
    void execute() {}
}

class Memory {
    void load() {}
}

class ComputerFacade {
    private CPU cpu = new CPU();
    private Memory memory = new Memory();

    public void start() {
        cpu.freeze();
        memory.load();
        cpu.execute();
    }
}

 

6. 플라이웨이트(Flyweight)

공유 객체를 사용해서 메모리 절약 자주 등장하는 작은 객체 캐싱

사용 시기

  • 반복되는 값(예 : 글자폰트, 배경 등)을 캐싱해 메모리 절약하고 싶을 때
class Circle {
    private String color;
    public Circle(String color) { this.color = color; }
}

class CircleFactory {
    private static Map<String, Circle> map = new HashMap<>();

    public static Circle getCircle(String color) {
        return map.computeIfAbsent(color, c -> new Circle(c));
    }
}

 

7. 프록시(Proxy)

실제 객체에 접근 전에 대리 객체를 사용해 제어 접근 제어, 로깅, 지연 로딩 등

사용 시기

  • 접근 제어, 리소스 절약, 로깅 등에서 사용됨(Spring AOP도 프록시 기반)
interface Service {
    void run();
}

class RealService implements Service {
    public void run() {
        System.out.println("Running actual logic");
    }
}

class ProxyService implements Service {
    private RealService real;

    public void run() {
        if (real == null) real = new RealService(); // 지연 로딩
        System.out.println("Proxy: Before");
        real.run();
        System.out.println("Proxy: After");
    }
}

 

행위 패턴

1. 책임 연쇄 패턴(Chain of Responsibility)

요청을 여러 객체에 순차적으로 전달하면서 처리할 수 있게 함.

사용 시기

  • 요청을 여러 객체 중 하나가 처리해야할 때
abstract class Handler {
    protected Handler next;
    public void setNext(Handler next) { this.next = next; }
    public abstract void handle(String request);
}

class AuthHandler extends Handler {
    public void handle(String request) {
        if (request.equals("auth")) System.out.println("Auth 처리됨");
        else if (next != null) next.handle(request);
    }
}

 

2. 커맨드 패턴(Command)

실행될 기능을 요청 객체로 캡슐화해서 요청자와 실행자를 분리

사용 시기

  • 요청을 큐에 저장하거나 실행 취소/다시 실행 가능하게 할 때
interface Command {
    void execute();
}

class LightOnCommand implements Command {
    public void execute() { System.out.println("Light On"); }
}

class RemoteControl {
    private Command command;
    public void setCommand(Command c) { this.command = c; }
    public void pressButton() { command.execute(); }
}

 

3. 인터프리터 패턴

언어 문법을 클래스 표현하고 해석기로 해석함

사용 시기

  • SQL, 정규식 등 언어 해석기 구현에 적합
interface Expression {
    boolean interpret(String context);
}

class TerminalExpression implements Expression {
    private String data;
    public TerminalExpression(String data) { this.data = data; }

    public boolean interpret(String context) {
        return context.contains(data);
    }
}

 

4. 이터레이터 패턴(iterator)

컬렉션 요소들을 순서대로 접근할 수 있게 해줌

사용 시기

  • 내부 구현을 노출하지 않고 컬렉션 순회하고 싶을 때
class MyList {
    private List<String> items = List.of("a", "b", "c");
    public Iterator<String> iterator() {
        return items.iterator();
    }
}

 

5. 상태 패턴(State)

객체의 상태에 따라 행동을 변경(if 대신 클래스 분리)

사용 시기

  • 상태에 따라 행동이 바뀌는 객체를 설계할 때 (ex : TCP 상태머신)
interface State {
    void handle();
}

class OnState implements State {
    public void handle() { System.out.println("전원 ON"); }
}

class Context {
    private State state;
    public void setState(State s) { state = s; }
    public void request() { state.handle(); }
}

 

6. 전략 패턴(Strategy)

알고리즘을 캡슐화해서 동적으로 교체 가능

사용 시기

  • 조건문 없이 동작을 교체 가능하게 하고 싶을 때 (ex : Spring의 AuthenticationProvider 전략 변경)
interface Strategy {
    int operate(int a, int b);
}

class AddStrategy implements Strategy {
    public int operate(int a, int b) { return a + b; }
}

class Calculator {
    private Strategy strategy;
    public Calculator(Strategy s) { this.strategy = s; }
    public int execute(int a, int b) { return strategy.operate(a, b); }
}

 

7. 템플릿 메서드(Template Method)

알고리즘의 뼈대를 정의하고 일부 로직은 서브클래스에서 구현

사용 시기

  • 공통 로직 + 변하는 부분을 분리하고 싶을 때 (ex : AbstractController)
abstract class Game {
    public void play() {
        start();
        playGame();
        end();
    }

    abstract void playGame();

    void start() { System.out.println("게임 시작"); }
    void end() { System.out.println("게임 종료"); }
}

 

8. 비지터 패턴(Visitor)

객체 구조를 변경하지 않고 새로운 연산을 추가

사용 시기

  • 객체 구조는 그대로 두고 다양한 기능을 추가하고 싶을 때 (ex : 컴팡일러에서 AST 방문)
interface Visitor {
    void visit(Book b);
}

class Book {
    void accept(Visitor v) { v.visit(this); }
}

class PriceCalculator implements Visitor {
    public void visit(Book b) {
        System.out.println("책 가격 계산");
    }
}

반응형