객체지향 프로그래밍의 SOLID 원칙

썩은요플렛's avatar
Aug 28, 2024
객체지향 프로그래밍의 SOLID 원칙
 
 
SOLID 원칙은 객체지향 설계에서 좋은 소프트웨어 구조를 만들기 위한 다섯 가지 원칙을 의미한다. 이 원칙들은 코드의 유연성, 유지보수성, 확장성을 높이고, 의도하지 않은 결합도를 줄이기 위한 가이드라인으로 사용된다. SOLID 원칙은 각 원칙의 머리글자를 따서 만들어졌으며, 소프트웨어 설계의 품질을 높이기 위한 필수적인 요소로 여겨진다.

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

단일 책임 원칙은 클래스는 단 하나의 책임만 가져야 하며, 클래스가 변경되는 이유는 하나뿐이어야 한다는 원칙이다. 이를 통해 클래스가 여러 가지 역할을 수행하면서 발생할 수 있는 복잡성과 의존성을 줄일 수 있다.
  • 예시: 하나의 클래스가 사용자 정보 관리와 파일 입출력 처리를 동시에 담당한다면, 이는 단일 책임 원칙을 위반한 것이다. 사용자의 정보가 변경될 때와 파일 입출력 로직이 변경될 때 클래스가 수정되어야 하므로, 이 클래스는 두 가지 책임을 가지고 있다.
잘못된 예시:
class UserManager { public void addUser(String userName) { // 사용자 추가 로직 } public void saveToFile(String fileName) { // 파일에 사용자 정보 저장 로직 } }
수정된 예시:
class UserManager { public void addUser(String userName) { // 사용자 추가 로직 } } class FileManager { public void saveToFile(String fileName) { // 파일에 저장 로직 } }
여기서 UserManager 클래스는 사용자 관리만 담당하고, FileManager 클래스는 파일 저장만 담당하므로 단일 책임 원칙을 따르게 된다.

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

개방-폐쇄 원칙은 소프트웨어 요소는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다는 원칙입니다. 즉, 기존 코드를 수정하지 않고도 기능을 확장할 수 있도록 설계해야 한다. 이는 주로 인터페이스나 추상 클래스를 통해 구현된다.
  • 예시: 새로운 기능을 추가할 때 기존 코드를 변경하지 않고, 새로운 클래스를 추가하거나 상속을 통해 기능을 확장하는 방식이다.
잘못된 예시:
class Rectangle { public double width; public double height; } class AreaCalculator { public double calculateRectangleArea(Rectangle rectangle) { return rectangle.width * rectangle.height; } }
수정된 예시:
interface Shape { double calculateArea(); } class Rectangle implements Shape { private double width; private double height; Rectangle(double width, double height) { this.width = width; this.height = height; } public double calculateArea() { return width * height; } } class Circle implements Shape { private double radius; Circle(double radius) { this.radius = radius; } public double calculateArea() { return Math.PI * radius * radius; } } class AreaCalculator { public double calculateArea(Shape shape) { return shape.calculateArea(); } }
여기서 AreaCalculator 클래스는 변경 없이도 Shape 인터페이스를 구현하는 새로운 도형 클래스를 추가할 수 있다.

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

리스코프 치환 원칙은 자식 클래스는 언제나 자신의 부모 클래스를 대체할 수 있어야 한다는 원칙이다. 즉, 부모 클래스가 사용되는 모든 곳에서 자식 클래스를 사용할 수 있어야 하며, 자식 클래스가 부모 클래스의 기능을 변경하거나 훼손해서는 안 된다.
  • 예시: 만약 자식 클래스가 부모 클래스의 메서드를 재정의하여 원래의 의도를 변경하면, 이는 리스코프 치환 원칙을 위반한 것이다.
잘못된 예시:
class Bird { public void fly() { System.out.println("Bird is flying"); } } class Ostrich extends Bird { @Override public void fly() { throw new UnsupportedOperationException("Ostrich can't fly"); } }
수정된 예시:
class Bird { public void move() { System.out.println("Bird is moving"); } } class Ostrich extends Bird { @Override public void move() { System.out.println("Ostrich is walking"); } }
위 예제에서 Ostrich 클래스는 fly() 메서드를 사용할 수 없으므로 Bird 클래스를 상속받아서는 안 된다. 대신, move() 메서드를 정의하고 자식 클래스에서 적절하게 오버라이딩하는 방식으로 수정할 수 있다.

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

인터페이스 분리 원칙은 특정 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록, 인터페이스를 분리해야 한다는 원칙이다. 하나의 인터페이스가 너무 많은 기능을 포함하면, 이를 구현하는 클래스는 사용하지 않는 메서드를 구현해야 할 수 있다. 이는 인터페이스를 더 작은 단위로 나누어 해결할 수 있다.
  • 예시: 클라이언트가 사용하지 않는 메서드를 강제로 구현하도록 하는 큰 인터페이스는 ISP를 위반한다.
잘못된 예시:
interface Worker { void work(); void eat(); } class HumanWorker implements Worker { public void work() { System.out.println("Human is working"); } public void eat() { System.out.println("Human is eating"); } } class RobotWorker implements Worker { public void work() { System.out.println("Robot is working"); } public void eat() { // 로봇은 먹지 않으므로 불필요한 메서드 구현 } }
수정된 예시:
interface Workable { void work(); } interface Eatable { void eat(); } class HumanWorker implements Workable, Eatable { public void work() { System.out.println("Human is working"); } public void eat() { System.out.println("Human is eating"); } } class RobotWorker implements Workable { public void work() { System.out.println("Robot is working"); } }
위 예제에서는 WorkableEatable 인터페이스로 분리하여, 각각 필요한 인터페이스만 구현하도록 한다.

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

의존 역전 원칙은 고수준 모듈이 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다는 원칙이다. 또한, 추상화는 구체적인 것에 의존하지 않아야 한다. 이를 통해 모듈 간의 결합도를 낮추고, 시스템을 더 유연하게 만들 수 있다.
  • 예시: 고수준 모듈이 저수준 모듈의 구현에 의존하면 DIP를 위반하게 된다.
잘못된 예시:
class Light { public void turnOn() { System.out.println("Light is on"); } public void turnOff() { System.out.println("Light is off"); } } class Switch { private Light light; public Switch(Light light) { this.light = light; } public void operate() { light.turnOn(); } }
수정된 예시:
interface Switchable { void turnOn(); void turnOff(); } class Light implements Switchable { public void turnOn() { System.out.println("Light is on"); } public void turnOff() { System.out.println("Light is off"); } } class Switch { private Switchable device; public Switch(Switchable device) { this.device = device; } public void operate() { device.turnOn(); } }
위 예제에서 Switch 클래스는 Switchable 인터페이스에 의존하도록 수정되어, 구체적인 Light 클래스가 아닌 추상화된 인터페이스에 의존하게 된다. 이를 통해 다른 장치도 Switchable 인터페이스를 구현하면 Switch 클래스에서 사용할 수 있다.

결론

SOLID 원칙은 객체지향 설계에서 소프트웨어의 품질을 높이기 위해 반드시 지켜야 할 중요한 가이드라인이다. 이 원칙들을 적용하면 코드의 유지보수성과 확장성을 높일 수 있으며, 더 나은 소프트웨어 아키텍처를 구축할 수 있다. 그러나 모든 원칙을 무조건 적용하기보다는, 상황에 맞게 적절히 사용하는 것이 중요하다.
Share article

RottenYogurt's Development Blog