6 분 소요

Reference

스프링 입문을 위한 자바 객체 지향의 원리와 이해, 위키북스, 김종민 지음

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

[객체지향 SW 설계의 원칙] ② 사례연구, 단일 책임 원칙


📌 What’s SOLID?

SOLID: Robert C. Martin이 2000년대 초 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙으로 제시한 것을 Michael Feathers가 두문자어로 소개한 것. SOLID란 객체지향의 특성을 올바르게 사용하는 방법, 즉 객체 지향 언어를 이용해 객체 지향 프로그램을 올바르게 설계해 나가는 방법이나 원칙을 뜻한다.

  • SOLID는 응집도는 높이고(High Cohesion), 결합도는 낮추라(Loose Coupling)는 고전 원칙을 객체 지향의 관점에서 재정립한 것이라고 할 수 있다.
  • 응집도란? 하나의 모듈 내부에 존재하는 구성 요소들의 기능적 관련성
    • 응집도가 높은 모듈은 하나의 책임에 집중하고 독립성이 높다.
    • 독립성이 높기 때문에 재사용이나 유지보수가 용이하다.
    • 응집도 수준은 기능, 순차, 통신, 절차, 시간, 논리, 우연 응집도를 통해 살펴볼 수 있다.
  • 결합도란? 모듈(클래스)간의 상호 의존 정도
    • 결합도가 낮은 모듈은 상호 의존성이 줄어들어 객체의 재사용성이 높다.
    • 다른 모듈(클래스)에 의존하는 정도가 낮기 때문에 유지보수가 용이하다.
    • 결합도 수준은 데이터, 스탬프, 컨트롤, 외부, 공유, 내용 결합도를 통해 살펴볼 수 있다.
  • SOLID는 속성, 메서드, 클래스, 객체, 라이브러리, 아키텍처 등 다양한 곳에 다양하게 적용된다. 이는 보는 사람의 관점에 따라 다르게 해석될 수 있음을 내포한다. 그 이유는 SOLID가 어떤 특정 제품이 아닌 개념이기 때문이다. 객체 지향 프로그래밍 개발자는 SOLID 개념을 소프트웨어에 녹여낼 수 있어야 한다.
  • SOLID 개념을 녹여낸 소프트웨어의 특징
    • 코드를 보고 다른 소프트웨어에 비해 상대적으로 이해하기 쉽다.
    • 상대적으로 이해하기 쉽기 때문에 리팩토링하기 더 쉽다.
    • 유지보수가 용이하다.
    • 코드의 재사용성이 높다.


📌 SRP: 단일 책임 원칙

“어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다 “ Robert C. Martin

객체 지향의 기본은 책임을 객체에게 할당하는 데 있다. 책임을 부여받은 존재가 객체가 되고, SRP는 책임과 관련된 원칙이다.

  • 슈퍼맨보다 분야별 전문가가 낫다.
  • 어떤 단일 조직이 여러 분야로 다각화하게 되면 조직 내에서 다른 목표를 추구하는 사람들 간의 이질감이 발생하고 또한 한 가지에 집중하지 못하고 힘이 흩어지기 때문에 다각화는 쉽사리 다악화로 변질된다.


What’s 역할(책임)?

클래스, 객체, 메서드, 라이브러리, 모듈 등이 갖는 역할(책임)은 무엇인가?

한 남자가 여자친구와 오랫동안 사귀어 서로를 사랑하고 신뢰가 커져 결혼을 한다고 가정한다. 남자의 기존 역할(책임)은 남자친구 였지만, 가정을 이뤄 남편(가장)으로 역할이 변경되었다. 여기서 역할이 변경되는 이유는 남자친구의 역할을 하며 서로를 사랑하고 신뢰가 커져 역할이 남편으로 변경되어야 하는 수정 사항이 발생했기 때문이다.

역할은 어떤 수정 요구가 있을 때 변경된다. 이를 생각해보면 역할(책임)은 어떤 요구가 존재해야 만들어진다.

그렇다면 역할의 단위는 어떻게 측정해야 할까? 변경 요구가 있을 때 역할이 수정된다면 역할의 단위는 변화되는 부분과 관련된다는 의미이다.

각각의 역할은 서로 다른 이유로 변경되고, 서로 다른 비율로 변경된다.

  • 역할(책임)이란 변경을 위한 이유이다.


역할(책임)을 나눠야 하는 이유

  • 소프트웨어는 항상 변경을 전제로 한다. 변화하는 요구사항에 대응하는 것이 소프트웨어의 숙명이고, 변화에 민감하게 반응하기 위해서는 책임에 따라 철저하게 분리되어 있어야 한다.

한 클래스, 객체, 메서드 등이 여러 역할(책임)을 갖게 되면 그 존재는 각 책임마다 변경되는 이유가 발생할 수 있다. 어떤 존재를 변경해야 하는 이유를 오직 하나로 만들기 위해서는 한 개의 책임만을 부여하면 된다.

예시로 한 남자의 역할과 책임은 총 5가지가 존재한다.

  1. 여자친구의 남자친구 역할
  2. 회사(직장 상사)의 부하직원 역할
  3. 부모님의 아들 역할
  4. 친구들의 친구 역할
  5. 동호회의 총무 역할
public class Man {

	public void date() {}

	public void trip() {}

	public void goToWork() {}

	public void meeting() {}

	public void hyodo() {}

	public void money() {}

	public void drink() {}

	public void camping() {}

	public void countingMoney() {}

	public void settleMoney() {}

}
  • 남자의 역할이 많으니 피곤해 보인다. 그 이유는 역할과 책임이 너무 많기 때문이다. 객체 지향에서 한 클래스, 객체, 메서드가 너무 많은 역할과 책임을 갖게 되면 나쁜 냄새가 난다고 말한다.

남자가 회사에서 직원 역할을 하며 업무를 보는 도중 큰 실수를 해 직장상사에게 크게 깨진 날은 남자의 기분이 매우 안좋을 것이다. 이런 상황에서 여자친구, 부모님, 친구들, 계모임 에서 여러 요청사항이 생긴다면 어떻게 될까? 남자는 참지 못하고 여자친구, 부모님, 친구들에게 화를 낼 수 있고, 계모임에 아무런 공지도 없이 참석하지 않을 수 있다. 이런 불상사를 예방하기 위해 역할(책임)을 분리해야 한다.

역할(책임)을 분리하면 어떻게 될까? 5가지 역할이 남자라는 한 클래스에서 다섯 개의 클래스로 쪼개진다.

이렇게 역할을 쪼갠 남자는 각각의 역할에만 충실하다. 회사에서는 오직 직원, 집에서는 오직 아들, 친구들과 있을 때 여자친구와 있을 때 각각 친구, 남자친구의 역할을 하며 계 모임에서는 오직 총무의 역할만 한다. 남자는 역할마다 완전히 감정이 분리되어 한쪽에서 기분이 나쁘더라도 다른 역할을 하고 있을 때는 아무런 영향을 끼치지 않게 된다.

  • 남자라는 하나의 클래스에 다섯 가지 역할이 있을 때는 각각 서로 엉켜 영향을 끼치고 냄새가 났지만, 역할(클래스)을 분리하니 각각 하나의 역할과 책임에만 집중할 수 있게 되었다.
class Man {

}

class BoyFriend extends Man {
	public void date() {
	}

	public void trip() {
	}
}

class Officer extends Man {
	public void goToWork() {
	}

	public void meeting() {
	}
}

class Son extends Man {
	public void hyodo() {
	}

	public void money() {
	}
}

class Friend extends Man {
	public void drink() {
	}

	public void camping() {
	}
}

class GeneralAffair extends Man {
	public void countingMoney() {
	}

	public void settleMoney() {
	}
}


단일 책임 원칙 위반의 나비효과

  • 왕따가 발생한다.

단일 책임 원칙을 위배한 클래스가 호출될 때 사용하고자 하는 기능 이외의 것들은 소외된다. 만약 A라는 책임과 B라는 책임을 갖고 있는 클래스가 있을 경우 A만 필요로 하는 애플리케이션은 항상 B를 들고 다니고, B는 애플리케이션 내에서 철저히 외면된다.

  • 책임의 개수가 많을수록 뒤엉키게 되고 서로간 영향력이 늘어나게 된다.

예시로, HTTP 프로토콜을 이용해서 데이터를 읽어 와 화면에 보여주는 기능을 하는 DataViewer 클래스가 있다고 가정한다.

class DataViewer {

	void display() { // 화면에 띄우기
		String data = loadHtml();
		updateGui(data);
	}

	String loadHtml() { // HTML 불러오기
		HttpClient client = new HttpClient();
		client.connect(url);
		return client.getResponse();
	}
	
	void updateGui(String data) { // GUI 업데이트
		GuiData guiModel = parseDataToGuiData(data);
		...
	}
	
	GuiData parseDataToGuiData(String data) { // 파싱 처리
		
	}
}

여기서 HTTP 프로토콜이 Socket 기반 프로토콜로 변경되면 수정사항이 발생한다. 기존의 String이 byte[] 로 변경되어 연쇄적인 코드 수정이 일어나게 되어 각 메서드의 타입이 String 에서 byte[]로 변경되어야 한다. 클래스의 역할을 따라 차례대로 코드 수정을 하게 되는데 이는 객체 지향 프로그래밍이 아닌 절차 지향 프로그래밍이다.

DataViewer가 단일 책임 원칙을 위반해 나비효과를 일으켰다.

  • 단일 책임 원칙 위반은 재사용성을 어렵게 만든다.

HTTP 프로토콜을 이용해 데이터를 읽어 화면에 보여주는 기능을 만들었고, Socket 기반 프로토콜로 읽어와 화면에 보여주는 기능도 만들고 싶어했다. 둘 다 필요한 상황이라면 어떻게 해야할까?

DataViewer가 화면에 보여주는 책임과 데이터를 읽는 책임을 동시에 가지고 있어 데이터를 읽는 부분만 Socket 기반으로 변경하면 된다. 하지만 두 책임이 엉켜 있어 한 책임을 수정하게 되면 위에서 본 나비효과가 발생한다. 다른 해결책은 DataViewer의 코드를 복붙해 새로운 클래스를 만들고 Socket 프로토콜로 변경하면 된다. 이는 매우 비효율적이다. 이렇게 문제를 해결하면 객체 지향 언어를 사용할 이유가 없다.

단일 책임 원칙 위반은 역할간 의존성을 높여 떼어낼 수 없게 만들어 필요한 부분만 가져다 쓸 수 있는 객체 지향의 장점을 없애버린다.


역할(책임)은 언제 분리시켜야 할까?

  • 아무 연관 없는 변경에 피해를 입는 경우
  • 불필요한 기능들이 호출되는 경우
  • 서로 다른 이유로 바뀌는 책임들이 한 곳에 포함되어 있는 경우. 즉, 클래스를 변경해야 하는 이유가 여러가지 인 경우
  • 하나의 속성이 여러 의미를 갖는 경우
  • 하나의 메서드가 여러 행위를 하는 경우
    • if-else 가 남발하는 냄새나는 코드가 된다.


SRP == 추상화

  • 단일 책임과 가장 관계가 깊은 것은 모델링 과정을 담당하는 추상화이다.
  • 애플리케이션의 경계를 정하고 추상화를 통해 클래스들을 선별하고, 속성과 메서드를 설계할 때 반드시 SRP를 고려하는 습관을 들여야 한다.
  • 추상 클래스(Extract Class)는 혼재된 각 책임을 각각의 개별 클래스로 분할해 클래스 당 하나의 책임만을 맡도록 만든다. 여기서 관건은 책임만 분리하는 것이 아니라 분리된 두 클래스간 관계의 복잡도를 줄이도록 설계하는 것이다.


SRP를 잘 지키고 있나?

단일 책임 원칙을 잘 지키고 있는지 확인 하기 위해서는 어떤 클래스, 객체, 메서드등이 서로 다른 이유로 변경되고 있음을 확인하면 된다. 하지만 경험이 적은 프로그래머가 이를 확인하긴 어렵다.

  • 어떤 클래스를 변경하고자 할 때 변경 하려는 이유가 한 가지인지 확인해 보자.
  • 메서드를 실행하는 것이 누구인지 확인해 보자. 단일 책임 원칙을 지키는지 확인하고 싶으면 메서드를 직접 사용하는 것이 누구인지 확인해 보면 된다.
  • 변경 사항이 발생해 적용시켰을 때 연관이 없는 다른 클래스에 에러가 뜨는지 확인해 보자.

댓글남기기