본문 바로가기
소프트웨어공학/SW 설계 원칙

[SW 설계 원칙] SOLID 원칙과 그 진정한 의미

by 클레어몬트 2025. 3. 17.

ㅁSOLID 원칙: 객체지향 설계의 핵심 원칙

내 추억이 담긴 카트라이더의 '솔리드 SR'..

 

소프트웨어 개발에서 유지보수성확장성을 높이기 위해서는 올바른 설계 원칙을 따르는 것이 중요하다. SOLID 원칙은 이러한 객체지향 설계를 효과적으로 수행할 수 있도록 돕는 다섯 가지 핵심 원칙을 의미한다. 이 원칙들은 로버트 C. 마틴(Robert C. Martin)에 의해 정립되었으며, 특히 객체지향 프로그래밍(OOP)에서 널리 사용된다.

 

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

"클래스는 단 하나의 책임만 가져야 한다."

단일 책임 원칙은 하나의 클래스가 하나의 역할만 수행하도록 설계해야 한다는 원칙이다. 즉, 하나의 클래스는 변경이 필요한 이유가 단 하나뿐이어야 한다. 이를 통해 코드의 응집도를 높이고, 변경에 대한 영향을 최소화할 수 있다.

class ReportPrinter {
    public void printReport(String report) {
        System.out.println(report);
    }
}

 

위 예제에서 ReportPrinter 클래스는 보고서를 출력하는 역할만 담당한다. 만약 파일 저장 기능을 추가해야 한다면 별도의 클래스를 생성하는 것이 바람직하다!


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

"소프트웨어 구성 요소는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다."

즉, 기존의 코드를 수정하지 않으면서도 새로운 기능을 쉽게 추가할 수 있도록 설계해야 한다.

 

(잘못된 예시)

class DiscountService {
    public double calculateDiscount(String type, double price) {
        if (type.equals("fixed")) {
            return price - 1000;
        } else if (type.equals("percentage")) {
            return price * 0.9;
        }
        return price;
    }
}

 

문제점

  1. OCP(Open-Closed Principle) 위반
    • calculateDiscount 메서드는 새로운 할인 방식이 추가될 때마다 if-else 문을 수정해야 한다.
    • 이는 기존 코드를 변경해야 하므로 확장에는 닫혀있고, 수정에는 열려있는 구조이다.
  2. 하드코딩된 조건문 사용
    • 문자열("fixed", "percentage")을 비교하는 방식은 가독성이 떨어지고, 오타 발생 시 오류를 찾기 어렵다.
  3. 유지보수 어려움
    • 새로운 할인 정책을 추가하려면 calculateDiscount 메서드를 계속 수정해야 한다.
    • 이는 코드 수정 시 기존 로직에 영향을 줄 수 있으며, 테스트도 어려워진다.

 

(개선된 예시)

DiscountPolicy 인터페이스를 만들고 applyDiscount 추상메서드를 정의한다

interface DiscountPolicy {
    double applyDiscount(double price);
}

class FixedDiscount implements DiscountPolicy {
    public double applyDiscount(double price) {
        return price - 1000;
    }
}

class PercentageDiscount implements DiscountPolicy {
    public double applyDiscount(double price) {
        return price * 0.9;
    }
}

class DiscountService {
    public double calculateDiscount(DiscountPolicy policy, double price) {
        return policy.applyDiscount(price);
    }
}

개선된 설계의 장점

OCP(Open-Closed Principle) 준수

  • 새로운 할인 정책이 추가되더라도 기존 코드를 수정할 필요 없이 새로운 DiscountPolicy 구현체를 만들면 된다.

유지보수성 향상

  • DiscountService는 할인 방식에 대한 세부 사항을 알 필요 없이 DiscountPolicy를 통해 동작한다.
  • 따라서 코드 수정 없이 새로운 할인 정책을 쉽게 추가할 수 있다.

의존성 주입(DI) 적용

  • DiscountService가 특정 할인 방식에 의존하지 않고 DiscountPolicy 인터페이스를 활용하므로, 코드의 유연성이 증가한다.
  • 테스트 시에도 다양한 할인 정책을 쉽게 모킹(mocking)하여 단위 테스트를 수행할 수 있다.

가독성과 코드 품질 향상

  • 조건문이 사라지고, 각 할인 정책이 독립적인 클래스로 분리되어 역할이 명확해진다.

 

 

https://claremont.tistory.com/entry/OOP-OCP%EA%B0%9C%EB%B0%A9-%ED%8F%90%EC%87%84-%EC%9B%90%EC%B9%99%EA%B3%BC-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4

 

[OOP] OCP(개방 폐쇄 원칙)과 디자인 패턴

ㅇ개방 폐쇄 원칙(OCP, Open-Closed Principle): 아주 좋은 객체 지향 설계 원칙 중 하나이다 - 확장에는 열려 있어야 한다(Open for extension)소프트웨어 엔티티(클래스, 모듈, 함수 등)는 새로운 기능을 추

claremont.tistory.com


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

"하위 클래스는 상위 클래스의 기능을 깨뜨리지 않으면서 확장할 수 있어야 한다."

즉, 부모 클래스의 객체가 사용되는 모든 곳에서 자식 클래스의 객체로 대체해도 프로그램이 정상적으로 동작해야 한다.

 

(잘못된 예시)

class Rectangle {
    protected int width, 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;
    }
}

 

위 코드에서는 SquareRectangle을 상속하지만, 정사각형의 특성상 너비와 높이가 항상 같아야 하므로 setWidth()에서 높이까지 변경해 버린다. 이는 LSP를 위반하는 경우로, 상위 클래스의 동작을 예상대로 따르지 않는다.

 

따라서 개선을 한다면, Shape이라는 인터페이스를 정의해서 리팩토링하면 된다!


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

"인터페이스는 클라이언트가 사용하지 않는 메서드에 의존하지 않도록 설계해야 한다."

즉, 하나의 거대한 인터페이스보다는 여러 개의 작은 인터페이스로 나누어 필요한 기능만 구현하도록 설계하는 것이 좋다.

 

(잘못된 예시)

interface Worker {
    void work();
    void eat();
}

class Robot implements Worker {
    public void work() {
        System.out.println("로봇이 작업을 수행합니다.");
    }
    public void eat() {
        throw new UnsupportedOperationException("로봇은 먹을 수 없습니다.");
    }
}

 

위 코드에서 Roboteat() 메서드를 필요로 하지 않음에도 불구하고 구현해야 한다. 이를 해결하려면 Worker 인터페이스를 분리해야 한다!

 

(개선된 코드)

interface Workable {
    void work();
}
interface Eatable {
    void eat();
}

class Robot implements Workable {
    public void work() {
        System.out.println("로봇이 작업을 수행합니다.");
    }
}

 

이제 RobotEatable 인터페이스를 구현할 필요가 없으며, 불필요한 의존성을 제거할 수 있다.


5. DIP(Dependency Inversion Principle, 의존성 역전 원칙)

"상위 모듈이 하위 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다."

즉, 클래스가 구체적인 구현이 아닌 인터페이스나 추상 클래스에 의존해야 한다.

 

(잘못된 예시)

// 저수준 모듈 (세부 구현)
class Keyboard {
    public void type() {
        System.out.println("키보드를 사용하여 입력합니다.");
    }
}

class Monitor {
    public void display() {
        System.out.println("모니터에 화면을 출력합니다.");
    }
}

// 고수준 모듈 (비즈니스 로직)
class Computer {
    private Keyboard keyboard;
    private Monitor monitor;
    
    public Computer() {
        this.keyboard = new Keyboard();
        this.monitor= new Monitor();
    }
    
    public void operate() {
        keyboard.type();
        monitor.display();
    }
}

public class Main {
    public static void main(String[] args) {
        Computer computer = new Computer();
        computer.operate();
    }
}

 

- 강한 결합(Tight Coupling)

Computer 클래스는 Keyboard와 Monitor의 구체적인 구현에 직접 의존한다

새로운 입력 장치(e.g. Mouse)가 추가된다면, Computer 클래스를 직접 수정해야 한다!

 

- 유지보수 및 확장성이 낮다

Keyboard를 만약 무선 키보드(WirelessKeyboard)로 변경하려면 Computer 클래스 수정이 필요하다

OCP(개방-폐쇄 원칙)도 위반

 

 

(개선된 코드)

InputDevice, DisplayDevice 인터페이스를 만들고 input, display 추상메서드를 정의한다

// 추상화 (인터페이스) 정의
interface InputDevice {
    void input();
}

interface DisplayDevice {
    void display();
}

// 저수준 모듈 (세부 구현)
class Keyboard implements InputDevice {
    public void input() {
        System.out.println("키보드를 사용하여 입력합니다.");
    }
}

class Monitor implements DisplayDevice {
    public void display() {
        System.out.println("모니터에 화면을 출력합니다.");
    }
}

// 고수준 모듈 (비즈니스 로직)
class Computer {
    private InputDevice inputDevice;
    private DisplayDevice displayDevice;

    // DI(의존성 주입)
    public Computer(InputDevice inputDevice, DisplayDevice displayDevice) {
        this.inputDevice = inputDevice;
        this.displayDevice = displayDevice;
    }

    public void operate() {
        inputDevice.input();
        displayDevice.display();
    }
}

public class Main {
    public static void main(String[] args) {
        InputDevice keyboard = new Keyboard();
        DisplayDevice monitor = new Monitor();

        // DI(의존성 주입)를 통해 Computer는 구현체에 직접 의존하지 않음
        Computer computer = new Computer(keyboard, monitor);
        computer.operate();
    }
}

 

상위 모듈(Computer)이 저수준 모듈(Keyboard, Monitor)과 직접 결합되지 않음

 

 

(개선된 코드에서의 확장)

 class WirelessKeyboard implements InputDevice {
    public void input() {
        System.out.println("무선 키보드를 사용하여 입력하니다.");
    }
}

class Projector implements DisplayDevice {
    public void display() {
        System.out.println("프로젝터에 화면을 출력합니다.");
    }
}

// 새로운 장치 사용
public class Main {
    public static void main(String[] args) {
        InputDevice wirelessKeyboard = new WirelessKeyboard();
        DisplayDevice projector = new Projector();

        // 코드 수정 없이 새로운 장치를 연결 가능!
        Computer computer = new Computer(wirelessKeyboard, projector);
        computer.operate();
    }
}

 

새로운 입력 장치(Mouse)나 출력 장치(Projector)를 추가해도 Computer 클래스 수정 불필요

실행 시점에 원하는 구현체를 주입할 수 있으므로, 코드 수정 없이 다양한 장치를 사용 가능

 

 

 

https://claremont.tistory.com/entry/SW-%EC%84%A4%EA%B3%84-%EC%9B%90%EC%B9%99-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85DI-Dependency-Injection-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0wPython

 

[SW 설계 원칙] 의존성 주입(DI, Dependency Injection) 쉽게 이해하기(w/Python)

괜히 쫄 거 없다!정말 간단한 개념이다예제를 통해 의존성 주입이 무엇인지 알아볼 거고, 언어는 제일 가독성이 좋은 파이썬을 이용하겠다 :]  ㅁ의존성 주입(DI, Dependency Injection): 객체 간의 의

claremont.tistory.com


 

[SOLID 원칙 정리표]

📌 핵심 요약

  • SRP → 하나의 클래스는 하나의 책임만
  • OCP → 기존 코드 수정 없이 기능 확장 가능해야
  • LSP → 부모 클래스를 자식이 대체할 수 있어야
  • ISP → 사용하지 않는 인터페이스를 강요하지 말 것
  • DIP → 세부 구현이 아닌 인터페이스에 의존하도록 설계

 

 

 

 

이 원칙들을 학습하면서 가장 중요한 것은 단순히 개념을 아는 것이 아니라 실제 코드에 적용하는 경험이다. SOLID 원칙을 따르는 것이 필수는 아니지만, 이를 활용하면 코드의 품질이 크게 향상될 수 있다. 또한, 협업하는 개발 환경에서 다른 개발자가 내 코드를 쉽게 이해하고 유지보수할 수 있도록 돕는다.

 

개인적으로 나는 개발을 하면서 SOLID 원칙이 항상 정답은 아니라는 것을 경험했다. 때로는 특정 원칙을 엄격하게 지키는 것이 오히려 코드의 복잡성을 증가시키기도 했다. 하지만 SOLID 원칙을 이해하고 있으면, 필요할 때 적절히 적용하는 선택지를 가질 수 있다는 점에서 큰 의미가 있다.

궁극적으로 좋은 코드는 원칙을 맹목적으로 따르는 것이 아니라, 상황에 맞게 유연하게 설계하는 것이라고 생각한다. 개발자의 역할은 이 원칙들을 단순히 적용하는 것이 아니라, 적절하게 조합하여 최적의 설계를 만드는 것이다. 그렇기에 SOLID 원칙을 깊이 이해하고, 상황에 맞게 활용하는 능력을 기르는 것이 중요하다고 본다 [:

#SK, #SKALA, #SKALA1기