SOLID 원칙 – 객체지향 설계의 5대 핵심 원칙 이해하기

오늘은 객체지향 설계에서 빠질 수 없는 핵심 개념인 SOLID 원칙에 대해 살펴보려고 합니다. SOLID 원칙은 로버트 C. 마틴이 제안한 5가지 객체지향 설계 원칙으로, 유지보수성과 확장성을 높이기 위한 가이드라인입니다.

 

SOLID란 무엇인가?

SOLID는 다음 다섯 가지 원칙의 머리글자를 따 만든 약어입니다.

약어 원칙명 설명
   S    Single Responsibility Principle (단일 책임 원칙) 클래스는 단 하나의 책임만 가져야 한다.
   O    Open/Closed Principle (개방-폐쇄 원칙) 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.
   L    Liskov Substitution Principle (리스코프 치환 원칙) 자식 클래스는 부모 클래스를 대체할 수 있어야 한다.
   I    Interface Segregation Principle (인터페이스 분리 원칙) 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
   D    Dependency Inversion Principle (의존성 역전 원칙) 구체화가 아닌 추상화에 의존해야 한다.

 

 

 각 원칙 상세 설명 

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

하나의 클래스는 오직 하나의 책임만 가져야 합니다.
여기서 ‘책임’이란 하나의 역할만 가지고 있어야 하며, 변경의 이유도 한가지 이유로만 변경해야 한다는 뜻입니다.

 

안좋은 설계

class UserService {
    public void createUser(String name) {
        // 사용자 생성 로직
    }
    public void sendWelcomeEmail(String email) {
        // 이메일 발송 로직
    }
}

위 예시는 사용자 생성과 이메일 발송이라는 두 가지 책임을 한 클래스가 가지고 있습니다.

 

개선된 설계

class UserService {
    public void createUser(String name) {
        // 사용자 생성 로직
    }
}

class EmailService {
    public void sendWelcomeEmail(String email) {
        // 이메일 발송 로직
    }
}

이렇게 책임을 분리하면 유지보수가 쉬워지고, 한 기능의 변경이 다른 기능에 영향을 미치지 않습니다.

 

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

소프트웨어 엔티티(클래스, 모듈, 함수)는 확장에는 열려(Open) 있어야 하고, 수정에는 닫혀(Closed) 있어야 합니다.

 

안좋은 설계

class DiscountService {
    public double getDiscount(String type) {
        if (type.equals("VIP")) {
            return 0.2;
        } else if (type.equals("Normal")) {
            return 0.1;
        }
        return 0.0;
    }
}

위의 코드에서는 새로운 할인 정책이 생길 때마다 if-else를 수정해야 합니다.

 

개선된 설계

interface DiscountPolicy {
    double getDiscount();
}

class VipDiscount implements DiscountPolicy {
    public double getDiscount() {
        return 0.2;
    }
}

class NormalDiscount implements DiscountPolicy {
    public double getDiscount() {
        return 0.1;
    }
}

class DiscountService {
    public double getDiscount(DiscountPolicy policy) {
        return policy.getDiscount();
    }
}

위의 코드에서는 새로운 할인 정책 클래스를 추가하기만 하면 됩니다. 기존 코드는 수정하지 않아도 됩니다.

 

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

서브타입(자식 클래스)은 언제나 기반타입(부모 클래스)으로 교체할 수 있어야 합니다. 즉, 부모 클래스의 객체가 사용되는 곳에 자식 클래스의 객체를 넣어도 프로그램의 기능이 깨지지 않아야 합니다.

 

안좋은 설계

class Bird {
    public void fly() {
        System.out.println("날다");
    }
}

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

위의 예시에서는 Penguin은 Bird로 대체할 수 없으므로 LSP 위반입니다.

 

개선된 설계

interface Bird { }

interface Flyable {
    void fly();
}

class Sparrow implements Bird, Flyable {
    public void fly() {
        System.out.println("참새 날다");
    }
}

class Penguin implements Bird {
    public void swim() {
        System.out.println("펭귄 수영");
    }
}

위의 예시에서는 날 수 있는 새와 날 수 없는 새를 구분하여 LSP를 지킵니다.

 

I – 인터페이스 분리 원칙 (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 Human implements Workable, Eatable {
    public void work() {}
    public void eat() {}
}

class Robot implements Workable {
    public void work() {}
}

이렇게 필요한 기능만 구현하도록 인터페이스를 분리합니다.

 

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

고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 합니다. 구체적인 구현 클래스가 아니라 인터페이스나 추상 클래스에 의존하라는 뜻입니다.

 

안좋은 설계

class MySQLDatabase {
    public void save(String data) {}
}

class UserService {
    private MySQLDatabase database = new MySQLDatabase();
    public void saveUser(String data) {
        database.save(data);
    }
}

위의 예시에서는 DB가 변경되면 UserService도 수정해야 합니다.

 

개선된 설계

interface Database {
    void save(String data);
}

class MySQLDatabase implements Database {
    public void save(String data) {}
}

class MongoDatabase implements Database {
    public void save(String data) {}
}

class UserService {
    private Database database;
    public UserService(Database database) {
        this.database = database;
    }
    public void saveUser(String data) {
        database.save(data);
    }
}

위의 예시에서는 어떤 DB를 쓰든 UserService는 수정할 필요 없습니다.

 

SOLID 원칙의 장점

  • 유지보수성 향상: 변경이 필요한 부분이 최소화됩니다.
  • 확장성 강화: 새로운 기능 추가가 쉽습니다.
  • 코드 재사용성 증가: 모듈 간 결합도가 낮아집니다.
  • 테스트 용이성: 단위 테스트와 Mocking이 쉬워집니다.

 

SOLID 원칙은 단순한 이론이 아니라, 실제 프로젝트에서 코드 품질을 좌우하는 핵심 기준입니다. 특히 S, O, D는 업무상에서 많이 적용되는 만큼 알고 있으면 좋을 것 가아요. 모든 코드를 처음부터 SOLID하게 작성하기는 어렵지만, 리팩터링 시 하나씩 적용해 나가면 변화에 강하고 확장에 유연한 코드를 만들 수 있을 것 같습니다.