객체 지향 프로그래밍에서 SOLID
원칙은 빼놓을 수 없다. 더 나은 객체 지향 프로그래밍 코드를 구현하기 위해서는 이 원칙의 적용이 필수적이기 때문이다. 각각 SRP
OCP
LSP
ISP
DIP
로 이루어져 있다.
Single Responsibility Principle
이며, 흔히 단일 책임 원칙이라고 한다. 객체지향 프로그래밍에서 책임 소재는 클래스에게 있으며, 하나의 클래스에는 하나의 책임만 존재해야한다는 의미이다.
책임의 범위는 기능을 추가하거나, 변경시에 파급효과로 확인하는게 맞다.
아래와 같은 클래스를 확인해보자
public class UI {
private final BufferedReader reader;
private CookingStepsSteak cookingStepsSteak;
private CookingStepsPasta cookingStepsPasta;
private CookingStepsPizza cookingStepsPizza;
public UI(){
reader = new BufferedReader(new InputStreamReader(System.in));
cookingStepsSteak=new CookingStepsSteak();
cookingStepsPasta=new CookingStepsPasta();
cookingStepsPizza=new CookingStepsPizza();
}
public void run() throws IOException {
int menu=selectMenu();
int detailedMenu=selectDetailMenu(menu);
switch (menu){
case 1:
cookingStepsSteak.takeCookingSteakSteps(detailedMenu);
break;
case 2:
cookingStepsPasta.takeCookingPastaStpes(detailedMenu);
break;
case 3:
cookingStepsPizza.takeCookingPizzaSteps(detailedMenu);
break;
}
}
private int selectMenu() throws IOException {
printSelectMenu("안녕하세요! 어떤 파트의 음식을 드시고 싶으세요?", "1: Steak요리 || 2: Pasta요리 || 3: Pizza요리");
return Integer.parseInt(reader.readLine());
}
private int selectDetailMenu(int mainMenu) throws IOException {
int detailMenu = 0;
switch (mainMenu){
case 1:{
printSelectMenu("Steak 중 어떤 Steak 메뉴를 고르시겠어요?", "1: T-Born Steak || 2: Sir-loin Steak || 3: Rib-eye Steak");
detailMenu=Integer.parseInt(reader.readLine());
break;
}
case 2:
printSelectMenu("Pasta 중 어떤 Pasta 메뉴를 고르시겠어요?", "1: Tomato-Pasta || 2: Cream-Pasta || 3: Oil-Pasta");
detailMenu=Integer.parseInt(reader.readLine());
break;
case 3:
printSelectMenu("Pizza 중 어떤 Pizza 메뉴를 고르시겠어요?", "1: Peperoni-Pizza || 2: Cheeze-Pizza || 3: Gorgonzola-Pizza");
detailMenu=Integer.parseInt(reader.readLine());
break;
}
return detailMenu;
}
private void printSelectMenu(String notice, String selectedMenu) {
System.out.println(notice);
System.out.println(selectedMenu);
}
}
위 코드는 UI를 보여주는 클래스이지만, 여전히 UI를 보여주는 책임과, 숫자가 들어왔을 때, 어떤 메뉴인지를 선택하는 책임을 모두 가지고 있다.
이런 경우에, 메뉴가 추가 되면, 어떤 메뉴인지를 선택하는 책임의 코드는 변경되어야하지만, UI를 보여주는 책임은 굳이 변경되어야하나 싶다.
따라서 두개의 책임을 분리해서, 하나의 변경사항에 대한 변경 코드는 한 클래스 에서만 변경하는것이 유지 보수 관점이나, 개발자가 범할 수 있는 오류 관점에서 훨씬 나은 코드이다.
open-closd-principle
원칙으로 기존의 코드 수정에는 닫혀있어야하고, 확장에는 열려있어야 한다는 의미이다.
이를 통해서 확장성 있는 코드를 설계할 수 있다.
이는 DIP
를 통해서 지킬수 있는 원칙이다. DIP
에 종속적인 원칙이라고 생각한다.
Liscove Substition Principle
원칙으로 하위 클래스는 상위클래스의 동작에 종속되어야한다는 의미이다.
여기서 말하는 종속은 완전히 엇나가지 않아야한다는 의미임.정확히는 대체할 수 있어야한다는 의미임.
class Bird {
public void fly() {
System.out.println("This bird can fly");
}
}
class Sparrow extends Bird {
// Sparrow는 정상적으로 날 수 있음
}
class Ostrich extends Bird {
// 타조는 날 수 없음, LSP 위반
@Override
public void fly() {
throw new UnsupportedOperationException("Ostrich can't fly");
}
}
public class Main {
public static void makeBirdFly(Bird bird) {
bird.fly(); // 이 메서드는 모든 새가 날 수 있다고 가정
}
public static void main(String[] args) {
Bird sparrow = new Sparrow();
Bird ostrich = new Ostrich();
makeBirdFly(sparrow); // 정상 작동
makeBirdFly(ostrich); // 예외 발생, LSP 위반
}
}
LSP위반의 예시이다. 상위 클래스에서 예측한 동작과는 다르게 동작된다.
SRP
가 클래스에 대한 책임 소재에 따라서 나누는 것이라면, ISP(Interface-Seperate-Principle
은 인터페이스에 대해서 나누는 것이다.
인터페이스를 구현해서 클래스를 만드므로, 한 인터페이스에서 너무 많은 메서드를 정의 한다면, 유연성은 당연하게 떨어질 수 밖에 없다.
이도 마찬가지로 변경에 대한 파급효과를 생각해서 설계해야한다. 특히 인터페이스의 설계를 바꾸면, 구조 완전히 바뀌므로, 처음에 설계를 할 때 잘 적용하는게 매우 중요한 원칙이라고 생각된다.
Dependency Inversion Principle
로 구체화에 의존하지 말고, 추상화에 의존해야한다는 의미이다.
여기서 말하는 추상화는 인터페이스를 말하고 구체화는 이를 구현한 구체클래스를 말한다.
class Keyboard {
public String getInput() {
return "Keyboard input";
}
}
class Computer {
private Keyboard keyboard;
public Computer() {
this.keyboard = new Keyboard(); // 구체 클래스에 의존
}
public void input() {
System.out.println(keyboard.getInput());
}
}
public class Main {
public static void main(String[] args) {
Computer computer = new Computer();
computer.input(); // "Keyboard input" 출력
}
}
이 클래스는 Computer가 키보드라는 구체 클래스를 의존하기에, 컴퓨터가 다른 부품이 필요한 경우 컴퓨터내의 변수를 직접 바꿔야해서 OCP에 위반된다.
따라서 아래처럼 InputDevice
라는 인터페이스를 통해서 클라이언트의 코드 수정없이, 주입을 통해서 확장성있는 코드를 설계할 수 있다.
// 추상화 계층 (인터페이스)
interface InputDevice {
String getInput();
}
// 저수준 모듈 (구체 클래스)
class Keyboard implements InputDevice {
@Override
public String getInput() {
return "Keyboard input";
}
}
class Mouse implements InputDevice {
@Override
public String getInput() {
return "Mouse input";
}
}
// 고수준 모듈 (Computer는 InputDevice에 의존)
class Computer {
private InputDevice inputDevice;
// 생성자 주입 (Dependency Injection)
public Computer(InputDevice inputDevice) {
this.inputDevice = inputDevice;
}
public void input() {
System.out.println(inputDevice.getInput());
}
}
public class Main {
public static void main(String[] args) {
// 키보드 사용
InputDevice keyboard = new Keyboard();
Computer computerWithKeyboard = new Computer(keyboard);
computerWithKeyboard.input(); // "Keyboard input" 출력
// 마우스 사용
InputDevice mouse = new Mouse();
Computer computerWithMouse = new Computer(mouse);
computerWithMouse.input(); // "Mouse input" 출력
}
}
DIP를 지킴으로써 클라이언트단에서 일어나는 실수를 줄여서, 유지보수성을 지킬 수 있고, 주입을 통해서 더욱 확장성 있는 코드를 설계할 수 있다.