코딩 규칙과 방법들

SOLID - 좋은 객체 지향 설계의 5가지 원칙

SoU330 2025. 1. 11. 22:05

 

 

Clean Code로 유명한 로버트 C. 마틴이 소프트웨어 개발에서 발생하는 설계 문제를 해결하고자 객체지향 설계 원칙을 체계적으로 정리하였다.

 

 

 

S : Single Responsibility Principle - 단일 책임 원칙

O : Open/Closed Principle - 개방/폐쇄 원칙

L : Liskov Substitution Principle - 리스코프 치환 원칙

I : Interface Segregation Principle - 인터페이스 분리 원칙

D : Dependency Inversion Principle - 의존 역전 원칙

 

 

 

이 5가지 원칙에 대해 살펴보자

 

 

 

Single Responsibility Principle - 단일 책임 원칙

클래스는 하나의 책임만 가져야 하며, 클래스가 변경되는 이유는 하나뿐이어야 한다.

클래스가 지나치게 많은 일을 하게 되면 재사용성이 낮아지고, 변경 시 부수적인 영향을 미칠 가능성이 높아진다. SRP는 각 클래스가 명확한 책임을 가지도록 하여 이러한 문제를 방지한다.

 

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

class ReportGenerator {
    public String generateReport() {
        return "Report Content";
    }
}

ReportGenerator는 보고서를 생성하는 책임만, ReportPrinter는 보고서를 출력하는 책임만 가진다.

 

 

 

 

 

Open/Closed Principle - 개방/폐쇄 원칙

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

기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있도록 설계한다. 이는 변경에 따른 부작용을 줄이고 유지보수를 용이하게 만든다.

 

interface Shape {
    double calculateArea();
}

class Circle implements Shape {
    private double radius;
    public Circle(double radius) {
        this.radius = radius;
    }
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

class Rectangle implements Shape {
    private double width, height;
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    public double calculateArea() {
        return width * height;
    }
}

class AreaCalculator {
    public double calculateTotalArea(List<Shape> shapes) {
        return shapes.stream().mapToDouble(Shape::calculateArea).sum();
    }
}

새로운 도형을 추가하려면 Shape 인터페이스를 구현하는 클래스를 추가하면 된다. 기존 클래스는 수정할 필요가 없다.

 

 

 

 

 

 

Liskov Substitution Principle - 리스코프 치환 원칙

서브타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다.

상속 관계에서 부모 클래스와 자식 클래스가 동일하게 동작해야 하며, 이를 통해 다형성을 안전하게 유지할 수 있다.

 

위반 예시

class Rectangle {
    protected int width;
    protected int height;

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

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

    public int calculateArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = this.height = width;
    }

    @Override
    public void setHeight(int height) {
        this.width = this.height = height;
    }
}

Square은 Rectangle을 상속받지만, setWidth와 setHeight의 동작이 다르다. 이는 Rectangle로 작성된 코드가 Square로 대체되었을 때 예상치 못한 결과를 초래할 수 있다.

 

-> Rectangle과 Square은 본질적으로 다른 도형이므로 상속 관계로 설계하기보다는 별도의 독립적인 클래스로 분리하는 것이 좋다.

 

 

 

 

 

 

 

 

Interface Segregation Principle - 인터페이스 분리 원칙

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

하나의 거대한 인터페이스 대신 클라이언트에 맞는 여러 개의 작은 인터페이스로 분리하여 불필요한 의존성을 줄인다.

 

interface Printer {
    void printDocument(String content);
}

interface Scanner {
    void scanDocument();
}

class MultiFunctionPrinter implements Printer, Scanner {
    public void printDocument(String content) {
        System.out.println("Printing: " + content);
    }

    public void scanDocument() {
        System.out.println("Scanning document...");
    }
}

class BasicPrinter implements Printer {
    public void printDocument(String content) {
        System.out.println("Printing: " + content);
    }
}

BasicPrinter는 프린트 기능만 필요하므로 스캔과 관련된 메서드를 구현할 필요가 없다.

 

 

 

 

 

 

 

 

Dependency Inversion Principle - 의존 역전 원칙

고수준 모듈은 저수준 모듈에 의존해서는 안 되며 둘 다 추상화된 것에 의존해야 한다.

의존성을 추상화하여 모듈 간 결합도를 낮추고 변화에 더 유연하게 대응할 수 있도록 한다.

 

interface NotificationService {
    void sendNotification(String message);
}

class EmailNotificationService implements NotificationService {
    public void sendNotification(String message) {
        System.out.println("Sending Email: " + message);
    }
}

class SMSNotificationService implements NotificationService {
    public void sendNotification(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

class UserController {
    private NotificationService notificationService;

    public UserController(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void notifyUser(String message) {
        notificationService.sendNotification(message);
    }
}

UserController는 NotificationService 인터페이스에 의존하며 특정 구현(EmailNotificationService, SMSNotificationService)에 직접 의존하지 않는다. 이를 통해 의존성을 유연하게 교체할 수 있다.

 

 

 

 

 

 

 

 

SOLID 원칙을 준수하여 깨끗하고 견고한 객체지향 설계를 해보자.