[SOLID] 단일 책임 원칙(SRP)

SeungHyuk Shin·2021년 9월 12일
0

SOLID 원칙

목록 보기
1/5
post-thumbnail

SOLID가 뭔데?


흔히 객체 지향 5대 원칙으로 불리는 이 SOLID 원칙은 SRP(단일 책임 원칙), OCP(개방-폐쇄 원칙), LSP(리스코프 치환 원칙), DIP(의존 역전 원칙), ISP(인터페이스 분리 원칙)을 말하며, 앞자를 딴 것으로, 프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 소프트웨어를 만드는 데 도움을 주려고 고안되었다.

단일책임 원칙의 원리


단일 책임 원칙은 "클래스는 단 한 개의 책임을 가져야 한다."를 의미하는 간단한 규칙이다. 클래스가 여러 책임을 갖게 되면 그 클래스는 각 책임마다 변경되는 이유가 발생하기 때문에 클래스가 한 개의 이유로만 변경되려면 클래스는 한 개의 책임만을 가져야한다고 한다. 이러한 이유로 이 원칙은 다른 말로 "클래스를 변경하는 이유는 단 한 개여야 한다."고도 표현한다.

그런데, 우리는 이 '책임'에 대해 명확하게 정의를 내리기 어렵다. 우선, 책임에 대해서 살펴보기 전에 단일 책임 원칙을 지키지 않았을 때의 문제점에 대해서 살펴 보자.

단일 책임 원칙을 지키지 않았을 때의 문제점


위 코드는 HTTP 프로토콜을 이용해서 데이터를 읽어 와 화면에 보여주는 기능을 한다. display() 메소드는 loadHtml()에서 읽어 온 HTML 응답 문자열을 updateGui() 메소드에 보낸다. 그리고 updateGui() 메소드는 parseDataToGuiData() 메소드를 이용해서 HTML 응답 메시지를 GUI에 보여주기 위한 GuiData 객체로 변한 뒤에 실제 tableUI를 이용해서 데이터를 보여주고 있다.

만약, 데이터를 제공하는 서버가 HTTP로 유지된다면 저렇게 여러 책임을 가져도 문제가 되지 않는다. 하지만, 데이터를 제공하는 서버가 소켓 기반의 프로토콜로 변경되었다면 어떨까? 만약, 이 프로토콜은 응답 데이터로 byte 배열로 제공한다면 대대적으로 코드의 변화가 발생한다.

LoadHtml() 메소드에서 읽어온 데이터의 구조가 String에서 byte[]로 변경되고, updateGui의 파라미터 타입이 변경되며 GuiData를 생성하는 parseDataToGuiData() 메소드의 코드가 변하게 되었다.

우리는 단지 데이터를 제공하는 서버만 달라졌을 뿐인데, 연쇄적으로 코드가 수정되었다. 이것은 책임의 개수가 많아질수록 한 책임의 기능 변화가 다른 책임에 주는 영향이 비례해서 증가하기 때문이다. 결국, 코드를 절차 지향적으로 변하게 하여 유지 보수를 엉망으로 만드는 것이다.

따라서, 데이터 GUI를 보여주는 책임을 담당하는 객체와 데이터를 읽는 책임을 담당하는 객체, 그리고 데이터 자체를 추상화한 객체 3가지를 이용하여 책임을 분리해야 한다.

단일 책임 원칙을 지키지 않았을 때의 2번째 문제점은 재사용을 어렵게 한다는 것이다. 위의 DataViewer는 데이터를 읽어오기 위한 클래스인데, 그 안에서 HTTP 연동을 위해서 HttpClient 패키지를 사용하고, 화면에 데이터를 보여주기 위해 GuiComp라는 패키지를 사용한다고 하자.

이때, 단순히 데이터만 읽어오려는 객체가 있다면, 필요 없는 GuiComp 패키지까지 필요하게 되는 문제가 발생한다. 단일 책임 원칙에 따라 책임이 분리되었다면, HttpClient 패키지만 필요하다.

책임은 무엇일까? - 1


위의 내용을 통해, 한 책임의 구현 변경에 의해 다른 책임과 관련된 코드가 변경될 가능성이 높아진다는 것을 알게 되었다. 하지만, 위의 코드에서 데이터의 읽어 오는 방식이 유지된다면 DataViewer 클래스는 수정할 필요가 없게 될 것이다. 이처럼 기능 변경 요구가 없을 때 수정에 대한 문제가 없다는 것은, 반대로 생각해 보면 책임의 단위는 변화되는 부분과 관련이 있다는 의미가 된다.

위 코드에서 DataViewer 클래스에서 데이터를 읽어 오는 기능에 변화가 발생하였는데, 이런 변화를 통해 데이터를 읽어 오는 기능이 별도로 분리되어야 할 책임이라고 이해할 수 있다.

또한, 각각의 책임은 서로 다른 이유로 변경되어야 하므로 데이터를 읽어 오는 책임의 기능이 변경될 때 데이터를 보여주는 책임은 변하면 안 된다.

그렇다면, 어떻게 서로 다른 이유로 변경되는 것을 알 수 있을까? 그것은 바로 메소드를 실행하는 것이 누구인지 살펴보는 것이다. 그리고 그 사용자들이 해당 클래스의 서로 다른 메소드들을 사용한다면 그들 메소드는 각각 다른 책임에 속할 가능성이 높고, 책임 분리 후보로 판단할 수 있게 된다.

DataViewer 클래스를 다시 보자.

여기서 A 클래스가 display() 메소드를 사용하고, B 클래스가 loadHtml() 메소드를 사용한다고 가정해 보면, A 클래스는 화면에 표시될 방식을 변경해야 할 경우 display() 메소드를 수정할 것이고, B 클래스는 읽어 오는 데이터 타입을 String이 아닌 다른 타입으로 바꿔야할 경우 loadHtml() 메소드를 수정할 것이다.

이처럼 클래스의 사용자들이 서로 다른 메소드를 사용하면 책임을 분리할 수 있을지 고민해 보면 된다.

책임은 무엇일까? - 2


책임은 변화에 대한 것임을 알게 되었다. 하지만, 코드를 보면 단일 책임은 단순히 하나의 메소드가 동작하는 기능같기도 하다. 따라서 우리는 그 외에도 아래와 같은 의문을 가질 수 있다.

  1. 클래스가 여러 가지의 (public) 메소드를 가진다면, 복수의 책임을 갖는가?
  2. 클래스가 다중 상속 (혹은 다중 구현)을 한다면, 복수의 책임을 갖는가?
  3. 해당 클래스를 의존하는 사용자(클라이언트)가 여럿이라면 변경되는 이유는 여러가지가 되는가?

단일 책임은 하나의 메소드가 하는 일이라고 생각하면, 첫 번째 생각을 할 수 있을 것이고, 특정 클래스가 여러 클래스에 상속을 받는 다면 단일 책임을 갖지 않는다고 생각할 수 있다. 마지막으로, 해당 클래스의 사용자가 여러 명이면 변경되는 이유가 여러 가지라고 판단하여 단일 책임을 갖지 않는다고 생각할 수 있다.

이 의문을 해결하는 훌륭한 정의가 있다.

하나의 모듈은 하나의, 오직 하나의 액터에 대해서만 책임져야 한다.

SOLID를 창시한 로버트C 마틴이 한 말로, SRP를 새롭게 정의하였다. 여기서 '액터'는 시스템이 동일한 방식으로 변경되기를 원하는 사용자 집단을 의미한다. 즉, 액터는 한 명일수도 있고, 여러 명이 될 수도 있는 것이다. 이렇듯, SRP를 설계할 때는 거시적인 관점에서 해당 클래스에 어떤 액터가 의존하는지 고려하는 것이 생각하는 것이 바람직하다.

액터라는 것을 예시로 들어 자세히 알아 보겠다. 예를 들어, '스마트폰'이라는 객체를 철수와 영희가 사용하고 있다고 가정해 보자. 이때, 철수는 스마트폰을 게임을 위해서 사용하고 영희는 영상 시청을 위해서 사용한다.

    class 스마트폰 implements 게임플레이어, 동영상플레이어 {
        ...
    }

위의 스마트폰 객체는 철수와 영희가 다른 방식으로 변경되기를 원할 수 있기 때문에 철수와 영희는 별 개의 액터이다. 철수가 만약 원할한 게임 플레이를 위해서 스마트폰의 CPU를 변경한다거나 영희가 영상 시청을 위하여 액정의 사이즈를 늘린다면 서로 액터에 맞지 않는 변경 사항이 된다. 그러므로 해당 스마트폰이 단일 책임 원칙을 지키기 위해서는 다른 액터로 분리되어야 한다.

하지만, 철수와 영희가 모두 같은 요구 사항으로 스마트폰을 사용한다면 철수와 영희를 하나로 액터로 볼 수 있으므로 단일 책임 원칙을 준수한다고 할 수 있다.

이제, 위의 의문점을 해결해 보자. 먼저, 두 번째와 세 번째는 액터라는 개념을 통해서 쉽게 해결이 가능하다. 다중 상속을 받더라도 액터가 그 다중 상속한 것들을 모두 사용한다면 단일 책임 원칙을 만족하는 것이고, 해당 클래스의 사용자가 여러 명이어도 모두 동일한 요구 사항으로 해당 클래스를 사용한다면 단일 책임 원칙을 준수하는 것이다.

첫 번째 의문점도 마찬가지로 서로 다른 액터가 해당 클래스의 여러 가지 메소드를 사용하는 것이 아니라면, 복수의 메소드여도 단일 책임 원칙을 지키고 있는 것이다.

0개의 댓글