SOLID는 OOP 4대특성을 발판으로 하고, 디자인 패턴을 공부하기 전에 꼭 알아야할 SOLID 원칙에 대해 알아본다.
객체지향 프로그래밍의 의미는 '객체에게 데이터를 요구하지 말고 작업을 요구하라'라고 하는데 ..
철학을 하는건지 개발을 공부하는건지 모르겠지만 일단 SOLID 원칙에 대해 알아본다.
SRP(Single Responsibility Principle)
"어떤 클래스를 변경해야하는 이유는 오직 하나뿐이여야한다." 는 하나의 클래스는 하나의 책임만 가져야한다로 해석할 수 있다.
비단 클래스에만 국한 되는 것이 아닌 메서드도 하나의 책임(기능)만 맡도록 짜는 것이 유지보수하기에 용이하다.
이유는 해당 클래스, 메소드에 대한 변경이 필요할 때 쉽게 찾을 수 있고, 파급효과도 적기 때문이다.
이전 장에서 나온 애플리케이션 컨텍스트에 따른 모델링을 어떻게 해야하는지 알려주는 것과 같다.
예를 들면 사람이 한턴마다 걷는게임이 있다고 한다.
class Person{
public int position = 0;
}
class WalkGame{
Person walkBoy = new Person();
public void turnContinue(){
walkBoy.position++;
}
}
여기서 walkGame는 턴을 진행하는 책임뿐 아니라 게임참여자가 걷는지까지 책임을 지고 있다.
walkGame은 참여자들이 앞으로 걷든, 뒤로 걷든, 멈춰있든 턴을 진행하는 책임만 져야한다.
따라서 걷는 책임을 사람에게 넘기는 다음과 같은 코드가 클래스 당 책임을 하나를 갖고 있다고 볼 수 있다.
class Person{
private int position = 0;
public void walk(){ // 뒤로 걷는 사람
this.position--;
}
}
class WalkGame {
Person walkBoy = new Person();
public void turnContinue() { // 사람이 어떻게 걷든 게임 진행
walkBoy.walk();
}
}
OCP(Open Colose Principle)
"SW Entity(클래스, 모듈, 함수)는 확장엔 열려있어야하지만, 변경엔 닫혀있어야 한다."
조금 더 의역을 해보자면 "자신의 확장에는 열려있지만, 주변의 변화에 대해서는 닫혀있어야 한다."를 이끌어 낼 수 있다.
interface Car{ // 차종에 상관없이 앞 뒤로 가는법만 알면됨
void 전진();
void 후진();
}
class Sonata implements Car{ // 소나타는 한 칸씩
private int go = 0;
@Override
public void 전진(){
go++;
}
@Override
public void 후진(){
go--;
}
}
class Benz implements Car{ // 벤츠는 두 칸씩
private int go = 0;
@Override
public void 전진(){
go+=2;
}
@Override
public void 후진(){
go-=2;
}
}
예를 들면 인터페이스의 사용이 있다.
인터페이스의 확장인 구현체의 한계치는 없지만, 사용자가 인터페이스의 사용법만 알면 어떤 구현체가 오든 상관이 없다는 점이 자신의(인터페이스)의 확장에는 열려있고 주변(구현체)의 변경에는 닫혀 있다고 할 수 있다.
애플리케이션을 설계할 때 OCP를 지키는 설계를 하기위해 상속, 인터페이스를 통해 추상화, 다형성을 갖게끔하고, 주변의 변화로 인해 자신까지의 변경을 일으킬 수 있는 전역변수나 setter 등의 사용을 자제해야 한다.
LSP(Lisckov Substitution Principle)
서브타입은 언제나 자신의 기반타입으로 교체할 수 있어야 한다.
상속을 분류처럼 사용한다면 문제가 되지 않지만, 조직도, 계층도처럼 사용하면 문제가 될 수 있다.
이전에 붕어빵과 붕어빵틀이 클래스와 객체, 상속의 예시로 알맞지 않다는 것과 같은 얘기인데,
붕어빵은 붕어빵틀로 치환될수없지만, 강호동은 사람으로 치환될 수 있다.
상속을 통한 재사용을 할 때는 기반 클래스와 서브 클래스 사이가 Is a 관계가 있을 경우로만 제한되어야 한다.
그 외에 경우는 합성을 이용한 재사용을 해야한다.
ISP(Interface Segragation Principle)
클라이언트는 자신이 사용하지 않는 메서드에 의존관계를 맺으면 안된다.
SRP와 달리 단일 책임을 갖는 클래스로 분할하기보다 역할에 맞는 부분들을 인터페이스로 분할해 사용한다.
같은 문제에 대한 다른 해결책이라고 볼 수 있는데 인터페이스로 분할할 때는 인터페이스 최소 원칙주의에 따라 가능한 최소한의 부분만 제공하는 부분도 생각을 해야한다.
SRP는 분류(클래스)의 책임을 나누고, ISP는 행동(메서드)의 책임을 나눈다고 생각한다.
사칙연산이 묶여있는 Operator 인터페이스를 정의하고 계산기를 구현하면 사칙연산 모두를 재정의해야한다.
interface Operator{
int plus(int a, int b);
int minus(int a, int b);
int multi(int a, int b);
int divide(int a, int b);
}
class Calculator implements Operator{
@Override
public int plus(int a, int b) {
return a+b;
}
@Override
public int minus(int a, int b) {
return a-b;
}
@Override
public int multi(int a, int b) {
return a*b;
}
@Override
public int divide(int a, int b) {
return a/b;
}
}
억지이긴 하지만 더하기가 없는 계산기를 만들고 싶을 땐 Operator가 많은 책임을 지고 있어 인터페이스를 분리하는 편이 좋다.
interface Plus{
int plus(int a, int b);
}
interface Minus{
int minus(int a, int b);
}
interface Multi {
int multi(int a, int b);
}
interface Divide{
int divide(int a, int b);
}
class CalculatorWithOutPlus implements Minus, Multi, Divide{ // 필요한 만큼 인터페이스 구현
@Override
public int minus(int a, int b) {
return a-b;
}
@Override
public int multi(int a, int b) {
return a*b;
}
@Override
public int divide(int a, int b) {
return a/b;
}
}
DIP(Dependency Inversion Principle)
자신보다 변하기 쉬운 것에 의존하던 것을 추상화 된 인터페이스나 상위클래스를 두어 변하기 쉬운 하위 클래스, 모듈의 변화에 영향을 받지않게 하는 것이 의존성 역전 원칙이다.
다음과 같이 운전자가 소나타에 의존하게 설계하는 것보다
운전자는 자동차 인터페이스에 의존하게 하고, 소나타, 벤츠는 자동차 인터페이스를 구현해 사용하도록 설계해야 운전자가 갑자기 다른차를 몰게되는 변화도 쉽게 수용할 수 있게한다.
OCP와 비슷한 것 같은데 OCP는 interface(여기선 자동차)의 확장과 변경에 대해, DIP는 interface를 사용하는 사용자(여기선 운전자)의 의존성에 집중한 것 같다.
기억할 것
SRP는 분류(클래스)의 책임을 나누고, ISP는 행동(메서드)의 책임을 나눈다고 생각한다.
OCP는 interface(의 확장과 변경에 대해, DIP는 interface를 사용하는 사용자의 의존성에 집중한다고 생각한다.