느리더라도 꾸준하게

안녕하세요? 제이온입니다.

 

이번 시간부터는 SOLID의 원칙 하나씩 알아보려고 합니다. 그렇다면, 이 SOLID 원칙이란 무엇일까요?

 

 

SOLID 원칙

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

 

 

단일 책임 원칙 (Single Responsibility Principle)의 정의

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

 

 

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

 

 

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

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) {
       ...// 파싱 처리 코드
    }
    ...// 기타 필드 등 다른 코드
}

 

 

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

 

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

 

 

public class DataViewer {
    public void display() {
        byte[] data = loadHtml();
        updateGui(data);
    }
    
    public byte[] loadHtml() {
        SocketClient client = new SocketClient();
        client.connect(server, port);
        return client.read;
    }
    
    private void updateGui(byte[] data) {
        GuiData guiModel = parseDataToGuiData(data);
        tableUI.changeData(guiModel);
    }
    
    private GuiData parseDataToGuiData(byte[] data) {
       ...// 파싱 처리 코드
    }
    ...// 기타 필드 등 다른 코드
}

 

 

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

 

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

 

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

 

 

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

 

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

 

 

책임은 무엇일까? - 1

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

 

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

 

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

 

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

 

DataViewer 클래스를 다시 봅시다.

 

 

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) {
       ...// 파싱 처리 코드
    }
    ...// 기타 필드 등 다른 코드
}

 

 

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

 

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

 

 

책임은 무엇일까? - 2

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

 

1. 클래스가 여러 가지의 (public) 메소드를 가진다면, 복수의 책임을 갖는가?

 

2. 클래스가 다중 상속 (혹은 다중 구현)을 한다면, 복수의 책임을 갖는가?

 

3. 해당 클래스를 의존하는 사용자(클라이언트)가 여럿이라면 변경되는 이유는 여러가지가 되는가?

 

 

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

 

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

 

 

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

 

 

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

 

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

 

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

 

 

위의 스마트폰 객체는 철수와 영희가 다른 방식으로 변경되기를 원할 수 있기 때문에 철수와 영희는 별 개의 액터입니다. 철수가 만약 영상 시청을 위해서 스마트폰의 액정 크기를 변경한다면 전화 통화 요구 사항에는 맞지 않는 변경 사항이 됩니다. 그러므로 해당 스마트폰이 단일 책임 원칙을 지키기 위해서는 다른 액터로 분리되어야 합니다.

 

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

 

 

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

 

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

 

 

정리

지금까지 단일 책임 원칙에 대해서 알아 보았습니다. 정의 자체는 간단하지만, 그 안에 '책임'이라는 기준을 세운다는 것이 굉장히 어렵다고 생각합니다. 이를 위해서는 단순히 해당 클래스만을 바라보기 보다면, 시야를 넓혀서 액터를 정의하는 것이 중요하겠습니다.

 

 

출처

개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴 - 최범균

 

 

SRP(단일 책임 원칙)의 '단일 책임'이란 무엇일까?

SRP (Single Responsibility Principle - 단일 책임 원칙) 클래스는 단 한 개의 책임을 가져야 한다. 클래스가 변경되는 이유는 단 한 개여야 한다. S-O-L-I-D로 유명한 다섯가지 객체지향 설계 원칙중에서 SRP

siyoon210.tistory.com

 

donaricano-btn

이 글을 공유합시다

facebook twitter kakaoTalk kakaostory naver band

본문과 관련 있는 내용으로 댓글을 남겨주시면 감사하겠습니다.

비밀글모드

  1. 감사링감사띠
    감사링 감사띠
    2021.07.18 16:21