객체지향 5대 원칙 (SOLID)

WillyByun·2025년 3월 11일

객체지향 프로그래밍의 5대 원칙은 로버트 마틴(Robert C. Martin)이 2000년대 초반에 정리한 원칙으로, 앞글자를 따서 'SOLID'라고 합니다. 이 원칙들은 유지보수가 쉽고, 확장 가능하며, 유연한 소프트웨어를 만들기 위한 지침입니다.

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

정의

클래스는 오직 하나의 책임만 가져야 하며, 클래스를 변경하는 이유도 오직 하나여야 한다.

핵심 내용

  • 하나의 클래스는 하나의 기능이나 관심사에 대해서만 책임을 가져야 함
  • "클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다"

예시

// 잘못된 예: UserService 클래스가 너무 많은 책임을 가짐
class UserService {
    void createUser(User user) { /* ... */ }
    void sendEmail(String to, String subject) { /* ... */ }
    void generateReport() { /* ... */ }
}

// 개선된 예: 각 클래스가 하나의 책임만 가짐
class UserService {
    void createUser(User user) { /* ... */ }
}

class EmailService {
    void sendEmail(String to, String subject) { /* ... */ }
}

class ReportService {
    void generateReport() { /* ... */ }
}

장점

  • 코드의 가독성과 유지보수성 향상
  • 한 책임의 변경이 다른 책임에 영향을 미치지 않음
  • 재사용성 증가

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

정의

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

핵심 내용

  • 기능을 추가하거나 변경해야 할 때, 기존 코드를 수정하지 않고 새로운 코드를 추가하는 방식으로 설계
  • 추상화와 다형성을 통해 구현

예시

// 잘못된 예: 새로운 도형을 추가할 때마다 클래스 수정 필요
class AreaCalculator {
    double calculateArea(Object shape) {
        if (shape instanceof Rectangle) {
            Rectangle rectangle = (Rectangle) shape;
            return rectangle.width * rectangle.height;
        } else if (shape instanceof Circle) {
            Circle circle = (Circle) shape;
            return Math.PI * circle.radius * circle.radius;
        }
        return 0;
    }
}

// 개선된 예: 인터페이스와 다형성 사용
interface Shape {
    double calculateArea();
}

class Rectangle implements Shape {
    double width;
    double height;
    
    @Override
    public double calculateArea() {
        return width * height;
    }
}

class Circle implements Shape {
    double radius;
    
    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

// 새로운 도형을 추가해도 AreaCalculator 수정 불필요
class AreaCalculator {
    double calculateArea(Shape shape) {
        return shape.calculateArea();
    }
}

장점

  • 기존 코드의 변경 없이 기능 확장 가능
  • 유지보수성 향상 및 버그 발생 가능성 감소
  • 새로운 기능 추가가 기존 코드에 영향을 미치지 않음

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

정의

상위 타입의 객체를 하위 타입의 객체로 치환해도 프로그램의 정확성은 변하지 않아야 한다.

핵심 내용

  • 하위 클래스는 상위 클래스의 행동을 완전히 대체할 수 있어야 함
  • 하위 클래스가 상위 클래스의 계약(사전조건, 후행조건)을 준수해야 함

예시

// 잘못된 예: Rectangle을 상속한 Square가 LSP 위반
class Rectangle {
    private double width;
    private double height;
    
    public void setWidth(double width) {
        this.width = width;
    }
    
    public void setHeight(double height) {
        this.height = height;
    }
    
    public double getArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(double width) {
        super.setWidth(width);
        super.setHeight(width); // 정사각형이므로 가로=세로
    }
    
    @Override
    public void setHeight(double height) {
        super.setHeight(height);
        super.setWidth(height); // 정사각형이므로 가로=세로
    }
}

// 문제 발생 코드
void resizeRectangle(Rectangle rectangle) {
    rectangle.setWidth(10);
    rectangle.setHeight(20);
    // Rectangle이면 area는 200이 되어야 하는데
    // Square가 전달되면 area는 400이 됨 (마지막 setHeight가 width도 변경)
    assert rectangle.getArea() == 200; // Square 객체가 전달되면 실패
}

개선 방법

  • 상속보다 컴포지션을 사용
  • 인터페이스와 구현 클래스를 명확히 분리

장점

  • 다형성을 보다 안전하게 사용할 수 있음
  • 예측 가능하고 일관된 동작 보장
  • 상속 관계의 논리적 정확성 향상

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

정의

클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.

핵심 내용

  • 큰 인터페이스보다 구체적인 여러 개의 인터페이스가 더 좋음
  • 클래스는 사용하지 않는 메서드를 가진 인터페이스에 의존하면 안 됨

예시

// 잘못된 예: 거대한 인터페이스
interface Worker {
    void work();
    void eat();
    void sleep();
}

// 일부 메서드만 필요한 클래스들이 불필요한 메서드에 의존하게 됨
class Robot implements Worker {
    public void work() { /* ... */ }
    public void eat() { /* 로봇은 먹지 않음 */ }
    public void sleep() { /* 로봇은 자지 않음 */ }
}

// 개선된 예: 인터페이스 분리
interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

interface Sleepable {
    void sleep();
}

// 로봇은 필요한 인터페이스만 구현
class Robot implements Workable {
    public void work() { /* ... */ }
}

// 사람은 모든 기능 필요
class Human implements Workable, Eatable, Sleepable {
    public void work() { /* ... */ }
    public void eat() { /* ... */ }
    public void sleep() { /* ... */ }
}

장점

  • 불필요한 의존성 제거
  • 인터페이스 변경이 미치는 영향 최소화
  • 필요한 기능만 포함하는 깔끔한 인터페이스 설계

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

정의

고수준 모듈은 저수준 모듈에 의존해서는 안 됨. 둘 다 추상화에 의존해야 함. 또한 추상화는 세부 사항에 의존해서는 안 되며, 세부 사항이 추상화에 의존해야 함.

핵심 내용

  • 구체적인 클래스보다 인터페이스나 추상 클래스와 같은 추상화에 의존해야 함
  • 의존성을 주입(Dependency Injection)받는 방식으로 설계

예시

// 잘못된 예: 고수준 모듈이 저수준 모듈에 직접 의존
class LightBulb {
    void turnOn() {
        // 전구를 켜는 코드
    }
    
    void turnOff() {
        // 전구를 끄는 코드
    }
}

class Switch {
    private LightBulb bulb; // 직접적인 의존
    
    public Switch() {
        this.bulb = new LightBulb(); // 강한 결합
    }
    
    void operate() {
        // 전구 제어 로직
    }
}

// 개선된 예: 의존성 역전 및 의존성 주입
interface Switchable {
    void turnOn();
    void turnOff();
}

class LightBulb implements Switchable {
    @Override
    public void turnOn() {
        // 전구를 켜는 코드
    }
    
    @Override
    public void turnOff() {
        // 전구를 끄는 코드
    }
}

class Fan implements Switchable {
    @Override
    public void turnOn() {
        // 선풍기를 켜는 코드
    }
    
    @Override
    public void turnOff() {
        // 선풍기를 끄는 코드
    }
}

class Switch {
    private Switchable device; // 추상화에 의존
    
    // 의존성 주입
    public Switch(Switchable device) {
        this.device = device;
    }
    
    void operate() {
        // device 제어 로직
    }
}

// 사용 예
Switch lightSwitch = new Switch(new LightBulb());
Switch fanSwitch = new Switch(new Fan());

장점

  • 모듈 간 결합도 감소
  • 테스트 용이성 증가
  • 시스템 확장 및 변경이 쉬워짐
  • 다형성을 활용한 유연한 설계 가능

SOLID 원칙의 종합적 이점

  1. 유지보수성 향상: 코드 변경이 쉽고 영향 범위가 제한적임
  2. 확장성 증가: 새로운 기능 추가가 기존 코드 수정 없이 가능
  3. 재사용성 증가: 작고 응집력 있는 컴포넌트 설계로 재사용 용이
  4. 테스트 용이성: 컴포넌트 분리와 의존성 주입으로 단위 테스트 수월
  5. 버그 감소: 명확한 책임과 의존성으로 예측 가능한 동작
  6. 병렬 개발 가능: 인터페이스 기반 설계로 팀 작업 효율성 향상

SOLID 원칙은 완벽하게 지켜야 하는 규칙이라기보다 좋은 설계를 위한 지침으로 이해하는 것이 중요합니다. 상황과 맥락에 따라 적절하게, 또는 부분적으로 적용하는 것이 바람직합니다.

profile
웃음 주는 개발자 되기

0개의 댓글