
'개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴'을 읽고 학습합니다. ( 최범균 저 )
지금까지 객체 지향의 기본 내용들인 책임 할당, 캡슐화, 다형성과 추상화 그리고 조립을 통한 재사용 등에 대해서 알아보았으며, 어떻게 객체 지향이 소프트웨어의 변경에 유연하게 대응하도록 만드는 지 학습했다.
그리고 지금부터는 객체 지향적으로 좋은 설계를 구성하기 위한 설계 원칙인 SOILD에 대해서 살펴보겠다. 그리고 이를 통해 보다 명확하게 객체 지향적 설계를 구성함에 필요한 사고에 대해서 익숙해지자.
단, 지금부터 학습할 SOLID 원칙들은 별도가 아닌 서로 밀접하게 연결되므로 종합적으로 이해하자.
단일 책임 원칙(Single responsibility principle)
→ ' 클래스는 단 한 개의 책임을 가져야 한다. '
이는 객체 지향 설계가 유연한 대응이라는 지점에서 ' 클래스의 변경의 이유는 단 한 개여야 한다. ' 는 말과 같은 의미이다.
클래스가 여러 책임을 지니게 되면, 각 책임마다 변경하는 이유가 발생하기 때문이다.
객체라는 것이 책임을 할당하는 의미를 지니기에 단일 책임 원칙은 매우 중요한 원칙이면서도 가장 어렵기도 하다.
한 개의 책임이라는 정의가 모호하고 그 핵심을 도출하기 위해서는 다양한 경험이 필요하기 때문이다.
그리고 해당 원칙이 무너지면 다른 원칙들도 효과가 반감되기에 최대한 지켜야하는 원칙으로 알아두자.
따라서 책임이라는 의미에 대한 파악이 필요한데 이에 앞서 해당 원칙을 위반한 경우 생기는 현상에 대해서 파악해보자.
1.1 단일 책임 원칙 위반이 불러오는 문제
Http 프로토콜을 이용해서 데이터를 읽고 화면에 보여주는 기능이 담긴 클래스가 있다.
public class DataVierwer {
public void display() {
String data = loadHtml();
updateGui(data);
}
public String loadHtml() {
HttpClient client = new HttpClient();
client.connect(url);
}
private void updateGui(String data) {
GuiData guiModel = parseDataToGuiData(data);
tableUI.changeData(guiModel);
}
private GuiData parsseDataToGuiData(String data) {
...// 파싱 처리 코드
}
..// 기타 필드 및 처리 코드
}
위의 클래스에서 display() 메소드부터 loadHtml() 에서 읽어 온 HTML 응답 문자열을 updateGui()에 보내고 파싱처리 로직을 통해 GUI에 보여주기 위한 기능을 담당하고 있다.
그런데 해당 클래스(DataVierwer)를 사용하다가 도중 HTTP 프로토콜에서 소켓 기반의 프로토콜로 변경되었다고 고려해보자.
이 프로토콜은 응답 데이터로 문자열이 아닌 byte 배열을 제공한다.
그럼 해당 클래스에서는 어떤 변화가 일어날까?
public class DataViewer {
public void display() {
byte[] data = load();
updateGui(data);
}
public byte[] loadHtml() {
SocketClient client = new SocketClient();
client.connect(sever, port);
return client.read();
}
private void updateGui(byte[] data) {
GuiData guiModedl = parseDataToGuiData(data);
tableUI.changeDate(guiModel);
}
private GuiData parseDataToData(byte [] data) {
파싱 코드 변경...// 파싱 처리 코드
}
...// 기타 필드 등 다른 코드
}
위의 코드처럼 데이터를 읽어오는 프로토콜의 변화는 아래와 같은 변화를 유도한다.
→ 읽어온 데이터의 구조 변화
→ updateGui() 파라미터 타입 변화
→ GuiData 생성하는 코드 변화
이처럼 연쇄적인 코드 수정은 하나의 클래스에 두 개의 책임이 부여되어서 그렇다.
( 데이터를 읽는 책임 / 화면에 보여주는 책임 )
하나의 책임의 변화가 다름 책임에 영향을 주고 있는 것이다.
따라서 이러한 현상을 해결하기 위한 방법은 하나의 클래스에 하나의 책임을 부여하는 것이다.
위의 코드에서 이를 위한 구체적인 방법은 아래와 같은 것이 있을 것이다.
→ 데이터 읽기와 데이터를 화면에 보여주는 책임을 두 개의 클래스로 분리한다.
→ 두 객체 간에 주고 받을 데이터를 String , byte 와 같은 저수준 타입이 아닌 추상화된 타입을 사용한다.

위의 그림처럼 한 개의 클래스에 섞였던 책임을 두 클래스로 분리하면 DataLoader 클래스가 내부적으로 구현을 변경하여도, DataDisplayer는 영향을 받지 않는다. 즉 특정 책임으로 인한 변경의 여파가 줄어드는 것이다.
단일 책임 원칙을 위반하면 생기는 또 다른 문제점은 재사용을 어렵게 만든다는 것이다.
앞서 하나의 클래스에서 두 개의 책임을 지니고 있던 경우를 고려해보자.

DataVierwer 클래스는 데이터를 읽어오는 책임과 GuiComp 책임을 모두 지니고 있었다.
그리고 DataRequiredClient 클래스는 데이터 읽기 기능을 재사용하기 위해서 DataViewer 클래스를 상속 받았지만, 사용하지 않는 기능에 의존하는 GuiComp 클래스까지 필요하게 된다.
만약, HttpClient 와 GuiComp 패키지가 각각 별도의 jar 파일 제공된 상황으로 고려하면 필요하지 않은 jar 파일까지 필요하게 되는 것이다. 이는 단일 책임 원칙 위반으로 재사용을 어렵게 만드는 예시이다.
따라서 아래와 같이 분리하면 데이터 읽어오는데 필요한 클래스와 패키지만 사용 가능한 구조가 된다.

1.2 책임이란 변화에 대한 것
단일 책임 원칙의 위반으로 발생하는 현상이 한 책임의 구현 변경에 의해 다른 책임과 관련된 코드가 변경될 가능성이 높아진다는 것을 살펴보았다.
위의 예시에서 만약 데이터를 불어오는 방식의 변경이 있지 않았다면, 두 개의 책임을 지니고 있던 DataViewer 클래스는 수정에 대한 문제가 유발되지 않았을 것이다.(물론 두 개의 책임을 지니고 있으니 재사용 측면에서는 여전히 문제가 있겠지만)
이를 바꿔말하면, 책임의 단위는 변경에 따른 전파 범위와 관련된다는 의미이다.
예를 들어, DataViewer 클래스에서 데이터를 읽어오는 기능에 변화가 생겼었다. 그리고 이는 변화가 필요한 부분이니 하나의 책임을 지니고 있다고 봐도 무방한 것이고 별도로 분리되어야 할 책임이라는 것을 파악할 수 있다.
때문에 각각의 책임은 자체의 기능적 이유가 아닌 것으로 변경된다면 분리할 필요성을 알려주고 있는 것이다. 예를 들어, 데이터를 읽어오는 책임의 기능이 변경될 때 데이터를 보여주는 책임은 변경되지 않으며 반대로 데이터를 테이블에서 그래프로 바꿔 보여주어도 데이터를 읽어 오는 기능이 변경될 이유가 없다. 따라서 책임에 따른 기능 변화로 변경 사항이 생기는 것이 아니라면 단일 책임 원칙을 어기고 있다고 볼 수 있다.
'Java > 객체지향' 카테고리의 다른 글
| 객체 지향과 디자인 패턴 Chapter 05 - 리스코프 치환 원칙(SOLID) (5) | 2024.11.13 |
|---|---|
| 객체 지향과 디자인 패턴 Chapter 05 - 개방 폐쇄 원칙(SOLID) (1) | 2024.11.05 |
| 객체 지향과 디자인 패턴 Chapter 04 - 재사용: 상속보단 조립 (0) | 2024.10.30 |
| 객체 지향과 디자인 패턴 Chapter 03 (1~3) - 상속, 다형성, 추상화 (0) | 2024.08.29 |
| 객체 지향과 디자인 패턴 Chapter 02 (2~6) - 객체 (1) | 2024.08.26 |
댓글