SOLID? 단단한거임?

김한결·2025년 4월 20일

SOLID 원칙이란?

객체지향 설계 시 유지보수성과 확장성을 높이기 위해 지켜야 할 5가지 원칙이다.
각 원칙은 클래스, 인터페이스, 모듈 설계 시 발생할 수 있는 문제를 방지해준다.

원칙이름설명
SSRP (단일 책임 원칙)하나의 클래스는 하나의 책임만 가져야 한다.
OOCP (개방-폐쇄 원칙)확장에는 열려 있고, 수정에는 닫혀 있어야 한다.
LLSP (리스코프 치환 원칙)하위 클래스는 상위 클래스를 완벽히 대체할 수 있어야 한다.
IISP (인터페이스 분리 원칙)클라이언트는 사용하지 않는 메서드에 의존하면 안 된다.
DDIP (의존성 역전 원칙)추상화에 의존해야 하며, 구현체에 의존하지 않아야 한다.

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

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

왜?

SRP 에서 책임이란, 기능정도로 생각하면 된다. 만약 한 클래스가 수행할 수 있는 기능 (책임) 이 여러 개라면, 클래스 내부의 함수끼리 강한 결합을 발생할 가능성이 높아져 유지보수가 복잡해질 것이다.

위반 예시

class UserManager {
    void createUser(User user) {
        // 사용자 생성 로직
    }

    void sendEmail(User user, String message) {
        // 이메일 전송 로직
    }
}

개선 예시

class UserManager {
    void createUser(User user) {
        // 사용자 생성 로직
    }
}

class EmailService {
    void sendEmail(User user, String message) {
        // 이메일 전송 로직
    }
}

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

  • 확장에는 열려 있고, 수정에는 닫혀 있어야 한다.

왜?

어떤 모듈의 기능을 하나 수정할 때, 그 모듈을 이용하는 다른 모듈들 역시 줄줄이 고쳐야 한다면 유지보수가 복잡할 것이다.

위반 예시

class PaymentProcessor {
    void pay(String method) {
        if (method.equals("card")) {
            // 카드 결제
        } else if (method.equals("cash")) {
            // 현금 결제
        }
    }
}

개선 예시

interface PaymentMethod {
    void pay();
}

class CardPayment implements PaymentMethod {
    @Overriding
    public void pay() {
        // 카드 결제 처리
    }
}

class CashPayment implements PaymentMethod {
    @Overriding
    public void pay() {
        // 현금 결제 처리
    }
}

class PaymentProcessor {
    void pay(PaymentMethod method) {
        method.pay();
    }
}

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

  • 하위 클래스는 상위 클래스의 자리를 대체할 수 있어야 한다.

왜?

상속관계에서는 꼭 일반화 관계 (IS-A) 가 성립해야 한다. 이를 위배하게 된다면 기능 확장을 위해 기존의 코드를 여러 번 수정해야 할 것이다.

위반 예시

class Rectangle {
    protected int width, height;

    void setWidth(int width) { this.width = width; }
    void setHeight(int height) { this.height = height; }
    int area() { return width * height; }
}

class Square extends Rectangle {
    void setWidth(int side) {
        width = height = side;
    }
    void setHeight(int side) {
        width = height = side;
    }
}

실행 코드

Rectangle rect = new Square();
rect.setWidth(5);
rect.setHeight(10);
System.out.println(rect.area());  // 기대값: 50, 실제값: 100

기대값과 실제값이 다름

개선 예시

abstract class Shape {
    abstract int area();
}

class Rectangle extends Shape {
    private int width, height;

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

    int area() { return width * height; }
}

class Square extends Shape {
    private int side;

    void setSide(int side) { this.side = side; }

    int area() { return side * side; }
}

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

  • 클라이언트는 사용하지 않는 메서드에 의존하면 안 된다.

왜?

하나의 통상적인 인터페이스보다는 차라리 여러 개의 세부적인 (구체적인) 인터페이스가 낫다.

위반 예시

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

class Robot implements Worker {
    public void work() { /* 로봇 일 처리 */ }
    public void eat() { /* 로봇은 먹지 않음 */ }
}

개선 예시

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Robot implements Workable {
    public void work() { /* 로봇 일 처리 */ }
}

class Human implements Workable, Eatable {
    public void work() { /* 인간 일 처리 */ }
    public void eat() { /* 식사 처리 */ }
}

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

  • 고수준 모듈은 저수준 모듈에 의존하지 말고, 추상화(인터페이스)에 의존해야 한다.

왜?

저수준 모듈이 변경되어도 고수준 모듈은 변경이 필요없는 형태가 이상적이기 때문이다.

위반 예시

class MySQLDatabase {
    void saveData(String data) {
        // MySQL 저장
    }
}

class DataService {
    private MySQLDatabase db = new MySQLDatabase();

    void save(String data) {
        db.saveData(data);
    }
}

개선 예시

interface Database {
    void saveData(String data);
}

// 구체 구현 A
class MySQLDatabase implements Database {
    public void saveData(String data) {
        System.out.println("MySQL에 저장: " + data);
    }
}

// 구체 구현 B
class InMemoryDatabase implements Database {
    public void saveData(String data) {
        System.out.println("메모리에 저장: " + data);
    }
}

// 상위 모듈: 추상화에만 의존
class DataService {
    private Database db;

    public DataService(Database db) {
        this.db = db; // 추상화에 의존 → 유연성 확보
    }

    void save(String data) {
        db.saveData(data);
    }
}

DIP 적용의 장점

항목설명
유연성MySQL ↔ InMemory 자유롭게 교체 가능
테스트 용이테스트 시 FakeDatabase 주입 가능
OCP와 궁합구현체 추가 시 DataService 수정 불필요
확장성다양한 저장 방식에 쉽게 대응 가능
의존성 방향 역전고수준 모듈이 하위 구현에 의존하지 않음

마무리

SOLID 원칙은 단순한 코딩 스타일이 아니라 좋은 소프트웨어 아키텍처의 기반이 된다.
처음엔 부담스러울 수 있지만, 점차 익숙해지면 유지보수하기 쉽고 확장 가능한 코드를 작성할 수 있을 것이다.

0개의 댓글