📖책의 내용을 요약하니 자세한 부가설명이 궁금하시면 보시는걸 추천드립니다. 📖
SOLID 설계 원칙은 다음의 다섯 가지 원칙으로 구성된다.
- 단일 책임 원칙 (Single responsibility principle, SRP)
- 개방-폐쇄 원칙 (Open-closed principle, OCP)
- 리스코프 치환 원칙 (Liskov substituion principle, LSP)
- 인터페이스 분리 원칙 (Interface segregation principle, ISP)
- 의존 역전 원칙 (Dependency inversion principle, DIP)
이들 원칙은 서로 밀접하게 연결되어 있어서 종합적으로 이해할 수 있어야한다.
1. 단일 책임 원칙 (Single responsibility principle)
클래스는 단 한개의 책임을 가져야한다. (클래스는 하나의 메서드만 갖는다는 뜻은 아니다)
클래스가 여러 책임을 갖게 되면 각 책임마다 변경되는 이유가 생기기 하나의 책임만 가져야한다.
다른 말로 클래스를 변경하려는 이유는 단 한개여야 한다.
* 가장 어려운 원칙이다. 한 개의 책임에 대한 정의가 명확하지 않고, 다양한 경험이 있어야 책임을 도출할 수 있다.
1.1 단일 책임 원칙 위반이 불러오는 문제점
다음은 여러 책임으로 이뤄진 DataViewer 클래스코드다
class DateViewer {
public void display() {
String data = loadHtml();
updateGui(data);
}
public String loadHtml() {
HttpClient client = new HttpClient(new URL("http://www.naver.com"));
return client.getURLFile();
}
private void updateGui(String data) {
GuiData guiModel = parseDataToGuiData(data);
...
}
...
}
HTTP (String) -> Socket(byte[]) 프로토콜로 기능이 변경되면?
class DateViewer {
public void display() {
byte[] data = loadHtml();
updateGui(data);
}
public byte[] loadHtml() {
SocketClient client = new SocketClient(new URL("http://www.naver.com");
return client.read();
}
private void updateGui(byte[] data) {
GuiData guiModel = parseDataToGuiData(data);
...
}
...
}
연쇄적인 수정이 벌어진다. 책임의 개수가 많아지면 한 책임의 변화가 다른 책임에 주는 영향이 증가하는데, 이는 코드를 절차지향적으로 만들어 변경이 어렵게 한다.
1.2 책임이란 변화에 대한 것
각각의 책임은 서로 다른 이유로 변경되고, 서로 다른 비율로 변경되는 특징이 있다.
단일 책임 원칙을 지킬 수 있는 방법은 메서드의 사용자들을 확인해본다.
사용자들이 서로 다른 메서드를 사용한다면 메서드가 각각 다른 책임에 속할 가능성이 높다.
2. 개방 폐쇄 원칙 (Open-closed principle)
확장에는 열려 있어야하고, 변경에는 닫혀있어야 한다는 의미다
뜻을 풀어보면 기능을 변경하거나 확장할 수 있고, 그 기능을 사용하는 코드는 수정하지 않는다.
개방 폐쇄 원칙을 구현하는 방법은 2가지가 있다.
1. 추상화를 이용한다.
추상화를 이용하면 구현 클래스 같은 저수준이 아닌 고수준에 의존한다.
class FileService {
Downloader down;
public FileService(Downloader down) {
this.down = down;
}
public void fileDown() {
down.download();
}
}
interface Downloader {
void download();
}
class FileDownloader implements Downloader {
@Override
public void download() {
System.out.println("파일");
}
}
class SocketDownloader implements Downloader {
@Override
public void download() {
System.out.println("소켓");
}
}
2. 상속을 이용한다.
상속은 상위 클래스의 기능을 그대로 사용하면서 하위 클래스에서 일부 구현을 오버라이딩 할 수 있는 방법을 제공한다.
class MoveServie {
Move move;
public MoveServie(Move move) {
this.move = move;
}
public void run() {
move.move();
}
}
class Move {
protected void move() {
System.out.println("움직임");
}
}
class FastMove extends Move {
@Override
protected void move() {
System.out.println("빠르게 움직임");
}
}
여기서 protected 공개 범위를 가지고 있기 때문에 하위 클래스에서 재정의할 수 있다.
2.1 개방 폐쇄 원칙이 깨질 때의 주요 증상
이 원칙을 어기는 방법은 다음과 같다.
- 다운 캐스팅 : 상속을 받은 하위 클래스의 타입을 찾기위해 instanceof 를 사용하는 경우
- 비슷한 if-else 블록이 반복 : 비슷한 요구사항을 else if () 형식으로 추가할 때
2.2 개방 폐쇄 원칙은 유연함에 대한 것
개방 폐쇄 원칙은 변화되는 부분을 추상화함으로써 사용자 입장에서 변화를 고정시킨다.
변화되는 부분을 추상화하지 않으면 시간이 지날수록 기능 변경이나 확장이 어려워진다.
3. 리스코프 치환 원칙 (Liskov substitution priciple)
이 원칙은 개방 폐쇄 원칙을 받쳐 주는 다형성에 관한 원칙을 제공한다.
상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 작동해야한다
3.1 리스코프 치환 원칙을 지키지 않을 때의 문제
자주 사용되는 대표적인 예가 직사각형-정사각형 문제다.
class Rectangle {
private int width;
private int height;
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
}
직사각형 코드다. 정사각형은 직사각형을 상속받아 세터를 재정의한다.
class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setWidth(height);
super.setHeight(height);
}
}
이제 사용자 입장에서 사각형을 가져와 높이와 폭을 비교하고 높이를 더 길게 만드는 기능을 제공한다고 해보자.
class RectangleClient {
public void increseHeight(Rectangle rec) {
if (rec.getHeight() <= rec.getWidth()) {
rec.setHeight(rec.getWidth() + 10);
}
}
}
일반적인 Rectangle 이라면 사용자는 문제없이 사용할 것이다. 하지만 Square 경우 높이, 폭이 동일하게 변경되기 때문에 위의 가정은 깨진다.
직사각형-정사각형 문제는 개념적으로 상속 관계처럼 보일지라도 실제 구현에서는 상속 관계가 아니라는 것을 보여준다.
실제 구현에서는 정사각형이 직사각형을 상속받는게 아닌, 별개의 타입으로 구현해야한다.
원칙을 어기는 또 다른 흔한 예는 상위 타입에서 지정한 리턴 값의 범위에 해당되지 않는 값을 리턴하는 것이다.
3.2 리스코프 치환 원칙은 계약과 확장에 대한 것
리스코프 치환 원칙은 기능의 명세(또는 계약)에 대한 내용이다.
이와 관련해서 발생하는 위반 사례는 다음과 같다.
- 명시된 명세에서 벗어난 값을 리턴한다.
- 명시된 명세에서 벗어난 익셉션을 발생한다.
- 명시된 명세에서 벗어난 기능을 수행한다.
명세에서 벗어난 행위를 하면 이를 사용하는 사용자는 비정상적으로 동작할 수 있기 때문에, 하위 타입은 상위 타입에서 정의한 명세를 벗어나지 않게 구현해야한다.
4. 인터페이스 분리 원칙 (Interface segregation principle)
클라이언트는 자신이 사용하는 메서드에만 의존해야한다.
조금 더 쉽게 풀이하면 인터페이스는 그 인터페이스를 사용하는 클라이언트 기준으로 분리해야 한다.
* C 같이 컴파일과 링크를 직접해 주는 언어를 사용할 때 장점이 드러난다.
4.1 인터페이스 변경과 그 영향
게시글 목록, 작성, 삭제 등 모든 기능을 제공하는 ArticleService 클래스를 작성한다고 보자.
여기서 변경사항이 생겨 목록의 메서드 시그니처를 변경한다면 다시 컴파일하게 될 것이다.
문제는 ArticleService.h 파일이 변경되었기에 이 파일을 가져다쓰는 작성, 삭제도 다시 컴파일해야 한다.
관련없는 소스코드도 영향을 받게 된 것이다.
4.2 인터페이스 분리 원칙
자신이 사용하는 메서드에만 의존해야 한다는 원칙인데, C++로 개발한 게시글 관리 프로그램은 이 원칙을 지키지 않았다.
자바 언어를 사용한다면 컴파일을 통해 .class 만 생성하면 될뿐, 링크과정은 수행하지 않는다. 이런 이유로 자바 언어는 소스 재컴파일 문제가 발생하지 않는다.
용도에 맞게 인터페이스를 분리하는 것은 단일 책임 원칙과도 연결된다.
클라이언트 입장에서 사용하는 기능만 제공하도록 분리함으로써 변경의 여파를 최소화한다.
4.3 인터페이스 분리 원칙은 클라이언트에 대한 것
인터페이스를 분리하는 기준은 클라이언트다. 각 클라이언트가 사용하는 기능을 중심으로 분리함으로써, 변경의 여파가 다른 클라이언트로 퍼지는걸 최소화한다.
5. 의존 역전 원칙 (Dependency inversion principle)
고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야한다
고수준 모듈은 어떤 의미 있는 단일 기능을 제공하는 모듈이고, 저수준 모듈은 고수준 모듈의 기능을 구현한 하위 기능의 실제 구현이다.
5.1 고수준 모듈이 저수준 모듈에 의존할 때의 문제
고수준 모듈은 상대적으로 큰 틀(상위 수준)을 다룬다면, 저수준 모듈은 각 개별 요소(상세)가 어떻게 구현될지 다룬다.
저수준에 의존하게 되면 변경이 일어날 때 마다 고수준 모듈까지 영향이 미치게 된다.
5.2 의존 역전 원칙을 통한 변경의 유연함 확보
추상화를 이용하여 저수준 모듈이 고수준 모듈을 의존하게 만든다.
실제 구현이 아닌 추상화 타입에 의존하여 고수준 모듈의 변경 없이 저수준 모듈을 변경할 수 있는 유연함을 얻게 된다.
즉, 의존 역전 원칙은 이전에 언급한 리스코프 치환 원칙과 개방 폐쇄 원칙을 따르는 기반이 되어준다.
5.3 소스 코드 이존과 런타임 의존
의존 역전 원칙은 소스 코드에서의 의존을 역전 시키는 원칙이다.
class FlowController {
public void process() {
// 저수준인 실제 구현에 의존
FileDataReader reader = new FileDataReader();
}
}
이 원칙을 적용하여 수정해보자.
class FlowController {
public void process() {
// 저수준이 아닌 고수준에 의존
ByteSource reader = new FileDataReader();
}
}
// 고수준인 추상화에 의존
class FileDataReader implements ByteSource {
...
}
ByteSource 인터페이스는 고수준 모듈인 FlowController(클라이언트) 입장에서 만들어진다. 고수준이 저수준에 의존했던 상황에서 저수준이 아닌 고수준으로 의존한다하여 의존 역전 원칙이라 불린다.
소스 코드에서는 의존이 역전되었지만 런타임에서의 의존은 아래 그림처럼 고수준에서 저수준으로 향한다
이 원칙은 변경이 유연함을 확보할 수 있는 원칙이지, 런타임에서의 의존역전은 아니다.
5.4 의존 역전 원칙과 패키지
의존 역전 원칙은 타입의 소유도 역전시킨다. 타입의 소유 역전은 각 패키지를 독립적으로 배포할 수 있도록 만들어 준다. (독립적으로 배포는 jar, DLL 등의 파일로 배포를 뜻한다.)
의존 역전 원칙은 개방 폐쇄 원칙을 클래스 수준뿐만 아니라 패키지 수준까지 확장 시켜주는 디딤돌이 된다.
(참고, 클린 아키텍처)
6. SOLID 정리
한마디로 정의하면 변화에 유연하게 대처할 수 있는 설계 원칙이다.
단일 책임 원칙과 인터페이스 분리 원칙은 객체가 커지지 않도록 막아준다.
리스코프 치환 원칙과 의존 역전 원칙은 개방 폐쇄 원칙을 지원한다.
개방 폐쇄 원칙은 변화되는 부분을 추상화하고 다형성을 이용하여 기능 확장이 되고 기존 코드를 수정하지 않도록 만들어 준다. 여기서 변화되는 부분을 추상화되도록 도와주는게 의존 역전 원칙이고, 다형성을 도와주는게 리스코프 치환 원칙이다.
또한, SOLID 원칙은 사용자 입장에서의 기능 사용을 중시한다.
인터페이스 분리 원칙은 사용자 입장에서 인터페이스를 분리하고 의존 역전 원칙은 저수준 모듈을 사용하는 고수준 모듈 입장에서 추상화 타입을 도출하도록 유도한다. 리스코프 치환 원칙은 사용자에게 기능 명세를 제공하고 그 명세에 따라 구현할 것은 약속한다.
즉, SOLID 원칙은 사용자 관점에서의 설계를 지향한다.
'서적 > 객체지향과 디자인패턴' 카테고리의 다른 글
객체지향과 디자인패턴 마무리 후기 (0) | 2021.06.04 |
---|---|
챕터6, DI(Dependency Injection)와 서비스 로케이터 (0) | 2021.06.04 |
챕터4, 재사용 : 상속보단 조립 (0) | 2021.06.03 |
챕터3, 다형성과 추상 타입 (0) | 2021.06.03 |
챕터2, 객체지향 (0) | 2021.06.02 |