디자인패턴

[디자인패턴] SOLID

개바리여리 2025. 9. 29. 22:16

 

SOLID 원칙은 객체 지향 프로그래밍(OOP)에서 유지보수성과 확장성을 높이기 위해 제안된 5가지 설계 원칙이다.

말 그래도 원칙일뿐이다. 원칙보다는 때에 맞는 유연한 설계와 코드 작성이 중요하다.

 

1. S : Single Responsibility Principle (SRP, 단일 책임 원칙)

하나의 클래스는 하나의 책임만을 가진다.

ex : Report 클래스가 보고서 출력과 저장까지 둘 다 한다면 → 출력과 저장을 분리하는 것이 SRP에 맞음.

하나의 UserService 클래스에서 유저를 저장하고 이메일발송하고 이런 코드들...

class ReportManager {
    public String createReport() {
        // 보고서 생성 로직
        return "Report Content";
    }

    public void saveReport(String report) {
        // 보고서를 파일로 저장하는 로직
        System.out.println("Saving report to file: " + report);
    }
}

//아래와 같이 service와 Repository로 나누어서 책임 분리 가능
class ReportService {
    public String createReport() {
        // 보고서 생성 로직
        return "Report Content";
    }
}

// 보고서 저장 책임
class ReportRepository {
    public void saveReport(String report) {
        // 보고서를 파일에 저장하는 로직
        System.out.println("Saving report to file: " + report);
    }
}

 

클래스에 여기저기 책임이 묶여있어서 필요한것만 가져다 쓰기도 힘들고 리팩터링 및 유지보수가 어렵다. (분류가 안됨)

 

 

2. Open-Closed Principle (OCP, 개방-폐쇄의 원칙)

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

public class ReportGenerator {

    public void generateReport(String type) {
        if(type.equals("PDF")) {
            System.out.println("PDF 생성");
        } else if(type.equals("HTML")) {
            System.out.println("HTML 파일 생성");
        }
    }
}

 

위와 같이 작성된다면 다른 케이스가 추가될때마다 해당 메서드는 수정되어야 할것이다. ---> 위험하다. 어떤 영향을 끼칠지 모르고 요구사항이 어떻게 변화할지도 모른다. 나중에는 메서드를 갈아 엎어야 할 수 있다.

public interface Report {
    void generate();
}

public class PDFReport implements Report {

    @Override
    public void generate() {
        System.out.println("pdf 생성");
    }
}

public class HTMLReport implements Report {

    @Override
    public void generate() {
        System.out.println("HTML 생성");
    }
}

 

이렇게 인터페이스를 통해 형태와 이름정도만을 정의해두고 각 인터페이스를 구현하는 구현체를 두어서 기존의 코드를 수정하지 않으며 추가하는 방식으로 요구사항에 대응 할 수 있게 된다.

 

3. Liskov Substitution Principle (LSP, 리스코프-치환 원칙)

자식 클래스는 언제나 부모클래스를 대체할 수 있어야 한다. (자식은 최대한 부모가 하는일은 수행해야 한다.)

class Bird {
    public void fly() {
        System.out.println("Flap flap, flying!");
    }
}

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

class Penguin extends Bird {
    @Override
    public void fly() {
        // 펭귄은 날 수 없음 → 부모의 계약을 위반
        throw new UnsupportedOperationException("Penguins can't fly!");
    }
}

public class Main {
    public static void main(String[] args) {
        Bird bird1 = new Sparrow();
        Bird bird2 = new Penguin();

        bird1.fly();  // 정상 동작
        bird2.fly();  //예외 발생
    }
}

 

펭귄은 날 수 없어서 부모클래스를 대체 할 수가 없다. 클라이언트는 Bird가 날수 있을거라 기대했지만 그렇지 못하게된다.

interface Bird {
    void eat();
}

interface Flyable {
    void fly();
}

// 참새는 날 수 있음
class Sparrow implements Bird, Flyable {
    @Override
    public void eat() {
        System.out.println("Sparrow is eating");
    }

    @Override
    public void fly() {
        System.out.println("Sparrow is flying!");
    }
}

// 펭귄은 날 수 없음
class Penguin implements Bird {
    @Override
    public void eat() {
        System.out.println("Penguin is eating");
    }

    // fly() 강제 구현 필요 없음 → 계약 위반 X
}

 

 

4. Interface Segregation Principle (인터페이스 분리 법칙, ISP)

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

즉, 덩치 큰 인터페이스보다는 작은 인터페이스 여러 개로 나누는 게 좋습니다.

// 너무 큰 인터페이스
interface Machine {
    void print(String content);
    void scan(String content);
    void fax(String content);
}

// 다기능 복합기 - 문제 없음
class MultiFunctionPrinter implements Machine {
    @Override
    public void print(String content) {
        System.out.println("Printing: " + content);
    }

    @Override
    public void scan(String content) {
        System.out.println("Scanning: " + content);
    }

    @Override
    public void fax(String content) {
        System.out.println("Faxing: " + content);
    }
}

// 단순 프린터 - 사용하지 않는 메서드도 억지로 구현해야 함
class SimplePrinter implements Machine {
    @Override
    public void print(String content) {
        System.out.println("Printing: " + content);
    }

    @Override
    public void scan(String content) {
        throw new UnsupportedOperationException("This printer can't scan");
    }

    @Override
    public void fax(String content) {
        throw new UnsupportedOperationException("This printer can't fax");
    }
}

위와 같은 코드를 아래와 같이 분리할 수 있다.

 

interface Printer {
    void print(String content);
}

interface Scanner {
    void scan(String content);
}

interface Fax {
    void fax(String content);
}

// 다기능 복합기
class MultiFunctionPrinter implements Printer, Scanner, Fax {
    @Override
    public void print(String content) {
        System.out.println("Printing: " + content);
    }

    @Override
    public void scan(String content) {
        System.out.println("Scanning: " + content);
    }

    @Override
    public void fax(String content) {
        System.out.println("Faxing: " + content);
    }
}

// 단순 프린터
class SimplePrinter implements Printer {
    @Override
    public void print(String content) {
        System.out.println("Printing: " + content);
    }
}

 

5. Dependency Inversion Priciple (의존성 역전 법칙, DIP)

고수준 모듈이 저수준 모듈에 의존해서는 안된다.

//구체적 동작을 정의하는 저수준 모듈
public class Fan {
    public void spin(){
        System.out.println("선풍기 돌아감");
    }
    public void stop(){
        System.out.println("선풍기 멈춤");
    }
}
// 선풍기를 제어하는 추상화된 로직을 제공하는 고수준 모듈
public class Switch {
    private Fan fan;

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

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

 

선풍기에 의존적인 코드가 작성되었다.

만약 저수준모듈의 클래스가 변경된다면 고수준 모듈의 클래스에도 수정이 필요하게 된다.

또한 선풍기말고 다른 전자기기를 다룰 수 없게 된다.

public interface Switchable {  // 추상화 (인터페이스)
    void turnOn();
    void turnOff();
}

public class Fan implements Switchable { // 저수준 모듈 (구현체)
    @Override
    public void turnOn() { System.out.println("선풍기돌아감"); }
    @Override
    public void turnOff() { System.out.println("선풍기 멈춤"); }
}

public class Switch { // 고수준 모듈
    private Switchable device;  // 추상화에 의존

    public Switch(Switchable device) {
        this.device = device;
    }
    public void switchOn() { device.turnOn(); }
    public void switchOff() { device.turnOff(); }
}

 

이제는 저수준 모듈과 고수준 모듈 모두 추상화에 의존하는 구조로 변경되었음.