테스트 상태인 Private 메소드를 Public메소드로 변환시 Unit Testing은 어떻게 해야하나?

2014-01-28 15:23

아래와 같은 상황인데 조언 좀 구할 수 있을까요?

예를들어, 아래와 같이 Statistics라는 class가 mean이라는 함수를 가지고 있습니다. 이 mean은 TDD를 통해서 잘 구현되어졌다라고 가정합시다(100% code coverage).

public class Statistics{
	public double mean(double[] data){
		return sum(data)/data.length;
	}


	private double sum(double[] data){
		...
	}
}

mean을 구현하고 refactoring과정에서 sum이라는 private 함수가 도출되었습니다. 이말을 달리 표현하면 'sum private메소드는 public API인 mean 메소드를 통해 테스트되었다'라고 할 수 있을 것 같습니다.

문제는 여기서, Statistics에서 sum이라는 메소드가 추가되어야 한다는 요구사항이 들어왔습니다. 이때 private인 sum 메소드를 public으로 바꿔준다면 끝나는 것일까요? 아니면 sum에 대한 unit-testing이 추가되어야 할까요?

더 나아가, 분명 sum 메소드는 테스트된 상태이므로, public으로 선언하면 끝날 것 같지만, 만약 다른 프로젝트에서 필요로 하는 함수라면 어떻게 해야 할까요? sum메소드를 복사해서 다른 프로젝트로 붙이면 sum 메소드는 전혀 test가 되지 않는 상황이 되어버립니다.

질문에서는 단순한 sum메소드로 말씀드렸는데, 실제는 복잡한 private 메소드를 다른 프로젝트의 public 메소드로 옮겨야 하는 상황인데 관련 테스트까지 옮길려니 쉽지가 않네요.

3개의 의견 from FB

BEST 의견 원본위치로↓
2014-02-02 16:24

단위 테스트를 하다보면 private method와 관련한 테스트에 대해 의문을 가지는 시점이 오는 듯 합니다. 프로그래밍이라는 것이 정답이 없는 경우가 많기 때문에 힘든 작업이지만 그렇기 때문에 재미있는 일이라는 생각을 합니다.

private method에 대한 단위 테스트를 할 필요가 있느냐 없느냐에 대한 결정은 프로그래밍을 하는 시점의 Context에 따라 다를 수도 있다는 생각이 드네요. 즉, 위 예의 경우 mean method에 전달하는 값의 합을 구한 후 평균을 구하고 있는데요. mean이라는 의미내에 sum이 모두 포함되어 있다면 sum은 mean method를 통해 단위 테스트할 수 있다고 봅니다. 즉, sum이 mean에 종속되는 경우 mean을 통해서 테스트해도 문제가 없을 것이라 봅니다. mean이 mean method 내부의 의도를 모두 드러내지 못한다면 mean method의 이름을 리팩토링하는 것이 맞겠죠. mean과 sum을 분리해서 생각한다면 sum에 대한 단위 테스트가 필요할 수도 있다고 봅니다. 이럴 경우 mean에 전달하는 인자 값은 sum method를 활용해 합산한 값, 길이가 인자로 전달할 수 있지 않을까요? 즉, mean과 sum의 coupling 정도에 따라 단위 테스트의 필요 유무를 결정할 수 있지 않을까 생각합니다.

최초 개발에서는 sum이 mean에 강한 의존관계를 가지다가 이후 여러 곳에서 중복해서 사용하는 상황이 발생한다면 이 시점에 sum에 대한 단위 테스트를 추가해도 되지 않을까라는 생각을 합니다. 이 경우 sum이 Statistics 클래스에 위치하는 것이 아니라 완전히 별개의 Sum이라는 클래스로 분리될 수도 있고, 새로운 클래스의 메소드로 이동할 수도 있다고 봅니다.

정리해보면 이런 상황이 되지 않을까 생각하네요.

  • TDD 기반으로 mean method를 구현한다.
  • mean method의 복잡도가 높아보여 extract method refactoring을 활용해 sum method를 분리한다. sum에 대한 별도의 단위 테스트를 구현하지 않는다. mean method를 통해 모든 의도를 드러나고 있다고 생각한다.
  • sum method가 mean method가 아닌 다른 곳에서 활용된다. 이 시점에 sum method를 다른 클래스로 이동한다. move method refactoring을 활용할 수 있겠다. 이 시점에 sum method에 대한 단위 테스트를 추가한다.

이 같은 활동은 지속적으로 이루어져야 하겠죠. 즉, 소스 코드를 구현하는 시점의 Context를 먼저 고려해야하지 않을까 생각합니다.

private method에 대한 단위 테스트가 필요하다고 느끼는 순간 무엇인가 bad smell이 있는 것은 아닐까라는 고민을 할 수 있는 시점이라고 봅니다. 즉, sum method가 Statistics 클래스에 위치하는 것이 맞지 않고 별도의 클래스로 이동해야 하는 것이 아닐까라는 고민을 할 수 있겠죠. Sum이라는 클래스를 추가할 수도 있는지를 검토해 볼 수 있는 것이겠죠. method의 위치를 이동하는 순간 private이던 method가 public으로 변경될 가능성도 높아질 겁니다. 그 시점에 단위 테스트를 추가할 수 있겠죠.

저도 비슷한 고민을 한 적이 있는데요. http://www.slipp.net/wiki/pages/viewpage.action?pageId=6160426 글에 정리되어 있으니 한번 읽어 보시기 바랍니다. 이 글이 최종적으로 어떻게 발전되어 왔는지에 대해 그 과정을 보시면 좋겠습니다. @Jin-Wook Chung 님이 지금까지 생각하던 방식과는 완전히 다른 방식으로 접근하는 부분도 있을 수 있습니다.

글 읽어 보시고 저의 부족한 부분이 있으면 마음껏 글 남겨주세요. 온라인 상에서 이 같은 논의가 즐겁네요.

14개의 의견 from SLiPP

2014-01-28 16:57
  1. 테스트는 클래스의 역할을 나타내주는 역할도 하기 때문에 저는 public이냐 private이냐를 떠나서 요구사항에 따라 테스트가 바뀌어야 합니다.

  2. 또, 기능상으로는 mean이든 sum이든 테스트되었다고 볼 수 있지만, 이러한 컨텍스트가 공유되지 않은 새로운 개발자는 혼란스러울 수 있습니다.

더 나아가서 하신 질문도 1, 2번 답변으로 해결될 것 같습니다. ^^

2014-01-28 17:08

@dongkuk

  1. 테스트는 클래스의 역할을 나타내주는 역할도 하기 때문에 저는 public이냐 private이냐를 떠나서 요구사항에 따라 테스트가 바뀌어야 합니다.

이 말씀은 sum메소드에 대한 단위 테스트를 추가로 작성해야 한다는 말씀이죠? 저도 이부분에 대해 전적으로 동의하지만, 다른 쪽에서는 이미 테스트된 기능을 추가로 테스트 한다는 것이 비효율적이다라는 주장을 할 수 있을 것 같습니다.

2014-01-29 00:58

@Jin-Wook Chung 다른쪽에서 비효율적이다 라고 주장한다면 전 지금 당장은 그렇게 보일 수도 있지만 장기적인 관점 또 팀원의 관점에서는 이게 더 효율적이라고 말하고 싶습니다.

먼저 장기적인 관점에서는 언젠가 리팩토링이 필요한 시기에 더 빨리 그리고 더 쉽게(복잡성 해결)개발할 수 있을 거라고 봅니다. 테스트가 없다면 나중에는 이 코드가 왜 있는지 판단하는데 제일 많은 시간을 쓰게 될겁니다. 본인이 만들었지만 다른 사람이 개발하고 테스트도 안넣었다고 불평하실 수도 있을 거 같아요:) 인간은 망각의 동물이니까요 ㅎㅎ

또 다른 팀원들은 질문자님이 겪고 있는 컨텍스트에 대해 전혀 알지 못합니다. 최악의 경우에는 테스트 코드가 없으니 없어도 되나보다 하고 지워버릴 겁니다. 물론 지우고 나서도 테스트는 초록불을 뿜어내고 있겠죠. 결국엔 배포전에 버그를 잡아내기는 하겠지만 이런 과정은 거치지 않는게 효율적이지 않을까요? 또 저런 사고가 한번 터지면 팀원의 코드에 대한 신뢰도 떨어질겁니다.

2014-01-29 12:09

@dongkuk 답변 감사드립니다.

질문을 두가지로 나눠서 생각해야할 것 같습니다. sum 메소드가 동일 프로젝트에 있을 경우와 그렇지 않고 다른 프로젝트로 복사될 경우, 이렇게 나눠생각한다면, 후자의 경우는 분명 테스트없기 때문에 테스트가 작성되어야 할 것입니다. 제가 비효율적이라고 언급한 부분은 전자의 경우로 sum 메소드가 테스트하에 있어서 sum메소드가 잘못되면, mean에 관한 테스트에서 빨간불을 뿜는다는 것입니다. 물론 sum메소드가 잘못되었을 경우, sum에 대한 테스트가 있어면 실패의 원인을 규명하는데 용이할 것입니다.

2014-01-30 21:40

@Jin-Wook Chung 질문을 두가지로 잘 나눠주셔서 의사소통이 훨씬 수월할 것 같습니다 :) 제 의견만 먼저 말씀드리면, sum 메소드가 동일 프로젝트에 있을 경우와 그렇지 않고 다른 프로젝트로 복사될 경우, 둘다 sum 메서드에 대한 테스트가 필요합니다. 후자에 있어서는 의견이 일치하는거 같아요 ^^

전자의 경우, '클래스의 기능이 잘 돌아가는가'를 확인하는 용도로 테스트를 사용하는 관점에선 sum에 테스트를 넣는게 비효율적일 수 있어요. 물론 sum과 같이 한번에 무슨 일을 하는지 알 수 있는 경우는 더더욱 테스트를 넣지 않아도 될 것 같아요.

하지만 '클래스가 어떤 역할을 수행하는가'를 확인하는 용도로 테스트를 사용하는 관점에선 달라질 것 같습니다. 기능과 상관없이 이젠 Statistics 클래스는 mean과 sum의 역할을 수행합니다. 이를 나타내기 위해서 mean 테스트와 sum 테스트를 각각 넣어야 합니다.

제가 이렇게 테스트는 클래스의 기능 뿐만 아니라 역할을 드러내야 한다고 강조하는 것은, 같이 일하는 동료 & 소스코드 관리를 위해서입니다. 다른 개발자분들은 위에서 언급했듯이 질문자님께서 고민하시는 컨텍스트를 전혀 알지 못합니다. 따라서 누군가는 분명 클래스의 역할을 오해할 것입니다. 물론 미래의 자기 자신을 포함해서요. 이렇게 테스트로 클래스의 역할을 분명히 나타내준다면 소스코드가 좀더 아름답게 유지될 수 있을 것 같습니다.

2014-02-02 16:24

단위 테스트를 하다보면 private method와 관련한 테스트에 대해 의문을 가지는 시점이 오는 듯 합니다. 프로그래밍이라는 것이 정답이 없는 경우가 많기 때문에 힘든 작업이지만 그렇기 때문에 재미있는 일이라는 생각을 합니다.

private method에 대한 단위 테스트를 할 필요가 있느냐 없느냐에 대한 결정은 프로그래밍을 하는 시점의 Context에 따라 다를 수도 있다는 생각이 드네요. 즉, 위 예의 경우 mean method에 전달하는 값의 합을 구한 후 평균을 구하고 있는데요. mean이라는 의미내에 sum이 모두 포함되어 있다면 sum은 mean method를 통해 단위 테스트할 수 있다고 봅니다. 즉, sum이 mean에 종속되는 경우 mean을 통해서 테스트해도 문제가 없을 것이라 봅니다. mean이 mean method 내부의 의도를 모두 드러내지 못한다면 mean method의 이름을 리팩토링하는 것이 맞겠죠. mean과 sum을 분리해서 생각한다면 sum에 대한 단위 테스트가 필요할 수도 있다고 봅니다. 이럴 경우 mean에 전달하는 인자 값은 sum method를 활용해 합산한 값, 길이가 인자로 전달할 수 있지 않을까요? 즉, mean과 sum의 coupling 정도에 따라 단위 테스트의 필요 유무를 결정할 수 있지 않을까 생각합니다.

최초 개발에서는 sum이 mean에 강한 의존관계를 가지다가 이후 여러 곳에서 중복해서 사용하는 상황이 발생한다면 이 시점에 sum에 대한 단위 테스트를 추가해도 되지 않을까라는 생각을 합니다. 이 경우 sum이 Statistics 클래스에 위치하는 것이 아니라 완전히 별개의 Sum이라는 클래스로 분리될 수도 있고, 새로운 클래스의 메소드로 이동할 수도 있다고 봅니다.

정리해보면 이런 상황이 되지 않을까 생각하네요.

  • TDD 기반으로 mean method를 구현한다.
  • mean method의 복잡도가 높아보여 extract method refactoring을 활용해 sum method를 분리한다. sum에 대한 별도의 단위 테스트를 구현하지 않는다. mean method를 통해 모든 의도를 드러나고 있다고 생각한다.
  • sum method가 mean method가 아닌 다른 곳에서 활용된다. 이 시점에 sum method를 다른 클래스로 이동한다. move method refactoring을 활용할 수 있겠다. 이 시점에 sum method에 대한 단위 테스트를 추가한다.

이 같은 활동은 지속적으로 이루어져야 하겠죠. 즉, 소스 코드를 구현하는 시점의 Context를 먼저 고려해야하지 않을까 생각합니다.

private method에 대한 단위 테스트가 필요하다고 느끼는 순간 무엇인가 bad smell이 있는 것은 아닐까라는 고민을 할 수 있는 시점이라고 봅니다. 즉, sum method가 Statistics 클래스에 위치하는 것이 맞지 않고 별도의 클래스로 이동해야 하는 것이 아닐까라는 고민을 할 수 있겠죠. Sum이라는 클래스를 추가할 수도 있는지를 검토해 볼 수 있는 것이겠죠. method의 위치를 이동하는 순간 private이던 method가 public으로 변경될 가능성도 높아질 겁니다. 그 시점에 단위 테스트를 추가할 수 있겠죠.

저도 비슷한 고민을 한 적이 있는데요. http://www.slipp.net/wiki/pages/viewpage.action?pageId=6160426 글에 정리되어 있으니 한번 읽어 보시기 바랍니다. 이 글이 최종적으로 어떻게 발전되어 왔는지에 대해 그 과정을 보시면 좋겠습니다. @Jin-Wook Chung 님이 지금까지 생각하던 방식과는 완전히 다른 방식으로 접근하는 부분도 있을 수 있습니다.

글 읽어 보시고 저의 부족한 부분이 있으면 마음껏 글 남겨주세요. 온라인 상에서 이 같은 논의가 즐겁네요.

2014-02-03 15:32

테스트 커버리지 100%가 문제를 모두 발견했다고 말해주지는 않겠죠. 메서드의 목적과 의도, 잠재적 위험을 테스트를 통해서 드러내기에는 분리된 테스트가 더 좋을 것 같네요.

예를 들어 data.length가 1인 경우에 정상동작했지만 data.length가 0인 경우에는 문제가 발생할 수 있으니까요. mean()은 평균을 정상적으로 구하는지에 대한 테스트에 집중하는게 좋겠죠. data.length가 0일 때도 문제가 없을까?

@Test public void 평균구하기() {
    assertEquals(1, mean(dataLength1));
    assertEquals(0, mean(dataLength0));
}

sum()에 대해서는 어떤 테스트가 필요할까요? 역시나 경계조건에 대한 테스트가 가장 먼저 떠오르네요. 큰 값일 때 문제가 없을까?

@Test public void 합계구하기() {
    assertEquals(Double.MAX * 2, sum(maxDoubles));
}

비공개 메서드를 테스트해야 하는가보다는 현실적인 관점에서 만들어진 테스트가 얼마나 견고한지 혹은 마음에 안도가 생기는지를 경험해보는게 더 중요한 거 같네요. 비공개 메서드를 테스트하지 않아도 신뢰가 생긴다면 안해도 된다고 생각합니다. 다만 찝찝하고 불안한 마음이라면 어떻게 해서든 테스트를 추가하는게 정신건강에 좋겠죠. 저 같으면 외부에서 직접 사용하지 않는다고 하더라도 찝찝한 기능이라면 public으로 바꿔서라도 테스트해버리겠네요. 장애로 새벽에 일하는거보다는 나을거 같아서요.

그리고 한 모듈을 다른 프로젝트에 가져다쓸 때도 뭔가 번거로움이 많다면 context independent에 대해서 고민해보셔야 하겠네요. 쉬운 기능들이지만 context independent가 보장되지 않으면 테스트 작성의 어려움 뿐만 아니라 copy&paste reuse 조차도 짜증이 날 수 있죠 ㅎㅎ

저는 GOOS를 통해서 많은 도움을 받았어요. 이에 대한 논의도 있네요. http://permalink.gmane.org/gmane.comp.programming.goos/2842

2014-02-03 19:23

@dongkuk @자바지기 같이 고민해 주시고 좋은 답변 주셔서 감사드립니다. 처음에는 이런 질문 해도 되는지 조금 망설였지만, 질문하길 잘했다는 생각이 드네요.

전자 vs 후자의 경우

private sum 메소드를 다른 프로젝트로 복사하여 public으로 선언할 경우(후자의 경우)는 저의 질문의 대상이 되는 경우입니다. private sum메소드에 throw exception문을 걸어서 실패하는 테스트들 중에 관련된 테스트 하나를 다른 프로젝트로 sum메소드 테스트에 맞게 옮긴 후, 구현코드를 작성하는 식으로, 하나 씩, 하나 씩 해결하였습니다. 특별한 방법이 없는 것 같습니다.

private sum 메소드가 동일 프로젝트 내에서 public으로 선언될 경우(전자의 경우)의 단위테스트에 대해서는 @dongkuk님 답변이 정확한 지적이신 것 같습니다. {quote}'클래스의 기능이 잘 돌아가는가'를 확인하는 용도로 테스트를 사용하는 관점에선 sum에 테스트를 넣는게 비효율적일 수 있어요. 물론 sum과 같이 한번에 무슨 일을 하는지 알 수 있는 경우는 더더욱 테스트를 넣지 않아도 될 것 같아요.

하지만 '클래스가 어떤 역할을 수행하는가'를 확인하는 용도로 테스트를 사용하는 관점에선 달라질 것 같습니다. 기능과 상관없이 이젠 Statistics 클래스는 mean과 sum의 역할을 수행합니다. 이를 나타내기 위해서 mean 테스트와 sum 테스트를 각각 넣어야 합니다.{quote}

참고

StackOverFlow에 비슷한 질문을 찾아 보았습니다(아래링크). 채택된 답변이 @dongkuk님의 답변과 크게 다르지 않네요. 차이점이라면 @dongkuk은 specification 관점을 얘기하셨고, @Yishai이란 분은 organization 관점에서 얘기하신 것 같습니다. http://stackoverflow.com/questions/3402177/extracting-class-when-tdding-how-to-test-the-new-extracted-class

private 메소드 단위테스트

@자바지기님께서 저의 질문과 관련하여 private method 테스트에 관한 의견을 주신 것 같습니다. 사실 private method에 대한 단위 테스트를 하시는 분이 있을 수 있지만, 저는 public API를 통해서만 단위테스트가 이루어져야 한다는 의견에 찬성하는 입장입니다. 아마 @자바지기님도 public API를 통한 테스트에 찬성하는 입장이 아니실까 생각합니다. public API를 통해서 단위테스트가 이루어져야한다는 입장을 가지고 private 메소드 단위테스트에 관한 제 생각을 말씀 드릴까 합니다.(private 또는 default 접근자를 통해서 단위테스트가 이루어져서는 안된다는 견해입니다.)

질문에서 예로 든 통계클래스를 아래와 같이 다자인 했다고 가정합시다. 이때 합계메소드는 통계클래스내의 다른 public 메소드들에 의해 사용되고 있을 뿐아니라, 성적이라는 클래스에 의해서도 사용되고 있습니다. 이 경우 합계메소드를 public으로 선언하여 단위테스트를 할 경우와 그렇지 않은 경우를 비교하여 생각해 볼 필요가 있을 것 같습니다. 경우에 따라선 합계 메소드가 extract class 리팩토링이 필요할 수도 있겠습니다.

public class 통계{
	public double 분산(double[] data){
		// 합계 메소드 사용
		throw new NotImplementedException();
	}


	public double 평균(double[] data){
		// 합계 메소드 사용
		throw new NotImplementedException();
	}


	private double 합계(double[] data){
		throw new NotImplementedException();
	}
}


public class 성적{
	public int 등수(double[] data){
		// 합계 메소드 사용
		throw new NotImplementedException();
	}
}

private 메소드로 두어 직접테스트하지 않는 경우

합계메소드가 encapsulation되어서 public API에 영향을 받지 않습니다. extract class 리팩토링된다해도 테스트코드가 변경되는 일이 없습니다. 하지만, 공용로직에 의한 단위테스트 중복이 일어 날 수 있습니다. 가령, 분산, 평균 그리고 성적메소드에서 음의 수(data)가 넘어온다면 InvalidArgumentException 예외를 던져야한다는 시나리오가 있다면, 이 경우에서는 세 메소드 모두에 대한 단위테스트가 추가되어야 할 것입니다.

public 메소드로 두어 직접테스트할 경우(DI 사용)

public 메소드로 두어 직접테스트할 경우는 또 다시 dependency injection(DI)을 사용할 경우와 그렇지 않을 경우로 나눠야 할 것 같습니다.

먼저 DI을 사용할 경우, 아마 우리는 아래와 같이 디자인할 수 있지 않을까 생각합니다. 아래 코드에서는 Constuctor Injection을 사용했지만, Setter Injection 또는 Parameter Injection 또한 사용될 수 있습니다.(http://xunitpatterns.com/Dependency%20Injection.html)) 이 경우 private 메소드로 두어서 테스트할 경우와 비교하여, 반대되는 경우로 앞서 언급한 공용로직에 의한 단위테스트 중복문제는 피할 수 있으나, YAGNI(http://en.wikipedia.org/wiki/You_aren't_gonna_need_it)을을) 위배하게 됩니다. 즉, Summation이라는 인터페이서는 테스트용도로만 사용되어, 실제 product코드에서는 전혀 불필요한 것입니다. Refactoring책에서 Speculative Generality이라고 언급되는 것도 같은 맥락이라고 생각됩니다.

http://books.google.co.kr/books?id=HmrDHwgkbPsC&pg=PT105&lpg=PT105&dq=refactoring++Speculative+Generality&source=bl&ots=y4besqH7WV&sig=LBi1A0uejvNiEGLBhbAohRTbkJc&hl=en&sa=X&ei=1EnvUvS3G8bSkAWcjYDYCg&ved=0CDEQ6AEwAQ#v=onepage&q=refactoring%20%20Speculative%20Generality&f=false

public class 통계{
	private readonly Summation summation


	public 통계(Summation sumation){
		...
	}


	public double 분산(double[] data){
		// summation.합계 메소드 사용
		throw new NotImplementedException();
	}


	public double 평균(double[] data){
		// summation.합계 메소드 사용
		throw new NotImplementedException();
	}
}


public class 성적{
	private readonly Summation summation


	public 성적(Summation sumation){
		...
	}


	public int 등수(double[] data){
		// summation.합계 메소드 사용
		throw new NotImplementedException();
	}
}


public interface Summation{
	double 합계(double[] data);
}

어떤경우에, 어떤 것을 선택하여야 하는가?

사실 위와 같은 예에서는 저의 기준으로는 합계메소드를 private로 그대로 두어 직접테스트하지 않는 것이 최상인 것 같습니다. 하지만 아래와 같은 경우는 Speculative Generality(YAGNI)위배와 상관없이 public메소드로 선언하여 DI를 통해 테스트되어야 하지 않을까 생각합니다. 저는 이 세 가지를 기준으로 private 메소드를 public으로 바꾸어 테스트할지 말지를 결정합니다.

  • non deterministic 로직(eg, 시간, 랜덤수)
  • 외부리소스 의존로직(eg, 데이터베이스, 파일시스템)
  • 시간이 많이 걸리는 로직

public 메소드로 두어 직접테스트할 경우(DI를 사용하지 않을 경우)

private 메소드를 public으로 바꾸어 테스트할 경우 앞서 두 가지만 생각을 하다 최근 들어서, public 메소드로 두어 직접테스트는 하지만 이 메소드를 DI를 통해 테스트되지 않는 경우도 앞의 두가지 경우와 더불어 다르게 분류되어야 한다는 생각을 가지게 되었습니다. 실제 의식하지 않고 흔히 사용하고 있는 경우라 하겠습니다.

위의 합계메소드 예는 이 경우와 어울리지 않지만, 예를들어 특정(합계) 메소드가 복잡도가 높아서 단위테스트가 많이 필요로하는 메소드일 경우, 달리말해 public 메소드들(분산, 평균, 성적)로 부터 간접적인 테스트가 용이하지 않을 때 이 경우에 속한다고 생각합니다. 굳이 평균을 예제를 이 경우에 사용해 본다면, 아래 코드와 같이 될 수 있습니다.

public class 통계{
	public double 분산(double[] data){
		// 합계 메소드 사용
		// new Summation().합계(data);
		throw new NotImplementedException();
	}


	public double 평균(double[] data){
		// 합계 메소드 사용
		// new Summation().합계(data);
		throw new NotImplementedException();
	}
}


public class 성적{
	public int 등수(double[] data){
		// 합계 메소드 사용
		// new Summation().합계(data);
		throw new NotImplementedException();
	}
}


public class Summation{
	public double 합계(double[] data){
		throw new NotImplementedException();
	}
}

이 경우를 앞서의 두 가지 경우와 비교해 보면 장단점이 중간 정도에 걸쳐 있다고 느껴집니다. Summation클래스는 테스트 목적을 위해 노출되었으므로 YAGNI를 위배하는 것이 -되지만, private 메소드로 두어 직접테스트하지 않는 경우처럼 인터페이스 구현클래스로 이어이지는 YAGNI 위배보다 크기가 작다라고 할 수 있을 것 같습니다. 이것을- 단점이라 한다면 장점은 음의 수(data)가 넘어온다면 InvalidArgumentException 예외를 던져야한다와 같은 시나리오를 Summation클래스로 한정지을 수 있습니다.

하지만, 여기서 주목해야 할 것은 합계 메소드가 분산, 평균 그리고 등수 메소드에서 호출되어 사용되리라는 확신이 있어야 합니다. 즉, 합계메소드가 다른 메소드에서 'new Summation().합계(data)' 이렇게 사용이 되었지만, 누군가가 리팩토링과정에서 이를 호출하는 방식이 아니고 InvalidArgumentException 예외시나리오를 빠뜨린 채, 직접구현해 버린다면 모든 테스트는 통과하지만, 결과적으로는 InvalidArgumentException 예외시나리오 테스트가 빠지게 되는 오류를 범할 수 있습니다.

사실 이 부분에 대해서 명확한 설명이 어려운 것이 사실입니다. 실제 코드 작성자가 합계 메소드를 호출하여 구현하였는데, 다른 누군가가 이를 삭제하고 직접구현하는 방식으로 변경해서 InvalidArgumentException 예외시나리오 빠뜨리게 될 가능성이 얼마나 될까요? 만약 가능성이 없다고 확신이 된다면 이 경우를 사용해도 무방할 것입니다.

끝으로

저는 Speculative Generality(YAGNI) 가이드라인을 따라 테스트 목적으로 interface, abstract 혹은 overridable한 멤버를 선언하지는 않습니다. 하지만 테스트가 시간과 관련이 있거나 혹은 랜덤수와 같이 non-deterministic 로직인 경우, 혹은 시간이 많이 걸리는 로직인 경우는 interface 혹은 abstract를 통한 DI를 사용합니다.(아마 이 부분이 TDD에서의 isolation이란 개념이 아닐까 합니다.) 또한 경우에 따라선 Speculative Generality(YAGNI) 가이드라인을 위배하지만 테스트를 용이하게 하기 위해 특정 private 로직을 public으로 선언하여 단위테스트를 행합니다.(public 메소드로 두어 직접테스트할 경우 - DI를 사용하지 않을 경우)

어디까지나 저의 개인적인 견해입니다. 따라서 저와는 다르게 생각하시는 분들이 계실 수 있습니다. 반대의견이 있으시면 주저 없이 말씀 주시기 바랍니다.

@자바지기님의 답변을 읽고난 후 사실 어느 정도는 제가 밝힌 위의 생각과 맞닿은 부분이 있는가 하면, 어떤 부분에서는 많이 다른 부분도 있는 것 같습니다. 제가 @자바지기님의 답변을 100% 이해하지 못했다고 하는 것이 맞을 것 같습니다. 조금 생각해 보고 다시 한번 글 남기도록 하겠습니다.

2014-02-03 19:47

@benghun 답변 감사드립니다.

아래 말씀이 와 닿네요. 저도 이렇게 생각해야 할 것 같네요. - 제가 앞서 말한 public 메소드로 두어 직접테스트할 경우(DI를 사용하지 않을 경우)와 맥락이 닿아 있다고 생각됩니다. {quote}저 같으면 외부에서 직접 사용하지 않는다고 하더라도 찝찝한 기능이라면 public으로 바꿔서라도 테스트해버리겠네요. 장애로 새벽에 일하는거보다는 나을거 같아서요.{quote}

GOOS 아직 부분적으로 읽어 보고 전체적으로 보지는 못 했습니다. 다른 책 보고 있는 중인데 다 보면 이 책부터 봐야 할 것 같네요.

이런 뉴스그룹도 있네요. 정보 감사합니다. https://groups.google.com/forum/#!forum/growing-object-oriented-software (http://permalink.gmane.org/gmane.comp.programming.goos/2842)로로) 부터

2014-02-03 20:39

@Jin-Wook Chung 와 정말 긴 답변을 다셨네요. 저도 온라인상에서는 하는 오랜만의 논의가 즐겁네요. 답변이 길고, 제가 진욱님의 의도를 이해하지 못하는 부분도 있어 섣부르게 답변을 달지 못하겠네요. 단, 다음 내용이 공감이 가네요.

사실 위와 같은 예에서는 저의 기준으로는 합계메소드를 private로 그대로 두어 직접테스트하지 않는 것이 최상인 것 같습니다. 하지만 아래와 같은 경우는 Speculative Generality(YAGNI)위배와 상관없이 public메소드로 선언하여 DI를 통해 테스트되어야 하지 않을까 생각합니다. 저는 이 세 가지를 기준으로 private 메소드를 public으로 바꾸어 테스트할지 말지를 결정합니다.

  • non deterministic 로직(eg, 시간, 랜덤수)
  • 외부리소스 의존로직(eg, 데이터베이스, 파일시스템)
  • 시간이 많이 걸리는 로직

저도 테스트를 위해 DI로 구현하는 부분이 @Jin-Wook Chung 님이 제안한 위 세 가지 경우가 많았던 것으로 기억합니다. 그 이외에는 public 메서드를 통해 테스트는 경우가 대부분이지 않을까 생각합니다. 단, private 메서드인데 별도의 단위 테스트를 만들 필요가 있다는 느낌을 받는다면 이 시점에 이 메서드를 별도의 클래스로 분리할 필요가 있는지에 대해 고민해 볼 필요가 있다는 것이 제 글의 주요 골자였습니다.

http://www.slipp.net/wiki/pages/viewpage.action?pageId=6160426 글의 마지막 소스 코드에서 Hour라는 클래스를 도출하고 getMessage()를 Default에서 public으로 변경하는 부분이 그 부분이라고 생각하시면 됩니다.

저도 쓰신 답변 정독해 보고 느끼는 인사이트가 있다면 다시 한번 글을 정리해 보도록 할께요.

2014-02-04 11:39

@Jin-Wook Chung 긴글 감사합니다. Speculative Generality 라는 주제로 논의하기 이전에 Summation은 좀 응집력을 떨어뜨리는거 같네요. 그렇게 생각하는 이유는 통계데이터 double[]를 입력으로 받아서 double을 반환하는 기능이 한 곳에 모여있지 못해서 사용성에 큰 이득이 없어보이기 때문입니다. 저 같으면 그냥 통계.sum()을 public으로 처리하겠네요.

StatsData data = new StatsData(new double[2] { 1, 2 });
StatsResult result = statsCalculator.calc(data);


assertEquals(1.5, result.평균값);
assertEquals(3.0, result.합계값);

위 코드의 경우에는 StatsCalculator에 대한 인터페이스가 불필요하다고 생각합니다. 하지만 StatsCalculator라는 클래스 자체가 필요한지 설계논의를 담아내기에 interface가 좋은 역할을 할 수 있다고 생각해요.

@Test public void 모바일_통계표_조회() {
    statsService = new StatsService(mockStatsDao, mockStatsCalculator, mobileStatsReportor);
    report = statsService.getStats(dateRange);
    
    // asserts here
}

통계 데이터를 조회하고, 연산을 한 다음에, 모바일용 보고서를 조회하기 위한 설계 논의를 위 테스트로 담아낼 수 있다고 생각합니다. 물론 여기서 dao나 calculator가 왜 interface여야 하는가? class로도 충분하다라고 주장할 수 있습니다. 하지만 TDD를 생각해보면 calculator의 구현 이전에 statsService설계를 진행할테고, 전체적인 관점에서 협력객체들의 역할과 책임을 구상할텐데요. 이 때 service의 테스트를 통과시키기에 mock만큼 좋은 것은 없습니다. 게다가 인터페이스로 협력하기 때문에 협력객체들의 디테일한 구현에 의해 public interface(method)가 흔들릴 확률이 적습니다. StatsService가 하위 모듈의 변경에 영향을 받을 확률이 낮아지겠죠.

제가 이런 말을 하는 이유는 interface는 DI와 Speculative Generality의 관점보다는 top-down 설계의 유용한 도구라고 생각하기 때문입니다. 당연히 YAGNI 입장에서도 top-down으로 필요한 것만 테스트해왔기 때문에 불필요한 군더더기가 상대적으로 적을 것 같네요.

저는 비교적 시간이 남고 여유가 있을 때는 TDD로 예술적 경지의 코드를 만들겠다고 도전하기도 하지만 실제 업무에서는 TDD를 설계, 구현, 검증의 도구로 적당히 사용하면서 개발하는 편입니다. 너무 세부적인 곳까지 YAGNI, OAOO, DRY, LoD 등의 설계기법들을 적용하고자하면 일하기가 너무 힘들더라구요.

2014-02-04 15:20

@benghun 우선, 위의 예제는 그냥 즉흑적으로 작성된 단편적인 예제일 뿐입니다. 이 예제에 너무 초점을 맞추지 않으시는게 좋을 뜻 합니다. 좋은 예제를 찾는다는 것이 중요하고 얼마나 어려운지 새삼 깨닫게 됩니다.

{quote} 통계 데이터를 조회하고, 연산을 한 다음에, 모바일용 보고서를 조회하기 위한 설계 논의를 위 테스트로 담아낼 수 있다고 생각합니다. 물론 여기서 dao나 calculator가 왜 interface여야 하는가? class로도 충분하다라고 주장할 수 있습니다. 하지만 TDD를 생각해보면 calculator의 구현 이전에 statsService설계를 진행할테고, 전체적인 관점에서 협력객체들의 역할과 책임을 구상할텐데요. 이 때 service의 테스트를 통과시키기에 mock만큼 좋은 것은 없습니다. 게다가 인터페이스로 협력하기 때문에 협력객체들의 디테일한 구현에 의해 public interface(method)가 흔들릴 확률이 적습니다. StatsService가 하위 모듈의 변경에 영향을 받을 확률이 낮아지겠죠. {quote} '구상 클래스보다 interface를 mocking하라'. 일리 있는 말씀이십니다. 잘 아시겠지만 GOOS 책에서도 이와 같은 얘기를 하고 있습니다. (http://www.mockobjects.com/2007/04/test-smell-mocking-concrete-classes.html))

{quote} 제가 이런 말을 하는 이유는 interface는 DI와 Speculative Generality의 관점보다는 top-down 설계의 유용한 도구라고 생각하기 때문입니다. 당연히 YAGNI 입장에서도 top-down으로 필요한 것만 테스트해왔기 때문에 불필요한 군더더기가 상대적으로 적을 것 같네요. {quote} 그러나, "mocking할 필요가 있는가"라는 질문과 "mocking을 할때는 구상클래스보다는 인터페이스로하라"라는 의미는 다르게 생각해야 되지 않을까 합니다. 다시말해, mocking할 필요가 있는지 따져보고, 만약 그렇다면 구상클래스보다는 인터페이스로 mocking해야한다 이렇게 이해해야 하지 않을까 합니다. 아마 같은 의미로 답변을 주셨다고 생각됩니다만 제가 정확히 @benghun말씀을 이해한 것인지 모르겠습니다.

new StatsService(mockStatsDao, mockStatsCalculator, mobileStatsReportor)

저는 아래와 같은 세가지 조건을 가지고 mocking할 필요가 있는지를 따져 봅니다. - non deterministic 로직(eg. 시간, 랜덤수) - 외부리소스 의존로직(eg. 데이터베이스, 파일시스템) - 시간이 많이 걸리는 로직

이 경우, - mockStatsDao: 외부리소스(데이터베이스) 의존로직이므로 mockding이 필요 - mockStatsCalculator: 위 조건을 만족하지는 않지만(경우에 따라선 만족할 수도 있습니다), mockStatsCalculator 구현 클래스가 두 개 이상이면, 의미가 있을 것입니다. 하지만 구현 클래스가 하나일 경우 다시말해 테스트를 위한 인터페이스라면 mocking을 하지 않는 것이 좋을 것 같습니다. - mobileStatsReportor: mockStatsCalculator와 동일조건

이렇게 되지 않을까 합니다.

{quote} 모바일용 보고서를 조회하기 위한 설계 논의를 위 테스트로 담아낼 수 있다고 생각합니다 {quote} 아마 이렇게 말씀하신 것은 구현 전에 설계를 말씀하신 것 같습니다.(Big Design Up Front) 이와는 반대로 mockStatsCalculator와 mobileStatsReportor는 TDD과정 중에 Emergent design으로 도출될 수 있다고 생각 됩니다. 예를들어 현재는 모르겠으나 TDD 구현 도중 mobileStatsReportor 인터페이스가 도출되고, 구현클래스가 StyleAReportor, StyleBReportor와 같은 클래스들이 만들어질지도 모르는 일입니다.

2014-02-04 15:40

@Jin-Wook Chung 테스트할 때 mocking에 대해 이야기해주셨는데 이 부분은 저도 동의합니다. 솔직히 테스트를 클래스로 직접할 수 있는데 굳이 mocking을 할 이유는 전혀 없으니까요. 다만 저는 설계 도구로써의 TDD와 mock에 대해서 좀더 강조했었습니다.

말씀하셨던 것처럼 단편적인 예제로 TDD나 refactoring에 대해서 논의하는 것은 더 깊이 있는 논의에 어쩌면 방해가 될지도 모른다고 생각합니다. OOP 자체가 소프트웨어의 크기가 커질 때 어떻게 하는게 좋을까에 좀더 초점이 맞춰져있으니까요. 그래서 제가 뭔가 놓쳤나보네요. ㅎㅎ

참고로 big design up front에 대한 이야기는 아닙니다. TDD 자체를 설계과정이라고 가정하고 말씀드린거에요.

2014-02-04 15:59

@benghun 공개된 장소에서 글로써 누군가에게 말한다는게 참 조심스럽습니다. 제가 잘못이해하고 말씀드린 부분에 대해서는 적극적인 토론(토의)의 의견으로 받아주시면 감사하겠습니다.

의견 추가하기

연관태그

← 목록으로