단일 책임 원칙(Single Responsibility Principle)

객체 지향의 기본은 객체에게 책임을 할당하는 데 있고, 단일 책임 원칙은 아래와 같이 정의한다.

객체는 단 한 개의 책임을 가져야 한다.

객체가 여러 책임을 갖게 되면 해당 객체는 각 책임마다 변경되는 이유가 발생하기 때문에, 객체가 한 개의 이유로만 변경되려면 객체는 한 개의 책임만을 가져야 한다.

단일 책임 원칙 위반이 불러오는 문제점

첫 번째 문제점은 아래와 같다.

한 책임의 구현 변경에 의해 다른 책임과 관련된 코드가 변경될 가능성이 높다는 것이다.

HTTP 프로토콜을 이용해서 데이터를 읽어와 화면에 보여주는 기능이 필요한 상황을 가정해 보자.

public class DataViewer {

  public void display() {
    String data = loadHtml();
    updateGui(data);

  }

  public String loadHtml() {
    HttpClient client = new HttpClient();
    client.connect(url);
    return client.getResponse();

  }

  private void updateGui(String data) {
    GuiData guiModel = parseDataToGuiData(data);
    tableUI.changeData(guiModel);

  }

  private GuiData parseDataToGuiData(String data) {
    ... // 파싱 처리 코드

  }

  ... // 기타 필드 등 다른 코드

}

display() 메서드는 loadHtml()에서 읽어 온 HTML 응답 문자열을 updateGui()에 보낸다. updateGui() 메서드는 parseDataToGuiData() 메서드를 이용해서 HTML 응답 메시지를 GUI에 보여주기 위한 GuiData 객체로 변환한 뒤에 실제 tableUI를 이용해서 데이터를 보여주고 있다.

DataViewer를 잘 사용하고 있는 도중에 데이터를 제공하는 서버가 HTTP 프로토콜에서 소켓 기반의 프로토콜로 변경되었고 응답 데이터는 byte 배열을 제공한다.

이러한 데이터를 읽어 오는 기능의 변화로 인해, 아래와 같이 DataViewer는 내부적으로 변화가 연쇄적으로 발생한다.

위의 그림과 같이, 연쇄적인 코드 수정은 두 개의 책임(데이터 읽는 책임, 화면에 보여주는 책임)이 한 객체에 아주 밀접하게 결합되어 있어서 발생한 증상이다.

책임의 개수가 많아질수록 한 책임의 기능 변화가 다른 책임에 주는 영향은 비례해서 증가하게 되는데, 이는 결국 코드를 절차 지향적으로 만들어 변경을 어렵게 만든다.

이에 대한 해결방안은 아래와 같다.

  1. 데이터 읽기와 데이터를 화면에 보여주는 책임을 두 개의 객체로 분리

  2. 둘 간에 주고받을 데이터를 저수준의 String이 아닌 알맞게 추상화된 타입을 사용

위와 같은 방법을 통해서, 데이터를 읽어 오는 부분의 변경 때문에 화면에 보여주는 부분의 코드가 변경되는 상황을 막을 수 있다. 요약해보면 아래 그림과 같다.

결과적으로, DataLoader 클래스가 내부적으로 구현을 변경하더라도, DataDisplayer는 영향을 받지않는다.

한 객체에 섞여 있던 책임을 두 객체로 분리함으로써 변경의 여파를 줄일 수 있게 되는 것 이다.

단일 책임 원칙을 지키지 않았을 때 발생하는 두 번째 문제는 아래와 같다.

단일 책임 원칙을 지키지 않으면 재사용을 어렵게 한다는 문제가 발생한다.

앞서 DataViewer 예제로 다시 돌아가 보자. HttpClient 패키지와 GuiComp 패키지가 각각 별도의 jar 파일로 제공된다고 가정한다.

이 상태에서 데이터를 읽어 오는 기능이 필요한 DataRequiredClient 클래스를 만들어야 한다면, 구현하기 위해 필요한 것은 DataViewer와 HttpClient jar 파일이다. 하지만, 실제로는 DataViewer가 GuiComp를 필요로 하므로 GuiComp jar 파일까지 필요하다.

즉, 실제로 사용하지 않는 기능이 의존하는 jar 파일까지 필요한 것이다.

단일 책임 원칙에 따라 책임이 분리되었다면, 의 그림과 같이 DataRequiredClient 클래스를 구현할 때에는 데이터를 읽어 오는데 필요한 dataloader 패키지와 HttpClient 패키지만 필요하며, 데이터를 읽어 오는 것과 상관없는 GuiComp 패키지나 datadisplay 패키지는 포함시킬 필요가 없어진다.

책임이란 변화에 대한 것

책임의 단위변화되는 부분과 관련되어 있다.

예를 들어, DataViewer 객체에서 데이터를 읽어 오는 기능에 변화가 발생했는데, 이런 변화를 통해 데이터를 읽어 오는 기능이 별도로 분리되어야 할 책임이라는 것을 알 수 있을 것이다.

각각의 책임은 서로 다른 이유로 변경되고, 서로 다른 비율로 변경되는 특징이 있다. 따라서, 서로 다른 이유로 바뀌는 책임들이 한 객체에 함께 포함되어 있다면 해당 객체는 단일 책임 원칙을 어기고 있다고 볼 수 있다.

그러면 어떻게 단일 책임 원칙을 지킬 수 있을까?

방법은 바로 메서드를 실행하는 것이 누구인지 확인해 보는 것이다.

위의 그림과 같이, 객체의 사용자들이 서로 다른 메서드들을 사용한다면 그들 메서드는 각각 다른 책임에 속할 가능성이 높으므로 책임 분리 후보가 된다.

Last updated