SOLID 원칙은 객체 지향 설계의 다섯 가지 기본 원칙을 나타내며, 소프트웨어 설계에서 유지보수성과 확장성을 개선하는 데 중점을 둡니다. 각 원칙은 객체 지향 프로그래밍에서 코드의 품질을 높이기 위해 고안되었습니다. 아래는 SOLID 원칙을 Java 코드 예제와 함께 설명한 내용입니다.
1. S: 단일 책임 원칙 (Single Responsibility Principle)
원칙: 하나의 클래스는 하나의 책임만 가져야 하며, 그 책임은 변경의 이유가 되어야 합니다.
예시:
// 잘못된 예시: Book 클래스가 데이터를 저장하고 출력까지 책임지고 있음.
class Book {
    private String title;
    private String author;
    // 저장 책임
    public void save() {
        // 데이터베이스에 책 정보를 저장하는 코드
    }
    // 출력 책임
    public void print() {
        System.out.println(title + " by " + author);
    }
}
// 개선된 예시
class Book {
    private String title;
    private String author;
    public String getTitle() { return title; }
    public String getAuthor() { return author; }
}
// 별도의 클래스가 출력 책임을 담당
class BookPrinter {
    public void print(Book book) {
        System.out.println(book.getTitle() + " by " + book.getAuthor());
    }
}
2. O: 개방-폐쇄 원칙 (Open/Closed Principle)
원칙: 소프트웨어 엔터티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.
예시:
// 잘못된 예시: 기존 클래스의 코드를 수정해야만 새로운 기능을 추가할 수 있음.
class Rectangle {
    public double length;
    public double width;
}
class AreaCalculator {
    public double calculateRectangleArea(Rectangle rectangle) {
        return rectangle.length * rectangle.width;
    }
}
// 개선된 예시: 새로운 도형이 추가되어도 기존 코드를 수정할 필요가 없음.
interface Shape {
    double area();
}
class Rectangle implements Shape {
    private double length;
    private double width;
    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }
    @Override
    public double area() {
        return length * width;
    }
}
class Circle implements Shape {
    private double radius;
    public Circle(double radius) {
        this.radius = radius;
    }
    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}
class AreaCalculator {
    public double calculateArea(Shape shape) {
        return shape.area();
    }
}
3. L: 리스코프 치환 원칙 (Liskov Substitution Principle)
원칙: 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서, 자식 클래스 객체로 치환할 수 있어야 한다.
예시:
// 잘못된 예시: Square가 Rectangle을 상속받았지만, Rectangle의 행동 규약을 깨뜨림.
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 getArea() {
        return width * height;
    }
}
class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width; // 높이와 너비가 같아야 함
    }
    @Override
    public void setHeight(int height) {
        this.width = height;
        this.height = height;
    }
}
// Square는 Rectangle을 대체할 수 없으므로 LSP를 위반함
해결책: 상속 구조를 재고하거나 상속 대신 별도의 클래스로 구현합니다.
4. I: 인터페이스 분리 원칙 (Interface Segregation Principle)
원칙: 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다. 인터페이스는 작고 구체적인 단위로 분리되어야 한다.
예시:
// 잘못된 예시: Interface가 너무 많아 모든 메서드를 구현할 필요가 없음.
interface Worker {
    void work();
    void eat();
}
class Developer implements Worker {
    @Override
    public void work() {
        // 코딩 작업
    }
    @Override
    public void eat() {
        // 개발자도 먹기는 하지만, 이 메서드는 무의미할 수 있음
    }
}
// 개선된 예시: 인터페이스를 분리함으로써 필요한 부분만 구현하도록 함.
interface Workable {
    void work();
}
interface Eatable {
    void eat();
}
class Developer implements Workable {
    @Override
    public void work() {
        // 코딩 작업
    }
}
class OfficeWorker implements Workable, Eatable {
    @Override
    public void work() {
        // 사무 작업
    }
    @Override
    public void eat() {
        // 식사
    }
}
5. D: 의존성 역전 원칙 (Dependency Inversion Principle)
원칙: 고수준 모듈은 저수준 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다. 또한, 추상화는 구체적인 것에 의존해서는 안 된다. 구체적인 것이 추상화에 의존해야 한다.
예시:
// 잘못된 예시: High-level 모듈이 Low-level 모듈에 의존하고 있음.
class LightBulb {
    public void turnOn() {
        System.out.println("LightBulb: Bulb turned on...");
    }
    public void turnOff() {
        System.out.println("LightBulb: Bulb turned off...");
    }
}
class Switch {
    private LightBulb lightBulb;
    public Switch(LightBulb lightBulb) {
        this.lightBulb = lightBulb;
    }
    public void operate() {
        lightBulb.turnOn();
    }
}
// 개선된 예시: 인터페이스를 사용하여 추상화에 의존하게 함.
interface Switchable {
    void turnOn();
    void turnOff();
}
class LightBulb implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("LightBulb: Bulb turned on...");
    }
    @Override
    public void turnOff() {
        System.out.println("LightBulb: Bulb turned off...");
    }
}
class Switch {
    private Switchable device;
    public Switch(Switchable device) {
        this.device = device;
    }
    public void operate() {
        device.turnOn();
    }
}
요약
- SRP: 클래스는 하나의 책임만 가져야 한다.
- OCP: 기존 코드를 수정하지 않고 기능을 확장할 수 있어야 한다.
- LSP: 자식 클래스는 부모 클래스를 대체할 수 있어야 한다.
- ISP: 클라이언트는 사용하지 않는 메서드에 의존하지 않아야 한다.
- DIP: 고수준 모듈은 저수준 모듈에 의존해서는 안 된다.
이 원칙들을 준수하면 코드를 더 유연하고 유지보수하기 쉽게 만들 수 있습니다.
Share article