CS: SOLID 원칙과 디자인 패턴

hyeppy·2025년 10월 5일

CS

목록 보기
6/14
post-thumbnail

SOLID 원칙 - 객체지향 디자인 패턴의 기본기
영상 속 예시 코드


객체지향 프로그래밍은 배우고 활용하기 매우 까다로운 개념이다. 문법과 기능을 익히는 것도 어렵지만, 이를 효과적으로 사용하는 방법을 체득하기까지는 많은 시행착오가 필요하다.

정보처리기사를 준비하며 디자인 패턴을 공부할 때는 솔직히 잘 와닿지 않았다. 그런데 이번 1차 프로젝트를 진행하면서 당시에는 이해하지 못했던 구조들이 실제로 어떻게 적용되는지 많이 체감하게 되었다. 그래서 원래 SOLID 원칙만 정리하려 했지만, 디자인 패턴까지 간략하게 함께 정리해 두면 나중에 복습할 때 유용할 것 같아 한 게시물에 담았다.


SOLID 원칙

객체지향 프로그래밍으로 소프트웨어를 구현할 때, 보다 효율적이고 견고하며 유지보수가 용이한 프로그램을 만들기 위한 원칙이 있다. 바로 SOLID 원칙이다.

SOLID 원칙

  • S: Single Responsibility Principle (단일 책임 원칙)
  • O: Open/Closed Principle (개방-폐쇄 원칙)
  • L: Liskov Substitution Principle (리스코프 치환 원칙)
  • I: Interface Segregation Principle (인터페이스 분리 원칙)
  • D: Dependency Inversion Principle (의존성 역전 원칙)

하지만 이 SOLID 원칙을 지키는 것이 항상 능사는 아니다. 대부분의 경우에는 최적의 선택으로 작용하지만, 실무에서는 때때로 유연함을 발휘해야 할 때도 있다.


S: Single Responsibility Principle (단일 책임 원칙)

핵심: 각 클래스는 하나의 책임만 가져야 한다.

각 클래스가 한 가지 일을 위해 만들어져야 한다는 의미이며, 메서드가 하나만 있어야 한다는 것은 아니다. 하나의 책임을 위해 여러 메서드가 사용될 수 있다. 단, 클래스가 이들을 통해 수행하는 대표적인 업무는 하나여야 한다.

위반 시 발생하는 문제

  • 각 클래스의 코드가 복잡해지고, 역할을 직관적으로 이해하기 어려워짐
  • 각 클래스를 수정할 이유가 많아짐 (가진 책임의 개수만큼)
  • 한 책임을 수정하는 일이 다른 책임에 의도치 않은 영향을 끼칠 수 있음
  • 여러 책임이 한 곳에 얽혀 있기 때문에 테스트와 리팩토링이 어려워짐
  • 확장성과 유연성에 제약이 생김 (필요한 것만 분리해서 가져다 쓰는 것이 불가능)

준수 시 장점

  • 클래스의 이름만 보고도 어떤 책임을 가졌는지 쉽게 알 수 있음
  • 각 클래스는 맡은 책임에 변경사항이 있을 때만 수정하면 됨
  • 재사용성이 향상됨
// ⛔️ Noncompliant Example
public class UserService {
    public void saveUser(User user) {
        // 사용자 정보를 데이터베이스에 저장
        System.out.println("User saved to database: " + user.getName());
    }

    public void sendWelcomeEmail(User user) {
        // 환영 이메일 전송
        System.out.println("Welcome email sent to: " + user.getEmail());
    }

    public void logUserActivity(User user) {
        // 로그 기록
        System.out.println("Logging activity for user: " + user.getName());
    }
}

class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

// ✅ Compliant Example
public class UserRepository {
    public void saveUser(User user) {
        // 데이터베이스에 사용자 저장
        System.out.println("User saved to database: " + user.getName());
    }
}

public class EmailService {
    public void sendWelcomeEmail(User user) {
        // 사용자에게 환영 이메일 전송
        System.out.println("Welcome email sent to: " + user.getEmail());
    }
}

public class UserActivityLogger {
    public void logUserActivity(User user) {
        // 사용자 활동 로그 기록
        System.out.println("Logging activity for user: " + user.getName());
    }
}

class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

public class UserService {
    private UserRepository userRepository = new UserRepository();
    private EmailService emailService = new EmailService();
    private UserActivityLogger userActivityLogger = new UserActivityLogger();

    public void registerUser(User user) {
        userRepository.saveUser(user);
        emailService.sendWelcomeEmail(user);
        userActivityLogger.logUserActivity(user);
    }
}

코드 예시:

전자상거래 시스템에서 주문을 처리할 때, 주문 데이터 검증, 결제 처리, 이메일 발송, 재고 관리를 모두 한 클래스에서 처리하면 SRP 위반이다. 각각을 별도의 클래스로 분리해야 한다.

O: Open/Closed Principle (개방-폐쇄 원칙)

핵심: 각 클래스는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.

클래스를 수정하지 말고 확장해서 사용하라는 의미로, 새로운 기능이 필요할 때 기존 코드를 변경하는 것이 아니라, 새로운 코드를 추가하는 방식으로 확장해야 한다.

위반 시 발생하는 문제

  • 새로운 로직이 추가될 때마다 한 메서드의 코드가 장황해지고 복잡해짐
  • 기존 로직을 실수로 변경할 위험이 있음
  • 해당 메서드가 이미 사용되는 곳에서 부작용이 발생할 수 있음
  • if-else 또는 switch-case 문이 계속 추가되어 유지보수가 어려워짐

준수 시 장점

  • 인터페이스로 메서드의 이름과 형태만 정의하고, 각 구현 클래스에서 각자의 방식으로 로직을 구현함
  • 새로운 기능이 필요하면 해당 인터페이스를 구현하는 새로운 클래스만 추가하면 됨
  • 기존 코드를 수정하는 과정에서 발생할 수 있는 모든 문제에서 자유로움
// ⛔️ Noncompliant Example
public class ReportGenerator {
    public void generateReport(String type) {
        if (type.equals("PDF")) {
            System.out.println("Generating PDF report...");
        } else if (type.equals("HTML")) {
            System.out.println("Generating HTML report...");
        }
        // 새로운 형식을 추가하려면 이 메서드를 수정해야 한다
    }
}

// ✅ Compliant Example
public interface Report {
    void generate();
}

public class PDFReport implements Report {
    @Override
    public void generate() {
        System.out.println("Generating PDF report...");
    }
}

public class HTMLReport implements Report {
    @Override
    public void generate() {
        System.out.println("Generating HTML report...");
    }
}

public class XMLReport implements Report {
    @Override
    public void generate() {
        System.out.println("Generating XML report...");
    }
}

public class Main {
    public static void main(String[] args) {
        Report pdfReport = new PDFReport();
        pdfReport.generate();  // Generating PDF report...

        Report htmlReport = new HTMLReport();
        htmlReport.generate();  // Generating HTML report...

        Report xmlReport = new XMLReport();
        xmlReport.generate();  // Generating XML report...
    }
}

코드 예시:

리포트 생성 시스템에서 PDF, Excel, HTML 등 다양한 형식을 지원해야 할 때, 각 형식마다 if문을 추가하는 대신 인터페이스를 구현한 별도의 클래스를 만든다.

L: Liskov Substitution Principle (리스코프 치환 원칙)

핵심: 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다.

자식은 최소한 부모가 하는 일은 다 해야 한다는 의미로, 부모 클래스 객체가 들어갈 자리에 자식이 들어가더라도 부모가 하던 일은 지장이 없어야 한다.

위반 시 발생하는 문제

  • 자식 클래스에서 부모 클래스의 기능을 수행할 수 없으면 예외가 발생함
  • 부모 클래스에서 정상 동작하던 코드가 자식 클래스에서는 오작동함
  • 다형성의 장점을 활용할 수 없게 됨
  • 타입 체크 로직이 필요해져서 OCP 위반으로 이어짐

준수 시 장점

  • 예외적인 자식 클래스의 동작은 독립된 인터페이스로 분리함
  • 실제로 동작할 수 있는 메서드에만 인터페이스를 적용함
  • 다형성을 안전하게 활용할 수 있음

주의사항:

단순히 메서드 오버라이딩만으로 판단할 수 있는 문제가 아니다. 행위적 하위타입(behavioral subtyping)을 고려해야 한다. 즉, 자식 클래스가 부모 클래스의 의도된 행위를 위반하지 않아야 한다.

// ⛔️ Noncompliant Example - 새와 펭귄
public class Bird {
    public void fly() {
        System.out.println("Bird is flying");
    }
}

public class Penguin extends Bird {
    @Override
    public void fly() {
        // 펭귄은 날 수 없다
        throw new UnsupportedOperationException("Penguins cannot fly");
    }
}

public class Main {
    public static void main(String[] args) {
        Bird bird = new Bird();
        bird.fly(); // Bird is flying

        Bird penguin = new Penguin();
        penguin.fly(); // UnsupportedOperationException 발생
    }
}

// ✅ Compliant Example - 새와 펭귄
public interface Flyable {
    void fly();
}

public class Bird {
    public void eat() {
        System.out.println("Bird is eating");
    }
}

public class Sparrow extends Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("Sparrow is flying");
    }
}

public class Penguin extends Bird {
    // 펭귄은 Flyable을 구현하지 않는다
}

public class Main {
    public static void main(String[] args) {
        Bird sparrow = new Sparrow();
        sparrow.eat(); // Bird is eating
        ((Flyable) sparrow).fly(); // Sparrow is flying

        Bird penguin = new Penguin();
        penguin.eat(); // Bird is eating
        // ((Flyable) penguin).fly(); // 컴파일 에러, Penguin은 Flyable이 아니다
    }
}

// ⛔️ Noncompliant Example - 직사각형과 정사각형
class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width;
    }

    @Override
    public void setHeight(int height) {
        this.width = height;
        this.height = height;
    }
}

class AreaCalculator {
    public void calculateArea(Rectangle rectangle) {
        rectangle.setWidth(5);
        rectangle.setHeight(4);
        System.out.println("Area: " + rectangle.getArea());
        // Rectangle 예상 출력: Area: 20
        // Square 실제 출력: Area: 16 (LSP 위반)
    }
}

// ✅ Compliant Example - 직사각형과 정사각형
interface Shape {
    int getArea();
}

class Rectangle implements Shape {
    private int width;
    private int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public int getArea() {
        return width * height;
    }
}

class Square implements Shape {
    private int side;

    public Square(int side) {
        this.side = side;
    }

    @Override
    public int getArea() {
        return side * side;
    }
}

class AreaCalculator {
    public void calculateArea(Shape shape) {
        System.out.println("Area: " + shape.getArea());
    }
}

public class Main {
    public static void main(String[] args) {
        AreaCalculator calculator = new AreaCalculator();

        Shape rectangle = new Rectangle(5, 4);
        calculator.calculateArea(rectangle); // Output: Area: 20

        Shape square = new Square(5);
        calculator.calculateArea(square); // Output: Area: 25 (예상대로 동작)
    }
}

코드 예시:

  1. 새(Bird) 클래스에 날기(fly) 메서드가 있을 때, 펭귄(Penguin)은 날 수 없으므로 Bird를 상속받으면 LSP 위반이다.
  2. 직사각형(Rectangle)과 정사각형(Square)의 관계 - 정사각형이 직사각형을 상속받으면, 너비와 높이를 독립적으로 설정하는 직사각형의 행위를 위반한다.

I: Interface Segregation Principle 인터페이스 분리 원칙

핵심: 클래스는 자신이 사용하지 않을 메서드를 구현하도록 강요받지 않아야 한다.

인터페이스는 자격증과 같은 것이다. 자동차 운전 면허를 따기 위해 굴착기까지 운전할 줄 알아야 한다면, 자동차 운전을 하고 싶은 사람들은 강제로 굴착기 운전까지 배워야 한다. 이는 부적합하다.

위반 시 발생하는 문제

  • 사용하지 않는 메서드를 구현해야 하는 잉여 메서드가 발생함
  • 인터페이스 변경 시 관련 없는 클래스들도 영향을 받음
  • 불필요한 의존성이 생김
  • 코드의 가독성과 유지보수성이 떨어짐

준수 시 장점

  • 클라이언트는 자신이 필요한 인터페이스만 의존함
  • 인터페이스가 명확하고 특정한 역할을 수행함
  • 시스템의 결합도가 낮아짐
// ⛔️ Noncompliant Example
public interface Worker {
    void work();
    void eat();
}

public class Employee implements Worker {
    @Override
    public void work() {
        System.out.println("Employee is working");
    }

    @Override
    public void eat() {
        System.out.println("Employee is eating");
    }
}

public class Robot implements Worker {
    @Override
    public void work() {
        System.out.println("Robot is working");
    }

    @Override
    public void eat() {
        // 로봇은 식사를 하지 않는다
        throw new UnsupportedOperationException("Robots do not eat");
    }
}

public class Main {
    public static void main(String[] args) {
        Worker employee = new Employee();
        employee.work(); // Employee is working
        employee.eat(); // Employee is eating

        Worker robot = new Robot();
        robot.work(); // Robot is working
        robot.eat(); // UnsupportedOperationException 발생
    }
}

// ✅ Compliant Example
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public class Employee implements Workable, Eatable {
    @Override
    public void work() {
        System.out.println("Employee is working");
    }

    @Override
    public void eat() {
        System.out.println("Employee is eating");
    }
}

public class Robot implements Workable {
    @Override
    public void work() {
        System.out.println("Robot is working");
    }
    // Robot은 Eatable 인터페이스를 구현하지 않는다
}

public class Main {
    public static void main(String[] args) {
        Workable employee = new Employee();
        employee.work(); // Employee is working
        ((Eatable) employee).eat(); // Employee is eating

        Workable robot = new Robot();
        robot.work(); // Robot is working
        // ((Eatable) robot).eat(); // 컴파일 에러, Robot은 Eatable이 아니다
    }
}

코드 예시:

직원(Employee)과 로봇(Robot)이 있을 때, 둘 다 일(work)을 하지만 로봇은 식사(eat)를 하지 않는다. Worker 인터페이스에 eat을 포함시키면 Robot은 사용하지 않는 메서드를 구현해야 한다.

D: Dependency Inversion Principle (의존성 역전 원칙)

핵심: 고수준 모듈이 저수준 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다.

구체적인 동작을 직접 구현하는 로직은 저수준 모듈이고, 이를 제어하는 추상화된 로직을 제공하는 것은 고수준 모듈이다. 고수준 모듈과 저수준 모듈 모두 추상화(인터페이스)에 의존해야 한다.

위반 시 발생하는 문제

  • 저수준 모듈의 변경이 고수준 모듈에 영향을 미침
  • 메서드 이름이나 매개변수가 변경되면 고수준 모듈도 수정해야 함
  • 메서드의 재사용성이 낮아짐
  • 테스트가 어려워짐 (구체 클래스에 의존)
  • 코드의 유연성과 확장성이 떨어짐

준수 시 장점

  • 인터페이스를 통해 고수준 모듈과 저수준 모듈이 소통함
  • 저수준 모듈의 구현이 변경되어도 고수준 모듈은 영향을 받지 않음
  • 다양한 구현체를 교체하여 사용할 수 있음
  • 테스트 시 Mock 객체를 주입할 수 있음
// ⛔️ Noncompliant Example
public class Fan {
    public void spin() {
        System.out.println("Fan is spinning");
    }

    public void stop() {
        System.out.println("Fan is stopping");
    }
}

public class Switch {
    private Fan fan;

    public Switch(Fan fan) {
        this.fan = fan;
    }

    public void turnOn() {
        fan.spin();
    }

    public void turnOff() {
        fan.stop();
    }
}

// ✅ Compliant Example
public interface Switchable {
    void turnOn();
    void turnOff();
}

public class Fan implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("Fan is spinning");
    }

    @Override
    public void turnOff() {
        System.out.println("Fan is stopping");
    }
}

public class Light implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("Light is on");
    }

    @Override
    public void turnOff() {
        System.out.println("Light is off");
    }
}

public class Switch {
    private Switchable device;

    public Switch(Switchable device) {
        this.device = device;
    }

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

    public void turnOff() {
        device.turnOff();
    }
}

public class Main {
    public static void main(String[] args) {
        // 선풍기에 연결
        Switch fanSwitch = new Switch(new Fan());
        fanSwitch.turnOn();  // Fan is spinning
        fanSwitch.turnOff(); // Fan is stopping

        // 전등에 연결
        Switch lightSwitch = new Switch(new Light());
        lightSwitch.turnOn();  // Light is on
        lightSwitch.turnOff(); // Light is off
    }
}

실무 예시:

스위치(Switch)가 특정 선풍기(Fan)에만 동작하도록 구현하면, 나중에 전등(Light)을 켜고 싶을 때 스위치를 수정해야 한다. 대신 스위치가 Switchable 인터페이스에 의존하면, 어떤 기기든 연결할 수 있다.


디자인 패턴

디자인 패턴은 소프트웨어 설계에서 자주 발생하는 문제들에 대한 재사용 가능한 해결책이다. GoF(Gang of Four)가 정리한 23가지 디자인 패턴은 목적에 따라 생성(Creational), 구조(Structural), 행위(Behavioral) 3가지로 분류된다.

생성 패턴 (Creational Patterns)

객체의 인스턴스 생성에 관여하고, 클래스 정의와 객체 생성 방식을 구조화하고 캡슐화하는 패턴이다. 객체 생성의 복잡성을 숨기고, 시스템이 어떤 구체 클래스를 사용하든지 독립적으로 만들어 준다.

Abstract Factory (추상 팩토리)

  • 구체적인 클래스에 의존하지 않고 서로 연관되거나 의존적인 객체들의 조합을 만드는 인터페이스를 제공함
  • 동일한 주제의 다른 팩토리를 묶어 관리함
  • 예시: UI 라이브러리에서 Windows용 버튼/텍스트박스와 Mac용 버튼/텍스트박스를 생성하는 팩토리

Builder (빌더)

  • 복잡한 인스턴스를 조립해서 만드는 구조임
  • 복합 객체를 생성할 때 객체를 생성하는 방법과 객체를 구현하는 방법을 분리함으로써 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있음
  • 생성과 표현을 분리해서 복잡한 객체를 생성함
  • 예시: StringBuilder, Lombok의 @Builder

Factory Method (팩토리 메서드)

  • 상위 클래스에서 객체를 생성하는 인터페이스를 정의하고, 하위 클래스에서 인스턴스를 생성하도록 하는 방식임
  • 생성할 객체의 클래스를 국한하지 않고 객체를 생성함
  • 객체 생성 처리를 서브 클래스로 분리해 처리하도록 캡슐화하는 패턴임
  • 예시: Spring의 BeanFactory, JDBC의 DriverManager

Prototype (프로토타입)

  • 처음부터 일반적인 원형을 만들어 놓고 그것을 복사한 후 필요한 부분만 수정해서 사용하는 패턴임
  • 생성할 객체의 원형을 제공하는 인스턴스에서 생성할 객체들의 타입이 결정되도록 설정함
  • 기존 객체를 복제함으로써 객체를 생성함
  • 예시: Java의 clone() 메서드, Object.clone()

Singleton (싱글톤)

  • 전역 변수를 사용하지 않고 객체를 하나만 생성하도록 하며, 생성된 객체를 어디서든지 참조할 수 있도록 함
  • 한 클래스에 한 객체만 존재하도록 제한함
  • 예시: Spring의 기본 빈 스코프, 로거 인스턴스, 데이터베이스 연결 풀

구조 패턴 (Structural Patterns)

클래스나 객체를 조합해 더 큰 구조를 만드는 패턴이다. 서로 다른 인터페이스를 가진 객체들을 함께 동작시키거나, 복잡한 구조를 단순화한다.

Adapter (어댑터)

  • 기존에 생성된 클래스를 재사용할 수 있도록 중간에서 맞춰주는 역할을 하는 인터페이스를 만드는 패턴임
  • 인터페이스가 호환되지 않는 클래스들을 함께 이용할 수 있도록, 타 클래스의 인터페이스를 기존 인터페이스에 덧씌움
  • 예시: InputStreamReader (InputStream을 Reader로 변환)

Bridge (브릿지)

  • 기능의 클래스 계층과 구현의 클래스 계층을 연결하고, 구현부에서 추상 계층을 분리하여 추상화된 부분과 실제 구현 부분을 독립적으로 확장할 수 있는 패턴임
  • 추상화와 구현을 분리해 둘을 각각 따로 발전시킬 수 있음
  • 예시: JDBC API (추상화)와 각 데이터베이스 드라이버(구현)

Composite (컴포지트)

  • 객체들의 관계를 트리 구조로 구성하여 부분-전체 계층을 표현하는 패턴임
  • 여러 개의 객체들로 구성된 복합 객체와 단일 객체를 클라이언트에서 구별 없이 다루게 해주는 패턴임
  • 예시: 파일 시스템의 디렉토리와 파일, GUI의 컨테이너와 컴포넌트

Decorator (데코레이터)

  • 기존에 구현되어 있는 클래스에 필요한 기능을 추가해 나가는 설계 패턴임
  • 기능 확장이 필요할 때 객체 간의 결합을 통해 기능을 동적으로 유연하게 확장할 수 있게 해주어 상속의 대안으로 사용됨
  • 기존 객체의 메서드에 새로운 행동을 추가하거나 오버라이드할 수 있음
  • 예시: Java I/O 스트림 (BufferedInputStream, DataInputStream 등)

Facade (파사드)

  • 복잡한 시스템에 대해 단순한 인터페이스를 제공함으로써 사용자와 시스템 간 또는 다른 시스템과의 결합도를 낮추어 시스템 구조에 대한 파악을 쉽게 함
  • 많은 분량의 코드에 접근할 수 있는 단순한 인터페이스를 제공함
  • 통합된 인터페이스를 제공함
  • 예시: 홈 시어터 시스템 (여러 기기를 하나의 리모컨으로 제어)

Flyweight (플라이웨이트)

  • 다수의 객체로 생성될 경우 모두가 갖는 본질적인 요소를 클래스화하여 공유함으로써 메모리를 절약하고 클래스의 경량화를 목적으로 하는 패턴임
  • 여러 개의 가상 인스턴스를 제공해서 메모리를 절감함
  • 예시: String Pool, 게임의 총알 객체 재사용

Proxy (프록시)

  • 실제 객체에 대한 대리 객체로, 실제 객체에 대한 접근 이전에 필요한 행동을 취할 수 있게 만듦
  • 실제 객체가 드러나지 않게 하여 정보 은닉의 역할을 수행함
  • 접근 조절, 비용 절감, 복잡도 감소를 위해 접근이 힘든 객체에 대한 대역을 제공함
  • 예시: Spring AOP, 지연 로딩(Lazy Loading), 원격 프록시

행위 패턴 (Behavioral Patterns)

객체나 클래스 사이의 알고리즘이나 책임 분배에 관련된 패턴이다. 한 객체가 혼자 수행할 수 없는 작업을 여러 개의 객체로 어떻게 분배하는지, 객체 사이의 결합도를 최소화하는 것에 중점을 둔다.

Chain of Responsibility (책임 연쇄)

  • 정적으로 어떤 기능에 대한 처리의 연결이 하드코딩되어 있을 때 기능 처리의 연결 변경이 불가능한데, 이를 동적으로 연결한 경우에는 다르게 처리할 수 있도록 하는 패턴임
  • 한 요청을 2개 이상의 객체에서 처리함
  • 예시: 서블릿 필터 체인, 예외 처리 핸들러

Command (커맨드)

  • 하나의 추상 클래스에 메서드를 만들어 각 명령이 들어오면 그에 맞는 서브 클래스가 선택되어 실행됨
  • 실행될 기능을 캡슐화함으로써 주어진 여러 기능을 실행할 수 있는 재사용성이 높은 클래스를 설계하는 패턴임
  • 요구사항을 객체로 캡슐화함
  • 예시: 텍스트 에디터의 Undo/Redo 기능, 메뉴 아이템

Interpreter (인터프리터)

  • 언어의 다양한 해석, 구체적으로 구문을 나누고 그 분리된 구문의 해석을 맡는 클래스를 각각 작성해 여러 형태의 언어 구문을 해석할 수 있게 만드는 패턴임
  • 문법 자체를 캡슐화해서 사용함
  • 예시: 정규표현식, SQL 파서, 계산기

Iterator (반복자)

  • 컬렉션 구현 방법을 노출시키지 않으면서 그 집합체 안에 들어있는 모든 항목에 접근할 방법을 제공하는 패턴임
  • 내부 구조를 노출하지 않고 복합 객체의 원소를 순차적으로 접근 가능하게 하는 패턴임
  • 예시: Java의 Iterator, for-each 문

Mediator (중재자)

  • 객체지향 설계에서 객체의 수가 너무 많아져 통신이 복잡해지면 느슨한 결합을 해칠 수 있기 때문에 중간에서 이를 통제하고 지시할 수 있는 역할을 하는 패턴임
  • 중재자에게 요구하여 통신의 빈도를 줄임
  • 상호작용의 유연한 변경을 지원함
  • 예시: 채팅방, 항공 관제 시스템, MVC의 Controller

Memento (메멘토)

  • 클래스 설계 관점에서 객체의 정보를 저장할 필요가 있을 때 적용하는 패턴임
  • Undo 기능을 개발할 때 사용함
  • 객체를 이전 상태로 복구시켜야 하는 경우 작업 취소 요청 기능을 제공함
  • 예시: 텍스트 에디터의 실행 취소, 게임의 세이브/로드

Observer (옵저버)

  • 어떤 클래스에 변화가 일어났을 때, 이를 감지하여 다른 클래스에 통보해주는 패턴임
  • 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들에게 연락이 가고 자동으로 내용이 갱신됨
  • 일대다의 의존성을 가지고 상호작용하는 객체 사이에서는 가능한 느슨하게 결합하는 패턴임
  • 예시: 이벤트 리스너, 옵저버블 패턴, 리액티브 프로그래밍

State (상태)

  • 객체 상태를 캡슐화해서 클래스화함으로써 그것을 참조하게 하는 방식임
  • 상태에 따라 다르게 처리할 수 있도록 행위 내용을 변경하고, 변경 시 원시 코드의 수정을 최소화할 수 있음
  • 객체의 상태에 따라 행위 내용을 변경함
  • 예시: TCP 연결 상태, 자판기의 상태 관리

Strategy (전략)

  • 알고리즘 군을 정의하고(추상클래스) 같은 알고리즘을 각각 하나의 클래스로 캡슐화한 후, 필요할 때 서로 교환해서 사용할 수 있게 하는 패턴임
  • 행위를 클래스로 캡슐화해 동적으로 행위를 자유롭게 바꿀 수 있게 해주는 패턴임
  • 예시: 정렬 알고리즘 선택, 결제 수단 선택

Template Method (템플릿 메서드)

  • 상위 클래스에서는 추상적으로 표현하고, 그 구체적인 내용은 하위 클래스에서 결정되는 패턴임
  • 어떤 작업을 처리하는 일부분을 서브 클래스로 캡슐화해 전체 일을 수행하는 구조는 바꾸지 않으면서 특정 단계에서 수행하는 내역을 바꾸는 패턴임
  • 예시: Spring의 JdbcTemplate, AbstractList의 구현

Visitor (방문자)

  • 각 클래스의 데이터 구조로부터 처리 기능을 분리하여 별도의 클래스를 만들어 놓고 해당 클래스의 메서드가 각 클래스를 돌아다니며 특정 작업을 수행하도록 만드는 패턴임
  • 객체의 구조는 변경하지 않으면서 기능만 따로 추가하거나 확장할 때 사용하는 패턴임
  • 특정 구조를 이루는 복합 객체의 원소 특성에 따라 동작을 수행할 수 있도록 지원함
  • 예시: 컴파일러의 AST 순회, 파일 시스템 탐색

profile
Backend

0개의 댓글