객체지향을 배우다보면 마주치게 되는 SOLID 무슨 의미일까?
간단하게 말하면 객체지향 설계에서 지켜야하는 5가지 소프트웨어 개발 원칙이다.
여러 디자인패턴들의 기반이 되는만큼 알아둬야 할 가치가 있는 개념이다.
| 주문자 | 약어 |
|---|---|
| S | SRP(single responsibility principle) |
| 단일책임 원칙 | 한 클래스는 하나의 책임만 가져야한다. |
| O | OCP(open/closed principle) |
| 개방/폐쇄 원칙 | 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다. |
| L | LSP(liskov substitution principle) |
| 리스코프 치환 원칙 | 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으며 하위타입의 인스턴스로 바꿀 수 있어야 한다. |
| I | ISP(interface segregation principle) |
| 인터페이스 분리 원칙 | 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다. |
| D | DIP(dependency inversion principle) |
| 의존관계 역전 원칙 | 프로그래머는 추상화에 의존해야지 구체화에 의존하면 안된다. 의존성 주입은 이 원칙을 따르는 방법 중 하나다. |
한 클래스는 하나의 책임(기능)만 가져야 한다.
내가 작성한 코드를 참고해보면,
[계산을 담당하는 Calculate 클래스]
public class Calculate {
public int calculateSum(int firstNum, int secondNum) {
int sum = firstNum + secondNum;
return sum;
}
}
[계산기의 입출력을 담당하는 View 클래스]
public class View {
int firstNum;
int secondNum;
public int[] insertNumbers() {
Scanner sc = new Scanner(System.in);
System.out.print("정수 두 개를 입력해주세요: ");
firstNum = sc.nextInt();
secondNum = sc.nextInt();
int[] numbers = {firstNum, secondNum};
return numbers;
}
public void printSum(int sum) {
System.out.println("결과는 " + sum);
}
}
연산 기능과 입출력을 담당하는 기능을 클래스를 분리함으로써 기능 수정으로 인한 연쇄작용을 예방할 수 있다. 한마디로 유지보수가 용이해진다.
소프트웨어 요소는 변경에는 닫혀있어야하지만 확장에는 열려있어야 한다는 원칙이다.
확장(기능 추가 등)은 손쉽게 할 수 있어야 하지만, 그 과정에서 기존의 코드의 수정은 되도록 하지 말아야 한다는 원칙이다.
[Animal.java]
public abstract class Animal {
public abstract void sayHello();
}
[Cat.java]
public class Cat extends Animal {
@Override
public void sayHello() {
System.out.println("meow i'm cat");
}
}
위 코드를 보면 추상화 클래스 Animal을 상속받은 Cat은 기능 확장을 손쉽게 하고 있고 Animal 코드의 수정은 하지 않고 있다. ocp 원칙을 지킨 코드라고 볼 수 있다.
자료형 A가 자료형 B의 하위형이면, 자료형 B의 객체는 자료형 A의 객체로 교체할 수 있어야 한다는 원칙이다.
자바에서 예시를 찾아보면 상속관계에서 부모 클래스를 자식 클래스로 교체할 수 있어야 LSP 원칙을 지켰다고 볼 수 있다.
[Shape.java]
public class Shape {
private int width;
private int height;
public void setWidth(int width) {
this.width = width;
}
public int getWidth() {
return width;
}
public void setHeight(int height) {
this.height = height;
}
public int getHeight() {
return height;
}
public int getArea() {
return width * height;
}
}
[Retangle.java]
public class Rectangle extends Shape {
public Rectangle(int width, int height) {
setWidth(width);
setHeight(height);
}
}
[Square.java]
public class Square extends Shape{
public Square(int length) {
setWidth(length);
setHeight(length);
}
}
[Main.java]
public class Main {
public static void main(String[] args) {
Shape rectangle = new Rectangle(10, 5);
Rectangle rectangle2 = new Rectangle(10, 5);
Shape square = new Square(5);
Square square2 = new Square(5);
System.out.println(rectangle.getArea()); //50 출력
System.out.println(rectangle2.getArea()); //50 출력
System.out.println(square.getArea()); //25 출력
System.out.println(square2.getArea()); //25 출력
}
}
인터페이스는 가능한 작게 최소한의 기능만 담게 잘라야 한다는 원칙이다. 어느정도로 작게인지를 생각해보면 그 인터페이스를 사용하는 객체가 원하는 기능만을 담고 있을 정도로 작게라고 나는 생각한다.
ISP를 지킬 경우 불필요한 코드 작성을 줄일 수 있으며 인터페이스 수정 시에 연쇄적으로 이어지는 코드 수정을 줄일 수 있다.
고수준 모듈(인터페이스/추상 클래스)은 저수준 모듈(객체)에 의존해서는 안되는데 그를 위해서는 구체화 말고 추상화에 집중해야 한다고 말하는 원칙이다.
더 자세히 얘기해보면 저수준 모듈이 고수준 모듈의 추상 타입에 의존해야 한다는 말이다.
일상생활에서 예시를 들어보면,
사람이 노란색 모자를 쓴다고 할 때 Person이 yellowHat이라는 구체적인 객체에 의존하면 DIP을 지키지 못한 것이다.
public class Person {
private YellowHat hat;
public void wearYellowHat(YellowHat hat) {
this.hat = hat;
}
}
위 코드를 봐보면 빨간색이나 초록색 등 다른 색 모자를 쓰고 싶으면 Person에 GreenHat, RedHat 등의 요소와 wearGreenHat() 등의 메소들을 추가해주는 등 코드 수정을 해줘야 한다.
DIP을 지켜서 YellowHat, RedHat 등 구체적인 객체말고 Hat이라는 추상 클래스와 Person이 의존 관계를 갖고 YellowHat, RedHat 등의 구체적인 객체들도 Hat에 의존하면 Person의 수정없이 구체적인 객체를 추가해줄 수 있다.