객체지향 개발 5대 원리 : SOLID

정찬's avatar
Oct 19, 2024
객체지향 개발 5대 원리 : SOLID
 

SOLID란?

 
2000년대 초 로버트 마틴이 고안해 낸 좋은 객체지향 소프트웨어를 설계할 때 지켜야 하는 5가지 원칙이다.
로버트 마틴이라는 아저씨는 클린코드, 클린 아키텍처로 유명한 사람이기도 하고,
여러 디자인 패턴이 SOLID 설계 원칙에 입각해서 만들어졌기 때문에 이미 SOLID는 많은 사람들에게 입증되어 사용되고 있다.
 
SRP
Single Responsibility Principle
OCP
Open Closed Principle
LSP
Liskov Substitution Principle
ISP
Interface Segregation Principle
DIP
Dependency Inversion Principle
 
좋은 설계란 시스템에 새로운 요구사항이나 변경사항이 있을 때, 영향을 받는 범위가 적은 구조를 말한다.
즉, SOLID 객체 지향 원칙을 적용하면 코드를 확장하고 유지 보수 관리하기가 더 쉬워지며, 불필요한 복잡성을 제거해 리팩토링에 소요되는 시간을 줄임으로써 프로젝트개발의 생산성을 높일 수 있다.
 

SRP : 단일 책임 원칙

 
클래스는 단 하나의 책임만 가져야 한다.
 
로버트 마틴에 의하면 책임 이라는 것은 변경의 사유가 된다고 한다.
즉 클래스가 변경되는 사유는 단 하나어야 된다는 것이다.
만약 하나의 클래스가 여러가지 일을 하도록 설계된다면, 코드의 복잡도가 증가하고 유지보수가 어려워지며, 한 기능의 변화가 다른 기능에 영향을 미치는 문제가 발생할 수 있다.
 

SRP 위반 사례

 
public class Employee { private String name; private double salary; public Employee(String name, double salary) { this.name = name; this.salary = salary; } public double calculateSalary() { // 급여 계산 로직 return this.salary; } public void saveToDatabase() { System.out.println("Saving employee to database"); } public void printReport() { System.out.println("Printing employee report"); } }
 
Employee 클래스는 급여 계산, 데이터베이스 저장, 보고서 출력이라는 세가지 책임을 가지고 있다. 즉 각 기능의 변경 사항이 생기면 이 클래스의 여러 부분을 수정해야하며, 유지보수가 어려워 진다.
 

SRP 적용하기

 
public class Employee { private String name; private double salary; public Employee(String name, double salary) { this.name = name; this.salary = salary; } public double calculateSalary() { return this.salary; } } public class EmployeeRepository { public void save(Employee employee) { System.out.println("Saving employee to database"); } } public class EmployeeReport { public void print(Employee employee) { System.out.println("Printing employee report"); } }
 
여러개의 책임을 갖는 Employee 클래스를 Employee, EmployeeRepository, EmployeeReport 클래스로 책임을 분리했다.
이제 클래스들이 각자 하나의 책임만 가지므로 변경 시 서로 간섭 없이 수정이 가능하며, 코드의 가독성과 유지보수성이 향상된다.
 

OCP : 개방 폐쇄 원칙

 
클래스는 확장에 열려있어야 하며, 수정에는 닫혀있어야 한다
 
기능을 변경 또는 확장할 수 있으면서 그 기능을 사용하는 코드는 수정하지 않아야 한다.
기능이 추가될 것이라 예상되는 메소드나 클래스는, 추상화(인터페이스 화)하여 구현한다. 새로운 기능이 추가 되었을 때 해당 인터페이스를 주입한다면 원래 코드는 손대지 않을 수 있다.
 

OCP 위반 사례

 
class Rectangle { public double width; public double height; public Rectangle(double width, double height) { this.width = width; this.height = height; } } class Circle { public double radius; public Circle(double radius) { this.radius = radius; } } class AreaCalculator { public 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; } }
 
만약 새로운 도형(예: 삼각형)이 추가된다면, AreaCalculator 클래스를 수정해야만 새로운 도형의 면적을 계산할 수 있다.
때문에 이 코드는 OCP원칙을 위반하고 있기 때문에 이렇게 되면 확장이 어렵고, 변경할 때마다 기존 클래스에 영향을 주게 된다.
 

OCP를 적용하여 해결

 
interface Shape { double calculateArea(); } class Rectangle implements Shape { private double width; private double height; public Rectangle(double width, double height) { this.width = width; this.height = height; } @Override public double calculateArea() { return width * height; } } class Circle implements Shape { private double radius; public Circle(double radius) { this.radius = radius; } @Override public double calculateArea() { return Math.PI * radius * radius; } } // 면적 계산기 class AreaCalculator { public double calculateArea(Shape shape) { return shape.calculateArea(); } }
 

OCP 원칙 적용 주의점

 
확장에는 열려있고 변경에는 닫히게 하기 위해서는 추상화를 잘 설계할 필요성이 있는데, 추상화를 정의할 때 여러 경우의 수에 대한 고려와 예측이 필요하다.
 

LSP : 리스코브 치환의 법칙

 
서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다.
 
서브 타입은 기반 타입이 약속한 규약을 지켜야 한다.
LSP는 다형성 원리를 이용하기 위한 원칙 개념이라고 보면 된다.
리스코프 치환 원칙의 핵심은 부모 클래스의 행동 규약을 자식 클래스가 위반하면 안 된다는 것이다.
행동 규약을 위반한다는 것은 자식 클래스가 오버라이딩을 할 때, 잘못되게 재정의하면 리스코프 치환 원칙을 위배할 수 있다는 의미이다.
 
 

LSP 위반 사례

 
class Rectangle { private int width; private int height; public void setWidth(int width) { this.width = width; } public void setHeight(int height) { this.height = height; } public int getWidth() { return width; } public int getHeight() { return height; } public int getArea() { return width * height; } } class Square extends Rectangle { @Override public void setWidth(int width) { super.setWidth(width); super.setHeight(width); // 정사각형이므로 높이와 너비는 동일해야 함 } @Override public void setHeight(int height) { super.setHeight(height); super.setWidth(height); // 정사각형이므로 너비와 높이는 동일해야 함 } }
 
public class Main { public static void main(String[] args) { Rectangle rect = new Rectangle(); rect.setWidth(5); rect.setHeight(10); System.out.println("Rectangle area: " + rect.getArea()); // 50 Rectangle square = new Square(); square.setWidth(5); square.setHeight(10); // 정사각형은 높이와 너비가 같아야 하므로 예상치 못한 결과 발생 System.out.println("Square area: " + square.getArea()); // 예상치 못한 값 } }
 
위 코드에서 정사각형의 면적은 10이 나와야 하지만, setHeight(10)이 호출되면 정사각형의 너비도 10으로 변경되어 잘못된 결과를 얻게 된다.
 

LSP를 적용하여 해결

 
// 공통 Shape 인터페이스 정의 interface Shape { int getArea(); } // 사각형 클래스 class Rectangle implements Shape { private int width; private int height; public Rectangle(int width, int height) { this.width = width; this.height = height; } public int getWidth() { return width; } public int getHeight() { return height; } @Override public int getArea() { return width * height; } } // 정사각형 클래스 class Square implements Shape { private int side; public Square(int side) { this.side = side; } public int getSide() { return side; } @Override public int getArea() { return side * side; } }
 

ISP : 인터페이스 분리의 법칙

 
클래스는 자신이 이용하지 않는 기능의 변경에 영향을 받으면 안된다.
 
SRP 원칙이 클래스의 단일 책임을 강조한다면, ISP는 인터페이스의 단일 책임을 강조하는 것이다.
 

ISP 위반 사례

 
interface Worker { void work(); void eat(); void sleep(); } class Human implements Worker { public void work() { System.out.println("Working..."); } public void eat() { System.out.println("Eating..."); } public void sleep() { System.out.println("Sleeping..."); } } class Robot implements Worker { public void work() { System.out.println("Working..."); } public void eat() { // 로봇은 먹지 않음 } public void sleep() { // 로봇은 자지 않음 } }
이 예시에서 Robot 클래스는 eat()과 sleep() 메소드를 구현해야 하지만, 실제로는 이 기능들을 사용하지 않는다.

ISP를 적용하여 해결

 
interface Workable { void work(); } interface Eatable { void eat(); } interface Sleepable { void sleep(); } class Human implements Workable, Eatable, Sleepable { public void work() { System.out.println("Working..."); } public void eat() { System.out.println("Eating..."); } public void sleep() { System.out.println("Sleeping..."); } } class Robot implements Workable { public void work() { System.out.println("Working..."); } }
ISP를 적용해 하나의 인터페이스를 Workable, Eatable, Sleepable로 분할하였다.
이렇게 인터페이스를 분리함으로써, Robot 클래스는 필요한 work() 메소드만 구현하면 된다.
 

DIP : 의존성 역전의 법칙

 
고수준 모듈이 저수준 모듈의 세부 구현에 의존하지 않고, 추상화에 의존해야 한다
 
DIP는 의존성 역전 원칙으로, 고수준 모듈이 저수준 모듈의 구체적인 구현에 의존하지 않고 추상화에 의존해야 한다는 원칙이다.
이 원칙은 코드의 결합도를 낮추고 유연성을 높이는 데 도움을 준다.
DIP를 적용하면 코드의 재사용성이 증가하고, 시스템의 유지보수가 용이해진다.
또한, 이 원칙은 테스트하기 쉬운 코드를 작성하는 데에도 도움을 준다.

DIP 위반 사례

class LightBulb { public void turnOn() { System.out.println("LightBulb: Bulb turned on..."); } public void turnOff() { System.out.println("LightBulb: Bulb turned off..."); } } class ElectricPowerSwitch { public LightBulb bulb; public boolean on; public ElectricPowerSwitch(LightBulb bulb) { this.bulb = bulb; this.on = false; } public boolean isOn() { return this.on; } public void press() { boolean checkOn = isOn(); if (checkOn) { bulb.turnOff(); this.on = false; } else { bulb.turnOn(); this.on = true; } } }
이 예시에서 ElectricPowerSwitch 클래스는 LightBulb 클래스에 직접적으로 의존하고 있다.

DIP를 적용하여 해결

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 ElectricPowerSwitch { public Switchable device; public boolean on; public ElectricPowerSwitch(Switchable device) { this.device = device; this.on = false; } public boolean isOn() { return this.on; } public void press() { boolean checkOn = isOn(); if (checkOn) { device.turnOff(); this.on = false; } else { device.turnOn(); this.on = true; } } }
DIP를 적용하여 ElectricPowerSwitch가 Switchable 인터페이스에 의존하도록 변경하였다.
이렇게 함으로써 ElectricPowerSwitch는 구체적인 구현(LightBulb)이 아닌 추상화(Switchable)에 의존하게 된다.
이제 ElectricPowerSwitch는 LightBulb 뿐만 아니라 Switchable 인터페이스를 구현한 어떤 장치와도 작동할 수 있게 되었다.

References

 
 
Share article

lushlife99