라이브러리
프로그래밍언어, 각종코드, 관련동향, 논문 등의 저장소
[객체지향 SW 설계의 원칙] ② 사례연구, 단일 책임 원칙

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

- 헤어져서 행복해진 사례연구, 단일 책임 원칙

 

연재순서
1회.다시 보면 크게 보이는 개방-폐쇄 원칙
2히.헤어져서 행복해진 사례연구, 단일 책임 원칙
3회.인터페이스 분리의 원칙
4회. 리스코프 치환 원칙
5회. 의존 관계 역전의 원칙
6회. 정리

 

오래 전 질레트가 여러 분야로 사업을 다각화한 적이 있다. 자신의 경쟁력 있는 분야인 면도날, 면도기 사업을 벗어나 샴푸 등 일용품 사업에까지 뛰어든 것이다. 면도용품도 일용품이니 사업분야 간 시너지 효과가 있을 것이라는 판단이었던 듯하다.

하지만 실적은 나지 않았고 주가는 곤두박질쳤으며 결국은 많은 손실을 입은 채로 다각화한 사업분야를 정리해야만 했다. 현재는 면도용품 사업에 집중해서 세계 최고의 면도용품 회사라는 명성과 실적을 되찾게 되었다.

이처럼 많은 기업들이 위험을 분산시키고 실적을 향상시키기 위해서 다각화(diversification)를 한다. 하지만 많은 경우 다각화는 오히려 위험을 증가시키고 실적을 악화시킨다. 이를 들어 월 스트리트의 전설적인 투자가로 불리는 피터 린치(Peter Lynch)는 “다각화는 대부분 다악화(diworsfication)로 끝난다”고 충고한다. 실제 그는 자신의 포트폴리오에 다각화를 시도하는 기업은 가능한 배제하며, 경험상 이러한 결정을 후회한 적은 거의 없다고 자신 있게 말한다.

스탠포드의 짐 콜린스 교수는 피터 린치의 경험을 트럭 수십 대에 해당하는 자료를 분석한 결과로 지지해 준다. 그의 연구에 따르면 위대한 기업으로 도약한 기업들은 모두가 우직하게 한 우물을 팠다고 한다. 다음은 그가 "좋은 기업에서 위대한 기업으로"란 책에서 ‘예상치 못한 발견’이라 놀라며 서술한 내용이다.

“좋은 회사에서 위대한 회사로 도약한 기업들은 고슴도치 - ‘한 가지’만 알고 그것에 집중하는 단순하고 촌스러운 동물-에 가깝다. 비교 기업들은 여우 - 많은 것을 알지만 일관성이 결여된 꾀 많고 교활한 동물-에 가깝다.”

어떤 단일 조직이 여러 분야로 다각화하게 되면 조직 내에서 다른 목표를 추구하는 사람들 간의 이질감이 발생하고 또한 한 가지에 집중하지 못하고 힘이 흩어지기 때문에 다각화는 쉽사리 다악화로 변질된다. 소프트웨어 세계에서도 이와 비슷하게 다각화를 경계하라는 원리가 있다. 바로 하나의 클래스는 하나의 책임만을 가져야 한다는 ‘단일 책임 원칙(이하 SRP)’이다.

이번 글에서는 프로그램에서 객체가 가지는 책임이란 것이 무엇인지, 그리고 왜 객체가 단일 책임만 가지는 것이 좋은지를 살펴볼 것이다. 또한 현재 객체가 이미 여러 책임을 지니고 있는 여우 객체인 경우 나타날 수 있는 문제점(악취)과 이를 고슴도치 객체로 바꾸는 방법에 대해서도 논의해 본다.

단일 책임 원칙의 개요
다음은 국제 거래 은행에서 사용하는 ‘잔고’라는 클래스이다. 잔고 클래스는 다음과 같은 인터페이스를 갖는다. 이 클래스의 인터페이스는 직관적으로 단순하고 기능적으로 완결된 클래스일 수 있다. 하지만 만약 두 개의 서로 다른 애플리케이션이 잔고 클래스를 사용한다고 했을 때 이 두 사용자 클래스는 서로 다른 메쏘드를 이용하게 된다. 환율 조정 애플리케이션은 ‘환율 계산’ 메쏘드를 이용할 것이고 이율 관리 애플리케이션은 금액에 관한 인터페이스를 이용할 것이다.

하지만 이 각 애플리케이션이 각각 배포됐을 때 잔고 클래스는 정체성의 혼란이 생긴다. 왜냐하면 이율관리 애플리케이션에 배포될 때는 ‘환율 계산’ 메쏘드가 무용하게 되고 환율 조정 애플리케이션과 배포될 때는 금액에 관한 인터페이스가 소외되기 때문이다. 문제는 여기서 그치지 않는다. 만약 환율 계산 메쏘드의 시그니처가 변경될 경우 환율 계산을 요청하는 잔고 클래스의 다른 메쏘드들이 같이 변경되어야 한다. 간결하게 설계한 잔고 클래스는 확실히 문제를 내장하고 있었다.

사용자 삽입 이미지
<그림 1> 잔고 클래스


문제의 원흉은 무엇일까? 잔고 클래스는 사실은 두 가지 책임을 가지고 있다. 수리적 이율 연산을 담당하는 ‘환율 계산’의 책임과 금액에 관련한 처리를 담당하는 메쏘드들이 또 다른 책임이다. 또한 하나의 클래스 안에 필요에 의해 두 가지 책임이 공존할 때 서로의 의존관계는 심각하게 강결합되기 때문에 변경에 대한 충격이 전달될 수밖에 없다.

확실히 두 가지 책임을 담당해야 하는 한 클래스는 불편한 점이 많다. 지난 글에 소개한 OCP는 ‘확장’이 설계적 관전 포인트라면 단일 책임 원칙(Single Responsibility Principle : SRP)은 ‘변경’이 관전 포인트가 될 것이다. 이 ‘변경’의 거북함을 조장하는 요소는 서로 다른 ‘책임’이 혼재해 있다는데 있다.

SRP의 키워드는 책임으로 요약되는데, 그렇다면 책임이란 무엇일까? 책임이란 ‘변경을 위한 이유’이다. 만약 하나의 클래스에 변경을 위한 두 가지 이상의 이유가 있다면 그 클래스는 한 가지 이상의 책임을 갖고 있는 것이다. <그림 1>의 잔고 클래스는 변경의 내용이 두 가지로 요약된다. 금액과 환율이다. 즉, 잔고 클래스는 금액과 환율의 책임을 갖고 있다.

SRP는 하나의 클래스에 한 가지 책임을 가르치는 원칙이다. 우리는 설계 관점에서 우리가 인식하지 못하는 SRP 위반을 자주 하게 된다. 이 위반을 경계하기 위해 깊은 통찰력이 필요하지도 않다. 단지 머리에 ‘책임’이란 단어를 상기하는 습관이면 된다.

위반 사항에는 대가가 따른다. SRP를 위반할 경우 따르는 재앙은 첫 번째로 ‘왕따’가 발생한다는 것이다. 잔고 클래스가 이율 관리 애플리케이션과 배포됐을 때 확실히 ‘환율 계산’ 메쏘드는 소외된다. 즉 만약 A라는 책임과 B라는 책임을 갖고 있는 클래스가 있을 경우 A만 필요로 하는 애플리케이션은 항상 B를 들고 다녀야 한다.

문제는 여기서 그치지 않는다. 두 번째 재앙은 무관한 메쏘드에 변경이 발생할 경우 불필요한 변경 임팩트가 전달된다. 만약 ‘환율 계산’ 메쏘드가 변경됐을 경우 이율 관리 애플리케이션은 사용하지도 않는 ‘환율 계산’ 메쏘드 때문에 다시 컴파일해야 하고 리테스트해야 하며 재배포해야 한다. 이율 관리와 전혀 무관한데도 불구하고... 사실은 이 임팩트의 영향은 더 심각한데 다음의 케이스 스터디에서 살펴보겠다.

이미 구현된 소프트웨어에서 이 재앙들은 다시 (『리팩토링』에서 소개하는) 악취(bad smell)로 분류될 수 있다. 즉, 무관한 변경에 피해를 당한다든가 불필요한 요소가 따라다닐 경우 SRP를 적용해야 하는 빨간불(bad smell)로 생각해도 무방하다. 그렇다면 SRP는 어떤 구조를 제안하고 있을까?

마틴 파울러의 엔터프라이즈 패턴을 이용한 케이스 스터디
지금은 DB에 관한 여러 우수한 툴들이 제공되고 DB와 관련한 좋은 설계방식이 많이 제안되고 있다. 그래서 과거에 복잡하고 장황했던 DB 관련 코드들이 현재는 이런 툴과 기법을 통해 많이 단순화, 은닉되고 있지만 일반적으로 개발자가 간단하게 DB 관련 클래스를 설계할 때는 <그림 2>과 같은 방식을 사용한다.

사용자 삽입 이미지
<그림 2> 액티브 오브젝트 패턴


이 Person이란 클래스는 세 가지 필드를 가지고 있고 이 클래스의 행위를 처리하는 메쏘드(비즈니스 로직 메쏘드)와 DB를 접근하기 위한 CRUD(Create, Read, Update, Delete) 메쏘드를 가지고 있다. Person이란 객체가 비즈니스 로직 메쏘드에 의해 상태변화가 일어났을 경우 (필드가 CRUD 됐을 경우) 적절한 시점에서 DB에 그 변화된 값을 반영해야 하며 이때 DB 접근 메쏘드를 사용하게 된다.

이렇게 비즈니스 로직 메쏘드와 DB 처리 메쏘드를 분리하는 이유는 하나의 메쏘드에 비즈니스 로직 루틴과 DB 처리 루틴이 혼재하게 됐을 때 메쏘드의 처리 루틴의 복잡도가 2배 이상 증폭되기 때문이다. 만약 분리시키지 않았다면 자바의 경우 Connection, PreparedStatement, ResultSet 같은 JDBC 클래스들이 비즈니스 로직과 무관하게 전체 루틴 사이에 등장하게 된다. 또한 이런 루틴은 필연적이게도 루핑을 하며 ResultSet에서 레코드를 읽어 와서 어떤 처리를 한다든가 변수에 대입하는 루틴이 작성되게 된다.

하지만 이런 메쏘드는 DB 처리 루틴이 변할 때 비즈니스 코드를 변경해야 하며 그 역의 경우도 발생하게 된다. 즉 하나의 책임이 변할 때 역시 필연적이게도 다른 책임도 같이 변하게 된다. SRP 위반의 두 번째 재앙의 대표적인 사례가 이런 경우이다.

이런 경우를 이번 호의 언어로 풀이하자면 하나의 메쏘드에 두 가지의 책임(‘비즈니스 로직’과 ‘DB 로직’)이 같이 있었기 때문이며 이를 분리하여 관리하는 설계가 <그림 2>의 액티브 오브젝트 패턴이다. 액티브 오브젝트 패턴은 이렇게 DB관련 처리를 따로 메쏘드로 캡슐화하며 비즈니스 로직과 DB 메쏘드를 분리시킴으로써 깔끔한 설계와 효과적인 관리를 보장하는 장점을 갖는다.

액티브 오브젝트 패턴은 하나의 메쏘드에서 두 가지 책임을 분리시켰을 뿐이지 하나의 클래스에서 두 가지 책임을 분리시키지 못했다. 즉 <그림 2>의 Person 클래스에는 여전히 Person 객체에 대한 DB에 접근 책임과 Person의 비즈니스 로직 책임이 혼재되어 있다. 데이터 맵퍼 패턴은 이 두 책임을 분리시키는 구조를 제안하고 있다(<그림 3> 참조).

사용자 삽입 이미지
<그림 3> 데이터 멥퍼 패턴


사실 데이터 맵퍼 패턴은 우리가 흔히 DAO(Data Access Object)로 알고 있는 인터페이스로 실현화되어 익히 사용하고 있는 패턴이다. 흔히 『Core J2EE Patters』에서 소개된 ‘DAO 패턴’으로 DAO를 알게 된 자바 개발자는 DAO 개념이 J2EE에서 제안된 것으로 알고 있다. 하지만 DAO는 마이크로소프트에서 4GL 언어 아키텍처 작업 당시 객체단위 DB 접근 인터페이스로 제안한 DB 접근 객체 인터페이스다.

데이터 맵퍼는 클래스가 비즈니스 로직에 집중할 수 있도록 DB 접근 루틴을 데이터 맵퍼 클래스로 분리시킴으로써 액티브 오브젝트 패턴에서 필자가 제기했던 ‘한 지붕 두 책임’의 문제를 해결한다. 데이터 맵퍼 패턴을 따르면 객체와 데이터베이스, 맵퍼 간의 독립성이 유지되며, 객체와 DB 간에 데이터를 이동시키는 맵퍼 레이어를 제공받게 된다. 따라서 DB 테이블이 변하거나 DB 접근 루틴이 변한다 해도 Person 클래스는 변경의 충격에서 안전하다.

또한 데이터 맵퍼를 사용하면 Person 객체의 이용방식도 자연스럽게 DB 관련 부분과 Person 사용으로 분리된다. 즉 최초 DB에 저장된 Person 객체를 생성할 경우 PersonDAO에게 load를 요청해 DB에서 Person을 얻어오고, 사용자는 자연스럽게 Person 객체의 비즈니스 로직 부분만 집중하게 된다. 이 Person 객체를 변경하거나 삭제를 원할 경우 각각 PersonMapper를 통해 insert, update를 요청하여 DB 작업을 위임한다.

액티브 오브젝트 패턴의 경우 이 두 가지 책임에 관한 사용자의 작업이 명백히 분리되지 못했는데 데이터 맵퍼 패턴을 사용하므로 사용자의 Person 객체에 관한 책임을 사용하는 목적과 방법이 명확해진다(Person 객체의 인터페이스인지, DB 관련 작업인지).

이로써 액티브 오브젝트와 데이터 맵퍼 패턴으로 최초의 Person 클래스의 책임은 명확하게 분리될 수 있었다. 하지만 만약 Person 객체의 상태변화가 압도적으로 많아서 DB 접근이 빈번하게 이뤄지고 이 빈번한 DB 접근 비용으로 인해 성능장애가 올 경우를 상상해 보자. 우리는 이와 같은 경우 DB 접근 비용을 감소시키기 위해 일반적으로 맵퍼 레이어 뒤에 캐싱 레이어를 둔다. 즉 Person 클래스에게 또 하나의 ‘캐싱’이라는 책임이 더해진다.

가령 앞서 제기한 문제처럼 ‘boby’라는 Person 객체를 n번 load한다고 했을 때 기존 방식으로는 n번 DB에 SELECT해야 한다. 하지만 SELECT는 한번만으로도 족하다. 한번 load된 객체를 재사용한다면 DB 접근은 n-1번 생략할 수 있다.

문제는 이 캐시란 책임을 어디에 두느냐일 것이다. 액티브 오브젝트처럼 (같은 클래스의) 메쏘드 단위로 클래스 내에 분리시킬 것인가, 데이터 맵퍼처럼 클래스 단위로 서로 분리시킬 것인가, 아니면 더 큰 컴포넌트나 패키지로 분리할 것인가의 분리 단위 결정이 갈등요소가 된다.

여기서 설계자는 분리의 ‘크기(granularity)’를 고민한다. 작은 단위로 섬세하게 사용할 수 있는 미세단위(fine-grained)로 구분할 것인가, 아니면 단순하지만 입도가 큰(COARSE-GRAINED) 단위로 구분할 것인가에 대해서 말이다. 이 문제의 경우 애플리케이션에서 문제 영역이 각각 서로 다른 케이스 바이 케이스로 이루어지기 때문에 명백히 일관적으로 적용할 가이드라인을 제공하기 힘들다.

하지만 그 기준은 대상에 대한 복잡도, 크기, 용도가 된다. 복잡도가 높고 부피가 큰데 반해 그 용법이 단순하다면 COARSE-GRAINED가 적합하다. 역으로 복잡도가 낮고 부피가 작으며 용법이 다양하다면 fine-grained가 적합하다.

사용자 삽입 이미지
<그림 4> 식별자 맵 패턴


여하튼 이 경우 보통 식별자 맵 패턴이 사용되는데 식별자 맵은 DB를 통해 얻어온 객체를 캐시하는 맵이다(<그림 4>에서 식별자 맵은 클래스 크기로 분리하고 있다). 한번 load된 객체는 식별자 맵에 등록되고 두 번째 load 요청부터 DB에 SELECT할 필요 없이 식별자 맵에서 가져오면 된다.

사용자 삽입 이미지
높은 응집도, 낮은 결합도
사용자 삽입 이미지
 
사용자 삽입 이미지
‘높은 응집도, 낮은 결합도(High Cohesion, Loose Coupling)’의 원리는 1970년대 Larry Constantine과 Edward Yourdon이 정의했던 아주 고전적인 원리이다. 이것은 현재 모든 소프트웨어 시스템 고유의 유지보수성과 적응성을 측정하는 가장 좋은 방법으로 사용되고 있다. 소프트웨어 디자인뿐만 아니라 아키텍처 평가에도 이 원리가 기준이 되는데, 그 이유는 이 원리의 적용 효과가 아주 명백하기 때문이다.

이 원리의 예외는 거의 찾아보기 힘들만큼 보편성을 가지고 있어서 마치 물리학의 엔트로피 법칙처럼 절대적인 기반원리를 제시한다. 낮은 응집도를 갖는 구조는 변경이나, 확장 단계에서 많은 비용을 지불해야 하며 높은 결합도의 경우도 마찬가지이다.

응집도는 ‘하나의 클래스가 하나의 기능(책임)을 온전히 순도 높게 담당하고 있는 정도’를 의미하며 이들은 서로 조화될수록 그 구조는 단순해진다. 응집도가 높은 동네에서 내부 개체가 변했을 때 다른 개체에 충격을 주는 것은 오히려 당연한 징후이다. 이들은 하나의 책임아래 서로 유기적인 관계를 갖고 있기 때문에 내부 개체가 변했을 때 다른 개체의 변경 확률이 높아진다. 마치 예쁜 부츠를 사면 부츠에 어울리는 치마를 입어야 하듯이… 응집도의 종류는 다양한데 다음은 권장할 만한 순기능적 응집 관계들이다.

* 기능적 응집(Functional Cohesion)
일관된 기능들이 집합된 경우를 말하며 <그림 3>의 데이터 맵퍼는 DB 처리라는 기능 항목의 높은 응집성을 갖는다.
* 순차적 응집(Sequential Cohesion)
한 클래스 내에 등장하는 하나의 소작업(메쏘드)의 결과가 다음 소작업(메쏘드)의 입력으로 사용되는 관계(파이프라인 방식의 처리 체인 관계).
* 교환적 응집(Communicational Cohesion)
동일한 입력과 출력 자료를 제공하는 메쏘드들의 집합을 말하며, 팩토리 클래스는 전형적인 교환적 응집도가 높은 인터페이스를 갖는다.
* 절차적 응집(Procedural Cohesion)
순서적으로 처리되어야 하는 소작업(메쏘드)들이 그 순서에 의해 정렬되는 응집관계
* 시간적 응집(Temporal Cohesion)
시간의 흐름에 따라 작업 순서가 정렬되는 응집관계
* 논리적 응집(Logical Cohesion)
유사한 성격의 개체들이 모여 있을 경우를 말하며 java.io 클래스들의 경우가 대표적인 예이다.

이와 반해 결합도는 ‘클래스간의 서로 다른 책임들이 얽혀 있어서 상호의존도가 높은 정도’를 의미하며 이들이 조합될수록 코드를 보기가 괴로워진다. 이유는 서로 다른 책임이 산만하고 복잡하게 얽혀있기 때문에 가독성이 떨어지고 유지보수가 곤란해지기 때문이다. 이유는 필요 없는 의존성에 있다. 마치 키보드의 자판 하나가 고장나도 키보드 전체를 바꿔야 하는 것처럼. 하나의 변경이 엄청난 민폐를 야기하는 관계이다. 다음은 수용할 수 있는 수준의 결합 관계들이다.

* 자료 결합(Data Coupling)
두 개 이상의 클래스가 매개변수에 의해서 결합 관계를 가지므로 낮은 수준의 결합도로 연관되는 경우
* 스탬프 결합(Stamp Coupling)
자료 결합의 경우에서 매개변수 일부만을 사용하는 경우
* 제어 결합 (Control Coupling)
두 클래스간의 제어 이동이 매개변수를 이용하여 사용되는 경우로 커맨드 패턴이 대표적인 사례이다(지난 연재 기사 OCP 참조).
사용자 삽입 이미지
사용자 삽입 이미지

즉 순서는 ‘1. 우선 key에 해당하는 객체를 식별자 맵에서 찾는다( 1.1. 없다면 DB에서 얻어온다. 1.2. 식별자 맵에 등록한다). 2. 리턴한다’'의 절차를 밟는다. 마치 싱글톤 패턴처럼 한 번의 접근을 보장함으로써 DB 접근 비용을 감소시킨다.

이와 더불어 실제 엔터프라이즈 애플리케이션 설계 시 데이터 접근의 동기화, 동시성 처리, 풀링, O/R 맵핑 등의 ‘책임’들이 등장하게 되는데 이 각 책임들을 담당하는 클래스를 분리시키고 이들 간의 관계를 잘 정의할수록 복잡도는 감소하고 아키텍처는 깨끗해진다.

현재 제공되고 있는 DB 관련 프레임워크는 이 책임들을 맡는 기능을 구현한 녀석들이다. SRP의 효과가 바로 여기에 있다. SRP를 적용하면 무엇보다도 책임 영역이 확실해지기 때문에 한 책임의 변경에서 다른 책임의 변경으로의 연쇄작용에서 자유로울 수 있다.

하지만 무조건 책임을 분리한다고 SRP가 적용되는 건 아니다. 가령 데이터 맵퍼 클래스의 메쏘드들이 각각의 insert, delete, update, load 클래스로 분리됐을 경우를 생각해 보자. 마치 절차적 언어에서와 같은 함수 단위의 클래스가 될 것이다. 각 메쏘드 역할에 따른 책임들이 분리되었지만 설계는 장황해지고 관계는 복잡해진다. 하지만 이 문장은 틀린 문장이다. 동일한 책임을 갖는 여러 메쏘드들이 분리된 것이다. 즉 분리의 기준은 책임이며 분리의 목적은 복잡도 감소에 있다.

반면 각각 분리된 insert, delete, update, load 클래스들이 있다고 했을 때 이들은 한 책임아래 병합되어야 할 것이다. 왜냐하면 이들은 병합될수록 관계는 단순해지고 설계가 일목요연해지기 때문이다. 그렇다면 이 서로 상반된 관계, 즉 어떤 경우는 분리를 할수록 설계가 깨끗해지고, 반대로 어떤 경우는 병합을 할수록 설계가 깨끗해지는데 이 차이는 무엇일까?

각 개체 간의 응집력이 있다면 병합이 순작용의 수단이 되고 결합력이 있다면 분리가 순작용의 수단이 된다. 응집력이 있다는 것은 여러 개체가 같은 책임아래 있다는 것을 의미하며 결합력이 있다는 것은 한 개체가 여러 책임을 갖고 있다는 의미가 된다. 따라서 응집도는 높을수록, 결합도는 낮을수록 좋은 설계 품질을 보장받는다.

SRP 위반의 악취들
SRP는 하나의 객체가 하나의 책임, 즉 하나의 ‘변경의 이유’만 지니게 함으로써 설계를 단순하게 하고 변화에 기민하게 만들어 준다. 필자는 서두에 SRP 위반했을 때 재앙을 악취로도 소개했는데 이번에 소개할 악취들은 『리팩토링』에서 구체화된 악취들이다. SRP 위반의 악취는 ‘여러 원인에 의한 변경(divergent change)’와 ‘산탄총 수술(shotgun surgery)’을 들 수 있다.

여러 원인에 의한 변경
여러 원인에 의한 변경은 한 클래스를 여러 가지 다른 이유로 고칠 필요가 있을 때 발생한다. 즉, 하나의 클래스에 여러 책임이 혼재하고 있어서 하나의 책임의 변화가 다른 책임에게 영향을 준다. 그리고 이 책임이 두 개보다 훨씬 많은 여러 개로 혼재된다면 이 클래스는 심각한 고문관이 된다. 더욱이 이 구조는 더 괴로운 경우로 심화될 수 있다.

<그림 1>의 ‘잔고 클래스’에서 ‘환율 계산’이란 책임의 변화로 다른 금액 관련 책임들의 인터페이스까지 칼을 대야 할 상황에 이른다면 작은 ‘환율 계산’의 변경이 전혀 무관한 금액 관련 인터페이스를 이용하는 모든 사용자 클래스까지 변경해줘야 하는 것이다. 이런 위험 상황을 소개한 것이 ‘여러 원인에 의한 변경’이라는 나쁜 냄새이다.

‘여러 원인에 의한 변경’을 해결하는 방법은 경우에 따라 다르다. 다음은 여러 원인에 의한 변경을 해결하는 리팩토링 기법이다.


◆ Extract Class는 혼재된 각 책임을 각각의 개별 클래스로 분할하여 클래스 당 하나의 책임만을 맡도록 하는 것이다. 액티브 오브젝트 패턴에서 데이터 맵퍼 패턴으로의 진화가 대표적인 사례가 된다. 여기서 관건은 책임만 분리하는 것이 아니라 분리된 두 클래스간의 관계의 복잡도를 줄이도록 설계하는 것이다.

◆ 만약 Extract Class된 각각의 클래스들이 유사하고 비슷한 책임을 중복해서 갖고 있다면 Extract Superclass를 사용할 수 있다. Extract Class된 각각의 클래스들의 공유되는 요소를 부모 클래스로 정의하여 부모 클래스에 위임하는 기법이다. 따라서 각각의 Extract Class들의 유사한 책임들은 부모에게 명백히 위임하고 다른 책임들은 각자에게 정의할 수 있다.



사용자 삽입 이미지
여러 원인에 의한 변경
사용자 삽입 이미지
 
사용자 삽입 이미지
윤성준, 조재박 님이 번역하고 대청미디어에서 출간된 마틴 파울러의『리팩토링』 한국어판을 보면 여러 원인에 의한 변경(divergent change)이 ‘확산적 변경’이라 되어 있다. 하지만 확산적 변경이라 했을 때는 변경이 다른 클래스로 확산된다는 느낌이 강하고, 클래스가 여러 변경 원인에 의해 변경된다는 느낌이 언뜻 오지 않는다.

그래서 이번 글에서는 divergent를 수렴(convergent)에 대응하는 의미가 아닌 ‘다른’의 뜻으로 세기고 divergent change를 ‘여러 원인에 의한 변경’이라 하겠다. They hold divergent opinions on controversial issues like abortion(그들은 낙태와 같은 논란이 되는 이슈에 대해 서로 다른 의견을 주장했다)에서의 divergent 용례를 참고하면 될 듯하다. 흔히들 ‘확산적 변경’과 뒤에 소개할 ‘산탄총 수술’을 잘 구분하지 못하는데 ‘확산적 변경’을 ‘여러 원인에 의한 변경’으로 바꾸어 이해하면 이 둘의 구분이 조금 더 직관적으로 다가올 것이다.
사용자 삽입 이미지
사용자 삽입 이미지

산탄총 수술
산탄총을 발사하면 하나의 탄환이 부서지면서 여러 개의 탄환으로 확산되어 발사된다. 따라서 (상상하기도 싫지만) 산탄총을 맞은 대상의 총상은 온몸 전체에 퍼지게 된다. 만약 이런 환자를 수술하는 의사는 마치 수십 발의 총을 맞은 환자를 수술하는 것처럼 힘들 것이다.

‘산탄총 수술(shotgun surgery)’은 ‘여러 원인에 의한 변경’과 비슷한 듯 하면서도 정 반대의 내용을 갖는다. ‘여러 원인에 의한 변경’이 하나의 클래스가 여러 변경 원인(책임)을 지니는 반면, 산탄총 수술은 어떤 변경이 있을 때 여러 클래스를 수정해야 하는 증상이다. 즉 어떤 변경의 대상이 여러 곳에 분포되어 마치 산탄총 총상 환자를 수술해야 하는 것 같은 많은 노동비용이 따른다.

‘산탄총 수술’이 괴로운 이유는 단지 수술 부위가 많다는 것만이 아니다. 이 수술을 했음에도 불구하고 혹시 치료하지 못한 상처가 존재할 수 있다는 가능성이 ‘산탄총 수술’의 더 큰 위험성이다. 가령 하나의 테이블을 조작하는 DB 처리문이 애플리케이션 전역에 퍼져 있는 상황에서 DB 테이블의 구조가 바뀌게 됐을 경우에 발생하는 재앙과 같다. 수술도 고되지만 모든 환부를 찾아야 하는 집중력과 긴장감이 개발자를 더욱 힘들게 한다.

사용자 삽입 이미지
<그림 5> 산탄총 수술


산탄총 수술이란 악취는 하나의 책임이 여러 클래스에 분산되어 있기 때문에 발생한다. 한 클래스가 너무 많은 책임을 맡고 있어도 곤란하지만, 책임을 식별하지 못해 이를 담당할 클래스를 만들지 않고 여러 클래스에 흩뿌려 놓는 것 또한 문제가 있다. 이는 보통 프로그램의 전체 책임을 올바로 분담하지 못해서 발생하게 된다.

이 악취는 Move Field와 Move Method를 통해 책임을 기존의 어떤 클래스로 모으거나, 이럴만한 클래스가 없다면 새로운 클래스를 만들어 해결할 수 있다. 즉 산발적으로 여러 곳에 분포된 책임들을 한 곳에 모으면서 설계를 깨끗하게 한다. 즉 응집성을 높이는 작업이 필요하다.

산탄총 수술의 냄새는 특히 설정 정보(configuration information), 로깅(logging), DB 처리에서 발생하기 쉬운데 이들을 다룰 때는 항상 산탄총 수술의 악취를 경계해야 한다. 예를 들어 한 곳에서 관리할 필요가 있는 설정 정보를 여러 클래스에서 나누어 처리하고 있다면 이는 산탄총 수술을 할 수 있는 좋은 본보기가 된다.

이를테면 쓰레드, 커넥션, 오브젝트 풀의 크기 값이나 DB, 서버의 주소 정보들을 각각의 클래스에 자체적으로 관리하고 있다면 이들을 설정 파일이나 설정 관리자에게 Move Field하는 것이 바람직하다. 더 나아가 플러그인을 도입해 설정 정보를 통해 동적으로 행위 변화를 통제(Enable Configurable Behavior with Plugin)하는 것도 생각해 볼만하다. 또한 XML 처리나 프로토콜 해석을 담당하는 메쏘드가 여러 곳에 분포되었다면 각각의 유틸성 클래스로 Move Method하는 것이 바람직하다.

‘여러 원인에 의한 변경’과 ‘산탄총 수술’이란 악취를 SRP를 어긴 신호로 여기고 제거한다면 변경이 여러 곳으로 확산되지 않을 뿐 아니라 책임을 적절히 분배함으로 인해 코드의 가독성 향상, 유지보수 용이라는 이점까지 누릴 수 있다. 또한 적절한 책임 분배는 객체지향 원리들의 대전제 격인 OCP뿐 아니라 다른 원리들을 적용하는 기초가 되어준다.

사용자 삽입 이미지
핵심 리팩토링 기법과 책임 분배
사용자 삽입 이미지
 
사용자 삽입 이미지
리팩토링에서 소개하는 대부분의 냄새들(코멘트, 비대한 클래스에서 시작해 산탄총 수술까지)은 객체간의 책임 분배와 직간접적으로 관련이 있는데, 항상 코드를 최상으로 유지한다는 리팩토링의 근본 정신은 곧 항상 객체들의 책임을 최상의 상태로 분배한다는 것이기 때문이다. 그리고 다음의 리팩토링 기법들은 ‘객체들의 책임을 최상의 상태로 분배’하는 가장 기본이 된다.

Extract Class(6)

Move Method(6)

Extract Method(4)

Move Field(4)

이들은 가장 많은 냄새를 해결하는 리팩토링 기법들을 정리한 것으로 괄호 안의 숫자는 냄새를 해결하는 데 사용된 빈도수이다. 고급스럽고 우아한 고급 리팩토링 기법이나 빅 리팩토링 기법은 때때로 유용하지만, 이들은 항상 유용하다.

필자는 앞의 4개의 리스트에 ‘Rename *’를 추가하여 5개의 리팩토링 기법을 가장 단순하면서도 중요한 핵심 리팩토링 기법으로 여긴다. 파울러의 책에는 Rename Method만이 수록되어 있지만, Rename Class, Rename Field, Rename Parameter 역시 굉장히 자주 쓰이며 실제 이클립스의 리팩토링 기능을 보면 이 모두가 하나로 Rename이라 되어 있다. 그리고 경험에 비추어 보면 어느 리팩토링 기능보다도 Rename을 자주 사용하게 된다. 올바른 클래스 이름은 해당 클래스의 책임을 나타낼 수 있는 가장 좋은 방법이다.

코딩을 하면서 키보드 앞에 A4 용지를 놓고 수시로 CRC 카드, 클래스 다이어그램, 시퀀스 다이어그램을 간략히 그려보며 객체들 간의 책임을 점검해 보고, 리팩토링을 수행하는 것은 분명 좋은 SRP를 어기는 코드를 사전에 어느 정도 방지할 수 있는 좋은 대비책이 되어줄 것이다. 할 수만 있다면, ‘사후 대처’보다는 ‘사전 예방’이 더 좋은 해결책이지 않은가?
사용자 삽입 이미지
사용자 삽입 이미지


위대함보다 단순함은 없다
눈동자의 초점이 분산되면 난시가 되고 마음이나 정신의 초점이 정상을 상실하고 분산하는 상태가 지속되면 정신착란 혹은 정신 분열이 온다. 마찬가지로 클래스가 하나의 책임에 집중하지 못한 채 이것저것 두리번거리는 여우와 같고, 이러한 클래스들이 누적된다면 프로그램은 점점 유지보수, 변경에 대응하기 어려워진다. 어쩌면 ‘툭’하고 잘못 건드리면 정신착란이 올지도 모를 일이다.

소프트웨어는 항상 변경을 전제한다. 따라서 변경에 민감하게 반응하지 못하는 설계는 프로젝트를 힘들게 한다. “소프트웨어 설계에는 두 가지 방법이 있다. 한 가지는 분명히 결함이 없도록 단순하게 설계하는 것이고 다른 한 가지는 분명한 결함이 없도록 가능하면 복잡하게 설계하는 것이다"라는 명언이 있다.

SRP를 적용하면 클래스의 숫자가 늘 수는 있다. 하지만 클래스 숫자의 증가가 프로그램의 복잡도 증가와 비례하는 것은 아니다. 오히려 SRP를 잘 따르는 프로그램은 적절한 책임 분배로 인해 클래스 숫자와 프로그램의 복잡도가 반비례하는 경향이 있다고도 할 수 있게 된다.

위대함보다 단순함은 없다. 실제로 단순한 것이 위대한 것이다. 그리고 이 단순함의 중심에는 단일 책임의 원칙이 있다.@

* 이 기사는 ZDNet Korea의 제휴매체인마이크로소프트웨어에 게재된 내용입니다.

 

저자 :최상훈 (핸디소프트), 송치형 (서울대)

2005/04/25

 

사용자 삽입 이미지

 

원문 :http://www.zdnet.co.kr/techupdate/lecture/etc/0,39024989,39135552,00.htm

  Comments,     Trackbacks