
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