»
S
I
D
E
B
A
R
«
단일 책임 원칙: 그 단순함과 복잡함
Feb 10th, 2014 by Wegra Lee

(원문) SRP: Simplicity and Complexity

Effective Unit Testing의7.1.2절에서 인용한 블로그 글이다.

단일 책임 원칙: 그 단순함과 복잡함

단순한 것이 차차 복잡해지는 것이 아니다. 오히려 그 반대다. – 알랜 퍼리스(Alan Perlis)

SOLID 원칙 중 하나인 “단일 책임 원칙(SRP, The Single Responsibility Principle)”은 내가 가장 좋아하는 객체지향 설계 원칙 중 하나다. 나는 이로부터 단순함과 복잡함을 동시에 발견할 수 있었다. 이 원칙을 알게 된 게 수 년 전이고, 직접 관련 글을 써본 지도 일 년이 넘었다. 하지만 이를 실무에 적용하기란 여전히 쉽지만은 않다. 수년간 여러 개발자에 이 원칙을 전파하면서 흥미로운 사실을 발견했다. 모두가 이 원칙의 의미는 이해하고 있지만, 막상 코딩할 때에는 까맣게 잊어버리거나 적용하는 법을 몰라 헤매는 것이다. 그래서 이 문제를 바라보는 나의 관점과 지금껏 겪어왔던 경험을 이야기해보려 한다.

외적 측면 (참고: 책에서는 ‘외면’이라 번역함)

모든 클래스와 메서드는 단 하나의 역할만 수행해야 한다.

이것은 내가 “외적 측면”이라 부르는 것으로, 이름이 중요한 상황이다.

외적 측면은 새로운 코드를 작성할 때 주로 고려된다. 무엇을 하는 코드인지 생각해본 후, 클래스나 메서드를 작성하고, 새 코드가 하는 일을 잘 표현하는 이름을 지어준다. 코드를 읽을 때도 외적 측면이 작용한다. 이름만 봐도 클래스나 메서드가 무슨 일을 하는지 바로 알 수 있어야 한다.

여기까지 이해하는 데 아무런 무리가 없으리라 믿는다.

그렇다면 이처럼 이해하기 쉬운데도, 사람들은 왜 이를 잘 지키지 않을까? 왜 적절한 클래스와 메서드를 찾으려 수천 줄의 코드 속에서 헤매고 있는 걸까? 그럴싸한 이유가 몇 개 떠오른다. 아마도 우리는 작명에 소질이 없나 보다. 혹은 너무 일반적이거나 광의적인 이름을 써서 너무 많은 일을 한꺼번에 처리하고 있을지도 모른다. 그것도 아니면 그냥 신경 쓰지 않는 것일 지도.

내적 측면 (참고: 책에서는 ‘내면’이라 번역함)

클래스와 메서드를 수정해야 하는 이유는 오직 하나뿐이어야 한다.

이는 내가 “내적 측면”이라 칭하는 것으로, 단일 책임 원칙의 또 하나의 (자주 잊히는) 측면이다.

내적 측면은 기존 코드를 변경하거나 새로운 코드를 집어넣으려 할 때 고려된다. 다시 말해, 각 클래스와 메서드가 무슨 일을 하는지 파악해야 하는 상황이다. 이때 분석해야 할 코드량이 예상보다 훨씬 많아 좌절할 때가 많은데, 주원인은 클래스나 메서드가 원래 해야 할 일보다 훨씬 많은 것을 처리하고 있기 때문이다.

하지만 외적 측면보다 내적 측면에 집중하면 단일 책임 원칙을 적용하기가 한결 쉬워진다. 즉, 역할에만 신경 쓰지 말고, 클래스나 메서드를 변경해야 할 이유가 몇 가지나 되는가를 항시 고민하자.

그렇다면, 언제 어디서 일이 틀어지는 것일까?

노트: 대게 개발자가 TDD와 리팩토링을 하지 않는 조직일수록 단일 책임 원칙에 어긋나는 사례가 많다.

생각해보자. 경험상 단일 책임 원칙에 어긋나는 사례 대부분은 시스템 인터페이스에 가까운 클래스와 메서드에서 발견되었다. 예를 들면, 웹 애플리케이션의 거대한 컨트롤러나 액션 클래스, 스윙 애플리케이션의 거대 이벤트 핸들러, 이벤트 기반 시스템에서 메시지 처리를 담당하는 거대한 메서드 등이 있다.

이는 시스템 인터페이스에 가까운 클래스와 메서드는 더 광범위하고 일반적인 역할을 담당하기 때문이다. 이런 메서드는 수많은 비즈니스 규칙이나 복잡한 워크플로우를 관장하는 경우가 제법 많다.

거래 정보를 담은 거대한 XML 파일을 입력받는 시스템을 상상해보자. 그리고 이를 처리하는 첫 메서드는 TradeService 클래스의 “processTrade(tradeXML)”라고 해보자. 이 메서드의 역할이 무엇인가? 바로 거래를 처리하는 것이다. 그렇다면 이름은 적절한가? 이 시스템은 입력받은 거래(trade)를 처리(process)하길 원하니 첫 메서드의 이름으로 processTrade는 적절해 보인다.

다른 예를 보자. 인터넷 쇼핑 사이트에서 고객이 상품 몇 개를 장바구니에 담고, 지불 정보를 입력하고, “주문” 버튼을 클릭했다. 그렇다면 뒷단에서는 주문 발주를 위해 대략 placeOrder(order) 정도의 메서드를 호출할 것이다. 나쁘지 않다.

생각 발전시키기

일반적으로, 시스템 인터페이스에 가까운 코드일수록 더 폭넓고 일반적인 역할을 담당하는 경향이 있다. 반면, 시스템 인터페이스에서 멀어질수록 더 좁고 특수한 역할을 담당하게 된다.

앞의 두 예에서, processTrade와 placeOrder 메서드는 단 하나의 역할만 수행한다고 주장할 수 있다. 전자는 입력받은 거래를 처리하고 후자는 고객의 주문을 발주한다. 그래서 단일 책임 원칙의 외적 측만을 고려하는 개발자라면 거리낌 없이 관련된 코드 모두를 그 메서드 안에 욱여넣을 것이다.

문제는 거래 처리와 주문 발주가 심히 복잡한 작업이라는 데 있다. 복잡한 과정을 거쳐야 하고, 수많은 비즈니스 규칙과 요구사항을 만족하게 하기 위한 수백 수천 줄의 코드를 작성해야 할 것이다. 그러니 이 많은 코드를 한 메서드에 욱여넣는 것은 단일 책임 원칙을 명백히 위반할 뿐 아니라, 어리석기까지 한 일이다.

단일 책임 원칙을 만족하는 코드를 만들려면, 변경 사유가 오직 하나뿐이어야 한다. 이는 다음과 같이 발전된 생각을 이끌어내 준다.

일반적으로, 시스템 인터페이스에 가까운 클래스일수록 더 많은 것을 위임(delegation)한다. 반면, 시스템 인터페이스에서 멀리 떨어진 클래스일수록 위임할 것이 적어진다.

전통적인 자바 웹 애플리케이션의 컨트롤러가 좋은 예다. 사용자 인터페이스와 가까운 컨트롤러는 폭넓은 역할을 담당하며 비즈니스 로직은 모두 다른 객체에 위임한다. 컨트롤러는 단순히 흐름만 제어할 뿐이다. 정 반대로, 매우 특수하고 제한된 역할만 수행하는 DAO(Data Access Object)는 일거리를 다른 클래스에 위임하는 경우가 거의 없다. 그 중간에 위치하는 서비스는 자신의 비즈니스 로직을 처리하지만, 종종 다른 협력 객체에 작업을 위임하기도 한다. 서비스는 컨트롤러보다는 좁고 DAO보다는 광범위한 역할을 처리하는 게 보통이다. 어쨌든, 각 클래스와 메서드는 단일 역할만을 담당한다.

다르게 질문하기

다른 개발자에게 맨토링해주거나 함께 짝(pair) 프로그래밍을 하며 한 메서드 안의 코드량이나 클래스 안의 메서드 수를 가지고 논쟁을 벌일 때가 많다. 단일 책임 원칙의 외적 측면만을 근거로 내세운다면 코드가 하는 일이 너무 많다는 걸 잘 인정하지 못하는 개발자가 많을 것이다. 그래서 내적 측면이 중요하다는 걸 깨달았다. 이제는 메서드나 클래스가 맡은 역할이 몇 개인가를 묻는 대신, 수정해야 하는 이유가 몇 가지나 되느냐고 묻기 시작했다.

JUnit 4, 60초만에 익히기
Jul 3rd, 2011 by Wegra Lee

JUnit in Action 책 출간 기념으로 짤막한 글 하나 번역해본다.

원문: JUnit 4 in 60 Seconds

이번 주말에 JUnit 4 라이브러리를 살펴보고 간단히 정리해보았다.

1. @Test

테스트 케이스임을 명시하기 위해 사용한다. “test”와 같은 접두어를 붙일 필요가 없어졌다. 또한 테스트 클래스 역시 더 이상 “TestCase” 클래스를 확장할 필요가 없어졌다.

  1. @Test
  2. public void addition() {
  3. assertEquals(12, simpleMath.add(7, 5));
  4. }
  5. @Test
  6. public void subtraction() {
  7. assertEquals(9, simpleMath.substract(12, 3));
  8. }

2. @Before and @After

@Before와 @After 애너테이션을 사용하여 “setup”과 “testDown” 메서드 명시한다. 이 메서드들은 각각의 테스트 전후로 실행된다.

  1. @Before
  2. public void runBeforeEveryTest() {
  3. simpleMath = new SimpleMath();
  4. }
  5. @After
  6. public void runAfterEveryTest() {
  7. simpleMath = null;
  8. }

3. @BeforeClass and @AfterClass

@BeforeClass와 @AfterClass는 클래스 단위의 “setup”과 “teadDown” 메서드에 붙인다. 1회성 setup, tearDown 메서드라 생각하자. 클래스고 파홈한 모든 테스트케이스들의 전후로 단 1회만 실행된다.

  1. @BeforeClass
  2. public static void runBeforeClass() {
  3. // run for one time before all test cases
  4. }
  5. @AfterClass
  6. public static void runAfterClass() {
  7. // run for one time after all test cases
  8. }

4. Exception Handling (예외 처리)

예외 발생이 예상되는 테스트 케이스에는 @Test 애너테이션에 “expected” 파라미터를 사용한다. 발생해야할 예외의 클래스 명을 적어두면 된다.

  1. @Test(expected = ArithmeticException.class)
  2. public void divisionWithException() {
  3. // divide by zero
  4. simpleMath.divide(1, 0);
  5. }

5. @Ignore

무시하고자 하는 테스트 케이스에는 @Ignore 애너테이션을 붙인다. 무시하는 이유도 기입해 넣어주면 좋다.

  1. @Ignore(“Not Ready to Run”)
  2. @Test
  3. public void multiplication() {
  4. assertEquals(15, simpleMath.multiply(3, 5));
  5. }

6. Timeout (시간 제한)

제한 시간이 필요하면 “timeout” 파라미터를 이용한다. 단위는 밀리초이다. 테스트가 제한시간 내에 완료되지 못하면 실패 처리된다.

  1. @Test(timeout = 1000)
  2. public void infinity() {
  3. while (true)
  4. ;
  5. }

7. 새로운 단언들

배열 비교에 쓰일 수 있는 새 단언 메서드들이 추가되었다. 배열의 길이가 같고, 각 원소들이 대상 배열의 대응되는 원소들과 같아야(equal) 한다.

  • public static void assertEquals(Object[] expected, Object[] actual);
  • public static void assertEquals(String message, Object[] expected, Object[] actual);
  1. @Test
  2. public void listEquality() {
  3. List<Integer> expected = new ArrayList<Integer>();
  4. expected.add(5);
  5. List<Integer> actual = new ArrayList<Integer>();
  6. actual.add(5);
  7. assertEquals(expected, actual);
  8. }

8. JUnit4Adapter

JUnit 3에서 JUnit 4용 테스트를 실행하려면 JUnit4Adapter를 사용한다.

  1. public static junit.framework.Test suite() {
  2. return new JUnit4TestAdapter(SimpleMathTest.class);
  3. }

자! 그럼 모두 즐코딩~ ^^

»  Substance: WordPress   »  Style: Ahren Ahimsa