3부 설계원칙에서는 "SOLID 원칙"에 대한 이야기이다.
SOLID 원칙은 함수와 데이터 구조를 클래스로 배치하는 방법, 그리고 이들 클래스를 서로 결합하는 방법을 설명해준다.
또한, SOLID 원칙의 목적은 다음과 같다.
SRP는 흔히 "모든 모듈이 단 하나의 일만 해야 한다" 라는 의미로 받아들이기 쉽다. 나또한 지금까지 이렇게 이해하고 있었지만 책에서 말하고자하는바는 조금 다르다. 저 말은 리팩터링하는 더 저수준에서 사용되며 책에서는 SRP를 아래와 같이 정의한다.
"하나의 모듈은 하나의, 오직 하나의 액터에 대해서만 책임져야 한다"
예를들어 급여 어플리케이션을 회계팀, 인사팀, 데이터베이스 관리자가 사용한다고 가정해보자.
여기서 액터는 회계팀, 인사팀, 데이터베이스 관리자가 되겠다.
그리고 아래와 같은 Employee 클래스가 있다.
public class Employee {
public void calculatePay() {
// 회계팀에서 기능을 정의하며, CFO 보고를 위해 사용한다.
}
public void reportHours() {
// 인사팀에서 기능을 정의하고 사용하며, COO 보고를 위해 사용한다.
}
public void save() {
// 데이터베이스 관리자가 기능을 정의하고, CTO 보고를 위해 사용한다.
}
}
Employee 클래스는 SRP 원칙을 위반하고 있는데, 해당 클래스가 3명의 액터를 책임지고 있기때문이다.
SRP 원칙을 준수한다면 모듈(혹은 코드)는 하나의 액터에 대해서만 책임져야한다.
즉 회계팀, 인사팀, 데이터베이스 관리자별로 클래스 혹은 모듈을 나누어야한다.
따라서 다음과 같은 예제로 SRP 원칙을 준수하는 형태로 바꾸고자한다.
public class Employee {
private String name;
private int age;
}
public class PayCalculator {
public void calculatePay(Employee employee) {
// 회계팀에서 기능을 정의하며, CFO 보고를 위해 사용한다.
}
}
public class HourReporter {
public void reportHours(Employee employee) {
// 인사팀에서 기능을 정의하고 사용하며, COO 보고를 위해 사용한다.
}
}
public class EmployeeSaver {
public void save(Employee employee) {
// 데이터베이스 관리자가 기능을 정의하고, CTO 보고를 위해 사용한다.
}
}
Employee 클래스가 가지고 있던 메소드를 각기 다른 클래스로 이동시키고 Employee 클래스는 데이터 구조로 사용한다.
이렇게 함으로써 단점또한 생기게 되는데, 클라이언트쪽에서 알아야될 클래스 수가 늘어나게 된다.
이에 대한 해결 방안으로는 퍼사드 패턴을 사용하는 방법이 있다.
즉, 클라이언트는 3개의 클래스를 모르더라도 하고싶은 "행동" 위주로 로직을 진행할 수 있다.
public class EmployeeFacade {
private final PayCalculator payCalculator;
private final HourReporter hourReporter;
private final EmployeeSaver employeeSaver;
public EmployeeFacade() {
this.payCalculator = new PayCalculator();
this.hourReporter = new HourReporter();
this.employeeSaver = new EmployeeSaver();
}
public void calculatePay(Employee employee) {
this.payCalculator.calculatePay(employee);
}
public void reportHours(Employee employee) {
this.hourReporter.reportHours(employee);
}
public void save(Employee employee) {
this.employeeSaver.save(employee);
}
}
SRP 원칙에 대해서 마무리하며, "액터"의 의미에 대해서 다시 한번 생각해보자.
그리고 앞으로 프로그래밍하면서 "하나의 액터가 책임질만한 범위 만큼 코드를 잘 분리하였는가?"를 고민하자.
개방 폐쇄 원칙은 다음과 같의 정의한다.
"소프트웨어 개체는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다"
OCP의 목표는 다음과 같다.
이러한 목표를 이루기 위해선
즉 의존성과 책임을 생각해서 잘 분리하고 잘 결합하도록 해야한다.
바바라 리스코프는 하위 타입을 아래와 같이 정의했다.
"S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2가 있고, T 타입을 이용해서 정의한 모든 프로그램 P에서 o2의 자리에 o1을 치환하더라도 P의 행위가 변하지 않는다면, S는 T의 하위 타입이다."
즉 서브 타입은 언제나 기반 타입으로 교체가 가능해야한다.
여기서 교체란 의미는 기반 타입 'A'의 서브 타입 'AA'를 쓰다가, 다른 서브 타입인 'AB'로 코드를 변경하더라도 문제가 없어야함을 말한다.
LSP 원칙의 대표적인 예시는 자바의 컬렉션 프레임워크 이다.
만일 변수에 LinkedList<> 타입을 사용하다가, 중간에 전혀 다른 HashSet<> 타입으로 바꿔도 add() 메소드를 보장받기 위해 Collection 이라는 인터페이스 타입으로 변수를 선언한다.
왜냐하면 인터페이스 Collection의 추상 메소드를 각기 하위 자료형 클래스에서 implements하여 인터페이스 구현 규약을 잘지키도록 설계되어 있기 때문이다.
public void sample() {
Collection<String> strings = new LinkedList<>();
strings.add("예제1");
...
// 중간에 다른 타입으로 변경하더라도 add() 메소드 동작을 보장한다.
strings = new HashSet<>();
strings.add("예제2");
}
인터페이스 분리 원칙은 다음과 같다.
"자신이 사용하지 않는 메소드에 의존 관계를 맺지 않고 분리 시켜야한다."
또한, 다음과 같은 특징도 지니게된다.
아래 예시는 ISP 원칙을 위반한 예시이다.
public interface Animal {
void eat();
void speck();
}
public class Dog implements Animal {
@Override
public void eat() {
System.out.println("간식 먹기");
}
@Override
public void speck() {
System.out.println("멍멍");
}
}
public class Fish implements Animal {
@Override
public void eat() {
System.out.println("먹기");
}
@Override
public void speck() {
// 생선은 말할 수 없음
// 해당 메소드 불필요
}
}
Animal 인터페이스를 상속받는 Dog, Fish 클래스가 있다.
Dog는 말하기(짖기)가 가능하지만, Fish는 말하기가 불가능하다.
그래서 Fish 클래스에 speck() 메소드는 불필요하다.
또한 Animal 인터페이스의 speak() 메소드명이 변경되어도 Fish 클래스에 영향이 끼치게 되는데, 전혀 관계없는 기능의 변경에도 영향을 받는셈이다.
따라서 아래와 같이 필요한 기능별로 인터페이스를 분리하자.
public interface Animal {
void eat();
}
public interface Speakable {
void eat();
}
public class Dog implements Animal, Speakable {
@Override
public void eat() {
System.out.println("간식 먹기");
}
@Override
public void speck() {
System.out.println("멍멍");
}
}
public class Fish implements Animal {
@Override
public void eat() {
System.out.println("먹기");
}
}
의존성 역전 원칙이 추구하는바는 다음과 같다.
"의존성은 추상에 의존해야 하며, 구체에 의존하지 않아야 한다."
이 규칙을 완전히 지키는것은 비현실적이다. 소프트웨어 시스템은 구체적인 장치에 반드시 의존하기 때문이다.
우리가 의존하지 않도록 피하고자 하는 것은 변동성이 큰 구체적인 요소다. 그리고 이 구체적인 요소는 우리가 개발하는 중이라 자주 변경될 수밖에 없는 모듈들이다.
즉, 변동성이 큰 구현체에 의존하는 일은 지양하고, 안정된 추상 인터페이스를 선호해야한다.
조금더 구체적인 실현법은 다음과 같다.
모두 의존성을 낮추는 것이 목표이다.
클린아키텍쳐 도서
https://dailyheumsi.tistory.com/237?category=967637