SOLID - 단일책임 원칙(Single Responsibility Principle)

 

본 글은 자바 객체지향과 디자인패턴를 읽고 개인적으로 학습한 내용을 복습하기 위해 작성된 글로 내용상 오류가 있을 수 있습니다. 오류가 있다면 지적 부탁드리겠습니다.

1. 책임의 의미

객체 지향 설계관점에서는 SRP에서 말하는 책임의 기본단위는 객체를 말한다. 즉, 객체는 단 하나의 책임만을 가져야한다는 의미이다.

그렇다면 책임이란 무엇일까? 책임을 여러관점에서 해석할 수 있지만 보통 “해야 하는 것”이나 “할 수 있는 것”으로 간주된다. 객체에 책을 할달할 때는 어떤 객체보다도 작업을 잘할 수 있는 객체에 책임을 할당해야한다.

다시 책임을 한마디로 정리해보자면 객체가 해야하는 것을 잘하는 것이라고 할 수 있다.

예를 들어 학생 클래스가 수강 과목을 추가하거나 조회하고, 데이터베이스에 객체 정보를 저장하거나 데이터베이스에서 객체정보를 읽는 작업도 처리하고, 성적표와 출석부도 출력하는 일도 한다고 가정해보자. 그렇다면 아래와 같은 클래스로 코드를 작성할 수 있을 것이다.

public class Student {

    public void getCourses(){}
    public void addCourse(Course course){}
    public void save(){}
    public Student load(){}
    public void printOnReportCard(){}
    public void printOnAttendanceBook(){}

}

위의 코드를 보면 학생 클래스가 너무나 많은 책임을 수행하고 있다. 학생 클래스에서 가장 잘할 수 있는 것을 생각해보면 수강 과목을 추가하거나 조회하는 것이다. 나머지 기능들은 학생 클래스가 아닌 다른 클래스에서 더 잘할 수 있을 여지가 많다. 그래서 학생 클래스는 수강과목을 조회하고, 추가하는 책임만을 수행하도록 하는 것이 바람직하다.

public class Student {

    public void getCourses(){}                  // 학생 클래스에서 잘할 수 있는 것
    public void addCourse(Course course){}      // 학생 클래스에서 잘할 수 있는 것
    // public void save(){}                     // 다른 클래스에서 잘할 수 있는 것
    // public Student load(){}                  // 다른 클래스에서 잘할 수 있는 것
    // public void printOnReportCard(){}        // 다른 클래스에서 잘할 수 있는 것
    // public void printOnAttendanceBook(){}    // 다른 클래스에서 잘할 수 있는 것

}

2. 변경

SRP를 따르는 실효성 있는 설계가 되려면 책임을 좀 더 현실적인 개념으로 파악할 필요가 있다. 설계원칙을 공부하는 이유는 예측하지 못한 변경사항이 발생하더라도 유연하고 확장성이 있도록 시스템 구조를 설계하기 위해서이다. 좋은 설계란 기본적으로 시스템에 새로운 요구사항이나 변경이 있을 때 가능한 영향을 받는 부분을 줄여야 한다. 어떤 클래스가 잘 설계되었는지를 판단하려면 언제 변경되어야 하는지를 물어보는 것이 좋다.

그럼 이전에 살펴본 학생클래스는 언제 변경되어야할까? 이 물음을 답하기 위해서는 학생 클래스의 변경 이유를 찾아보는 것이 좋다.

  • 데이터베이스 스키마가 변경된다면 학생 클래스도 변경되어야하는가?
  • 학생이 지도교수를 찾는 기능이 추가되어야 한다면 학생 클래스는 영향을 받는가?
  • 학생 정보를 성적표와 출석부 이외의 형식으로 출력해야한다면 어떻게 해야하는가?

위의 질문이 학생 클래스를 변경해야하는 이유가 될 수 있다.

변경의 영향

위의 그림의 빨간 선처럼 또한 책임이 많아질수록 클래스 내부에서 서로 다른 역할을 수행하는 코드끼리 강하게 결합될 가능성이 높다. 예를 들어 현재 수강과목을 조회하는 코드와 데이터베이스의 학생정보를 가져오는 코드가 연결될 수 도 있고, 학생이 수강 과목을 추가하는 코드와 데이터베이스 학생 정보를 갱신하는 코드가 서로 연결될 수도 있다.

3. 책임 분리

학생 클래스는 여러 책임을 수행하므로 학생 클래스의 도움을 필요로 하느느 코드도 많을 수밖에 없다. 학생 수강 과목 목록을 사용하는 코드, 신입생 정보를 데이터베이스에 기록하는 코드, 성적표와 출석부를 필요로 하는 코드 등등에서 사용될 것이다. 만약 학생 클래스에서 변경사항이 생기면 학생 클래스를 직접적이든 간접적이든 사용하는 모든 코드를 다시 테스트해야만 한다. 이렇게 모든 코드를 테스트하는 문제를 해결하려면 한 클래스에 너무 많은 책임을 부여하지 말고 단 하나의 책임만을 수행하도록 하여 변경 사유가 될 수 있는 것을 하나로 만들어야 한다. 이것을 책임 분리라고 한다.

책임분리

위의 그림의 초록, 빨강, 노랑, 파랑 선처럼 각각의 기능별로 책임을 부여하게 되면 테스트를 해야되는 코드의 양을 줄일 수 있다.

학생 클래스의 경우 변경 사유가 될 수 있는 것은 학생의 고유 정보, 데이터베이스 스키마, 출력 형식의 변화 3가지 이다. 따라서 학생 클래스는 학생 고유의 역할을 수행하게 변경하고, 학생 클래스의 인스턴스를 데이터베이스에 저장하거나 읽어들이는 역할을 담당하는 학생 DAO 클래스, 출석부와 성적표의 출력을 담당하는 성적표 클래스와 출석부 클래스로 분리하는 편이 좋다. 이렇게 클래스들이 책임을 적절하게 분담하도록 변경하면 어떤 변화가 생겼을 때 영향을 최소화할 수 있다.

아래의 그림은 위의 설명을 통해 개선한 것이다.

개선된 디자인

4. 산탄총 수술

지금까지는 한 클래스가 여러가지 책임을 가진 상황을 살펴봤다. 그런데 하나의 책임이 여러 개의 클래스로 분산되어 있는 경우에도 단일 책임에 입각해 설계를 변경해야 하는 경우도 있다. 이것을 산탄총 수술이라는 용어를 사용한다. 산탄총 수술은 산탄이 사방으로 퍼져날라가 동물에게 맞았을 때 수술해야하는 부위가 많아지는 것 처럼 어떤 변경이 있을 때 하나가 아닌 여러 클래스를 변경해야한다는 점에서 착안되었다.

여러 개의 클래스에 책임이 분산된 경우 클래스 하나하나를 모두 변경하지 않으면 프로그램이 정상적으로 동작하지 않고 에러가 발생되기 쉽다.

하나의 책임이 여러 클래스로 분리되어 있는 예는 로깅, 보안, 트랜잭션과 같은 횡단관심으로 분류할 수 있는 기능이 대표적이다. 횡단관심에 속하는 기능은 대부분 시스템 핵심 기능안에 포함되는 부가기능이다. 아래의 그림과 같이 부가 기능에 변경사항이 발생하면 해당 부가기능을 실행하는 모든 핵심 기능에도 변경사항이 적용되어야 한다.

횡단 관심

만약 시스템에서 실행하는 특정 메서드들의 실행 로그를 데이터베이스에 저장한다고 생각해보자. 분명 메서드에 로그기능을 실행하는 코드가 삽입되어 있을 것이다. 로그를 데이터베이스에 저장하지 않고 파일로 저장하는 경우에도 우선 로그 기능이 삽입된 메서드를 찾고 삽입된 로그 코드를 적절하게 변경해야한다.

이러한 방식은 산탄총 수술하는 것과 같이 변경될 곳을 모두 찾아 수정해야되는 작업이다. 이러한 문제를 해결하기 위한 방법은 부가 기능을 별개의 클래스로 분리해 책임을 담당하게 하는 것이다. 즉, 여러 곳에 흩어진 공통 책임을 한 곳에 모으면서 응집도를 높인다.

하지만 이런 독립 클래스를 구현하더라도 구현된 기능을 호출하고, 사용하는 코드는 해당 기능을 사용하는 코드 어딘가에 포함될 수 밖에 없다.

5. 관심지향 프로그래밍과 횡단 관심 문제

위의 문제점을 해결하는 방법은 관심지향(관점지향) 프로그래밍(Aspect-Oriented-Programming)이다. AOP는 횡단 관심을 수행하는 코드를 Aspect라는 특별한 객체로 모듈화하고 Weaving이라는 작업을 통해 모듈화한 코드를 핵심 기능에 끼워 넣을 수 있다. 이를 통해 기존의 코드를 전혀 변경하지 않고도 시스템 핵심 기능에서 필요한 부가기능을 효과적으로 이용할 수 있다. 만약 횡단관심에 변경이 생긴다면 해당 Aspect만 수정하면된다.

이것은 스프링을 공부하면서 AOP를 적용해봤을 때 봤던 내용들이다.