본문 바로가기

개발/소프트웨어공학

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

객체는 단 하나의 책임만을 가져야 한다. 책임이란 객체가 할수 있는 것, 해야 하는 것을 말한다.

 

예를들어 학생 클래스가 수강 과목을 추가하거나 조회하고 데이터베이스에 객체 정보를 저장하거나 데이터베이스에서 객체 정보를 읽는 작업도 처리하고 성적표와 출석부를 출력하는 일도 한다고 가정했을 때 이런 경우 학생 클래스의 코드는 다음과 같다.

public class Student {
    public void getCourses() {...}
    public void addCourses(Course c) {...}
    public void save() {...}
    public Student load() {...}
    public void printOnReportCard() {...}
    public void printOnAttendanceBook() {...}
}

 

현재 코드의 경우 Student 클래스는 너무 많은 책임을 수행해야 한다. 이는 곧 변경될 여지가 많다는 것이다. 현재 Student 클래스는 할당된 책임 중 가장 잘할 수 있는 것은 수강 과목을 추가하고 조회하는 일이다.

 

DB에 학생 정보를 저장하고 DB로부터 읽는 일이나 성적표와 출석부에 출력하는 일은 Student 클래스가 아닌 다른 클래스가 더 잘할 수 있는 여지가 많다. 따라서 Student 클래스에는 수강 과목을 추가하고 조회하는 책임을 수행하도록 하는 것이 SRP를 따르는 설계다.

 

설계 원칙을 학습하는 이유는 예측못한 변경사항에 유연하고 확장성 있도록 시스템 구조를 설계하기 위함이다. 즉 가능한 한 영향을 받는 부분을 줄여야 한다. 어떤 클래스가 잘 설계되었는지를 판단하려면 언제 변경되어야 하는지를 묻는것이 좋다.

 

현재의 Student 클래스가 언제 변경되어야 하는지를 알아보려면 변경 이유를 찾아보는 것이 좋다.

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

3가지 모두 Student 클래스를 변경해야 하는 이유가 된다.

 

또한 책임을 많이 질수록 클래스 내부에서 서로 다른 역할을 수행하는 코드끼리 강하게 결합된 가능성이 높아진다.

 

예를 들어 현재 수강 과목을 조회하는 코드(getCourse 메서드)와 DB에서 학생 정보를 가져오는 코드(load 메서드) 중 어딘가가 연결될 수도 있고, 학생이 수강 과목을 추가하는 코드(addCourse 메서드)와 DB에 학생 정보를 갱신하는 코드(save 메서드)가 서로 연결될 수도 있다.

 

이런 경우 DB 스키마의 변화가 학생의 고유한 기능(getCourses 메서드, addCourse 메서드)을 구현한 코드에 변화를 필요하게 할 수도 있다.

책임 분리

Student 클래스는 여러 책임을 수행하므로 Student 클래스의 도움을 필요로 하는 코드도 많을 수 밖에 없다.

 

학생의 수강 과목 목록을 사용해 어떤 일을 수행하는 코드도 Student 클래스의 도움을 필요로 하며, 신입생 정보를 DB에 기록하는 데도 Student 클래스를 필요로 할 수 있다. 또한 성적표와 출석부를 필요로 하는 코드도 Student 클래스를 사용할 수 있다.

 

이런 이유 때문에 Student 클래스에 변경사항이 생기면 Student 클래스를 사용하는 코드와 전혀 관계가 없더라도 직접 또는 간접적으로 사용하는 모든 코드를 다시 테스트해야 한다.

 

성적표에 학생을 표시하는 기능에 변경사항이 생기면 수강 과목을 조회하거나 등록하는 기능을 사용하는 코드도 다시 테스트해야 한다는 의미다. 이와 같이 어떤 변화가 있을 때 해당 변화가 기존 시스템의 기능에 영향을 주는지 평가하는 테스트를 회귀테스트라 한다.

 

모든 코드를 테스트하는 문제를 해결하려면 한 클래스에서 단 하나의 책임만 수행하도록 해 변경 사유가 될 수 있는 것을 하나로 만들어야 한다. 이를 책임 분리라 한다.

 

Student 클래스의 경우 변경 사유가 될 수 있는 것은 학생의 고유 정보, DB 스키마, 출력 형식의 변화 등 3가지다.

 

따라서 Student 클래스는 학생 고유의 역할을 수행하게끔 변경하고 학생 클래스의 인스턴스를 DB에 저장하거나 읽어들이는 역할을 담당하는 학생 DAO 클래스, 출석부와 성적표에 출력을 담당하는 성적표 클래스와 출석부 클래스로 분리하는 편이 좋다.

 

클래스들이 책임을 적절하게 분담하도록 변경하면 어떤 변화가 생겼을 때 영향을 최소화할 수 있다.

산탄총 수술

지금까지는 한 클래스가 여러 가지 책임을 가진 상황이었다. 그런데 하나의 책임이 여러 개의 클래스들로 분산되어 있는 경우에도 단일 책임 원칙에 입각해 설계를 변경해야 하는 경우도 있다.

 

어떤 변경이 있을 때 하나가 아닌 여러 클래스를 변경해야 한다는 것이다.

 

클래스 하나하나를 모두 변경하지 않으면 프로그램이 정상적으로 동작하지 않고 에러가 발생할 수 있다. 하나의 책임이 여러 개의 클래스로 분리되어 있는 예는 로깅, 보안, 트랜잭션과 같은 횡단 관심으로 분류할 수 있는 기능이 대표적이다.

횡단 관심에 속하는 기능은 대부분 시스템 핵심 기능(하나의 책임) 안에 포함되는 부가 기능(여러 개의 클래스로 분리)이다.

 

보통 부가 기능에 변경사항이 발생하면 해당 부가 기능을 실행하는 모든 핵심 기능에도 변경사항이 적용되어야 한다.

 

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

 

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

 

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

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

횡단 관심 문제를 해결하는 방법으로 관심지향 프로그래밍(AOP, Aspect-Oriented Programming) 기법이 있다.

 

AOP는 횡단 관심을 수행하는 코드를 aspect라는 특별한 객체로 모듈화하고 weaving이라는 작업을 통해 모듈화된 코드를 핵심 기능에 끼워넣을 수 있다. 이를 통해 기존의 코드를 전혀 변경하지 않고도 시스템 핵심 기능에서 필요한 부가 기능을 효과적으로 이용할 수 있다. 만약 횡단 관심에 변경이 생긴다면 해당 aspect만 수정하면 된다.

 

*연관 관계의 역할 이름은 연관된 클래스의 객체들이 서로를 참조할 수 있는 속성의 이름으로 활용할 수 있다.

 

SRP와 고전적 설계 개념인 응집도와 결합도의 관계는 다음과 같다.

 

응집도 - 한 프로그램 요소(절차지향 관점에서는 프로그램 함수나 프로시저, 객체지향적 관점에서는 클래스나 메서드)가 얼마나 뭉쳐 있는가를 나타내는 척도다. 가령 프로시저 하나가 단일 기능을 실행하도록 문장이나 자료구조가 구성되었다면 해당 기능을 실행하기 위해 해당 구성 요소 어떤 것도 빠뜨리지 못할 것이다. 말 그대로 구성 요소들 사이의 응집력이 강하다. 이에 반해 프로시저가 여러 기능을 실행하도록 구성되어 있다면 각각의 기능을 실행하는 데 필요한 구성 요소들 사이는 서로 별다른 연관이 없을 것이다. 이런 경우에 SRP를 따르면 응집도는 높아진다.

 

결합도 - 프로그램 구성 요소들 사이가 얼마나 의존적인지를 나타내는 척도. 프로그램 안 프로시저 하나의 자료구조가 다른 형태로 변경되었을 때 이 프로시저를 사요여하는 곳도 변경되어야 한다면 이 두 프로시저는 결합도가 높다. 그러나 아무런 영향을 미치지 않는다면 결합도는 낮다고 한다.

 

설계의 기본 원칙은 응집도는 높고, 결합도는 낮게 하는 것으로 세운다. 응집도가 높으면 관련 기능이 한 곳에 모여있게 되는데, 이는 재사용과 유지 보수가 쉬워진다. 결합도가 낮아야 하는 이유는 결합도가 높은 시스템의 한 부분이 변경이 되면 이에 연관된 부분들도 같이 변경하거나 회귀 테스트를 실행해야 한다. 더군다나 변경하려는 부분을 독립적으로 떼어놓기 어렵기 때문에 재사용성이 낮으며 이해하기도 쉽지 않다.

 

응집도와 결합도는 서로 독립적인 개념이 아니라 밀접한 관계가 있는 개념. 관련된 것들을 한 곳에 두어 응집도를 높이면 자연스럽게 결합도는 낮아진다. 따라서 한 클래스로 하여금 단일 책임을 갖게하는 SRP에 따른 설계를 하면 응집도는 높아지고 더불어 결합도는 낮아진다.

 

*AOP 관련 용어
조인포인트(Jointpoint) - 애플리케이션 실행 중의 특정한 지점을 의미. 전형적인 조인포인트의 예로는 메서드 호출, 메서드 실행 자체, 클래스 초기화, 객체 생성 시점 등이 있다. 조인포인트는 AOP의 핵심 개념이며 애플리케이션의 어떤 지점에서 AOP를 사용해 추가적인 로직을 삽입할지를 정의한다.

 

어드바이스(Advice) - 특정 조인포인트에 실행하는 코드. 조인포인트 이전에 실행하는 Before 어드바이스와 이후에 실행하는 After 어드바이스를 비롯한 여러 종류의 어드바이스가 있다.

 

포인트컷(Pointcit) - 여러 조인포인트의 집합체. 언제 어드바이스를 실행할지 정의할 때 사용. 포인트컷을 만들면 애플리케이션 구성 요소에 어드바이스를 어떻게 적용할지 상세하게 제어할 수 있다. 가장 일반적으로 사용하는 조인포인트는 메서드 호출. 따라서 가장 일반적인 포인트컷은 특정 클래스에 있는 모든 메서드 호출로 구성된다. 종종 어드바이스 실행 지점을 좀 더 다양하게 제어할 필요가 있을 때는 복잡한 형태로 포인트컷을 구성할 수도 있다.

 

애스펙트(Aspect) - 어드바이스와 포인트컷을 조합한 조합물. 즉, 애플리케이션이 가져야 할 로직과 그것을 실행해야 하는 지점을 정의한 것

 

위빙(Weaving) - 애플리케이션 코드의 해당 지점에 애스펙트를 실제로 주입하는 과정. 컴파일 시점 AOP 솔루션은 이 작업을 컴파일 시점에 하며 빌드 중에 별도의 과정을 거친다. 마찬가지로 실행 시점 AOP 솔루션은 실행 중에 동적으로 위빙이 일어난다.

 

#참고자료

https://blog.bitsrc.io/solid-principles-every-developer-should-know-b3bfa96bb688