* TDD 과정을 공유하기 위해 연습하는 과정을 글로 남긴다.
시작은 간단한 설계로
만들어 볼 소프트웨어는 동영상 파일 변환 시스템이다. 주요 기능을 다음과 같이 잡아 보았다.
- 임의 위치의 미디어 파일을 읽어와 지정한 포맷으로 변환한 뒤 결과를 원하는 위치에 저장한다.
- 원하는 방식으로 썸네일 이미지를 생성해서 원하는 위치에 저장한다.
- 진행 상태를 중간 중간 확인할 수 있다.
- 변환 결과를 통지 받는다.
변환 처리를 수행할 TranscodingService가 필요할 것 같았고, 그 외에 작업의 의미를 담는 Job 그리고 그 Job을 보관할 JobRepository가 필요하다고 판단했다.
테스트 코드로 시작하기
자, 이제 할 작업은 TranscodingService를 위한 테스트를 만드는 것이었다.
가장 먼저 만든 코드는 다음과 같다.
public class TranscodingServiceTest {
@Test
public void transcodeSuccessfully() {
// 미디어 원본으로부터 파일을 로컬에 복사한다.
// 로컬에 복사된 파일을 변환처리한다.
// 로컬에 복사된 파일로부터 이미지를 추출한다.
// 변환된 결과 파일과 썸네일 이미지를 목적지에 저장
// 결과를 통지
}
}
transcodeSuccessfully() 메서드는 TranscodingService가 성공적으로 동작하는 상황을 테스트 하기 위해 만들었으며, 성공적으로 작업이 진행될 때 수행해야 할 작업을 주석으로 입력해 놓았다. 이 주석을 구현으로 바꿔치기 해 나가면서 TranscodingService를 점진적으로 완성해 나갈 것이다.
가장 먼저 해야 할 작업은 미디어 원본으로부터 파일을 로컬에 복사해 오는 부분을 구현하는 것이다. 자, 시작은 아래와 같다.
public class TranscodingServiceTest {
@Test
public void transcodeSuccessfully() {
// 미디어 원본으로부터 파일을 로컬에 복사한다.
Long jobId = new Long(1);
File multimediaFile = copyMultimediaSourceToLocal(jobId);
// 로컬에 복사된 파일을 변환처리한다.
// 로컬에 복사된 파일로부터 이미지를 추출한다.
// 변환된 결과 파일과 썸네일 이미지를 목적지에 저장
// 결과를 통지
}
private File copyMultimediaSourceToLocal(Long jobId) {
return mediaSourceCopier.copy(jobId); // 컴파일 에러!
}
}
미디어 원본 파일을 로컬에 복사해주는 부분을 copyMultimediaSourceToLocal() 메서드가 처리하도록 했고, 이 메서드는 다시 mediaSourceCopier 라는 객체의 copy() 메서드에 위임하도록 코드를 작성했다. 아직 mediaSourceCopier의 정확한 구현은 모르지만, 단지 jobId에 해당하는 Job과 관련된 미디어 원본 파일을 로컬 파일 시스템에 복사한 뒤에 복사된 그 파일을 리턴해주는 역할을 mediaSourceCopier 객체에 부여했다. 아! 아직 Job 클래스는 출현하지 않았다.
컴파일 오류 때문에 위 테스트를 수행할 수 없다. 테스트를 통과시키기 위해 Mocking을 할 것이다. 우선, mediaSourceCopier 필드를 추가한다. (이클립스라면 빨간줄에서 Ctrl+1을 눌러서 빠르게 필드를 추가할 수 있다.) 그리고, 그 필드의 타입으로 MediaSourceCopier를 입력한다.
public class TranscodingServiceTest {
private MediaSourceCopier mediaSourceCopier; // MediaSourceCopier 타입 없음
@Test
public void transcodeSuccessfully() {
// 미디어 원본으로부터 파일을 로컬에 복사한다.
Long jobId = new Long(1);
File multimediaFile = copyMultimediaSourceToLocal(jobId);
// 로컬에 복사된 파일을 변환처리한다.
// 로컬에 복사된 파일로부터 이미지를 추출한다.
// 변환된 결과 파일과 썸네일 이미지를 목적지에 저장
// 결과를 통지
}
private File copyMultimediaSourceToLocal(Long jobId) {
return mediaSourceCopier.copy(jobId); // copy 메서드 없음
}
}
- TranscodingService 클래스를 만든다.
- transcode() 메서드 및 그 메서드에서 사용하는 메서드를 TranscodingService 클래스로 옮긴다.
- transcode() 메서드에서 필요로 하는 협업 객체를 필드로 정의한다.
- 생성자를 이용해서 또는 Setter를 이용해서 협업 객체를 초기화한다.
결과로 만들어진 TranscodingService 클래스는 아래와 같다.
public class TranscodingService {
private MediaSourceCopier mediaSourceCopier;
private Transcoder transcoder;
private ThumbnailExtractor thumbnailExtractor;
private CreatedFileSender createdFileSender;
private JobResultNotifier jobResultNotifier;
public TranscodingService(MediaSourceCopier mediaSourceCopier,
Transcoder transcoder, ThumbnailExtractor thumbnailExtractor,
CreatedFileSender createdFileSender,
JobResultNotifier jobResultNotifier) {
this.mediaSourceCopier = mediaSourceCopier;
this.transcoder = transcoder;
this.thumbnailExtractor = thumbnailExtractor;
this.createdFileSender = createdFileSender;
this.jobResultNotifier = jobResultNotifier;
}
public void transcode(Long jobId) {
File multimediaFile = copyMultimediaSourceToLocal(jobId);
List<File> multimediaFiles = transcode(multimediaFile, jobId);
List<File> thumbnails = extractThubmail(multimediaFile, jobId);
sendCreatedFilesToDestination(multimediaFiles, thumbnails, jobId);
notifyJobResultToRequester(jobId);
}
private File copyMultimediaSourceToLocal(Long jobId) {
return mediaSourceCopier.copy(jobId);
}
private List<File> transcode(File multimediaFile, Long jobId) {
return transcoder.transcode(multimediaFile, jobId);
}
private List<File> extractThubmail(File multimediaFile, Long jobId) {
return thumbnailExtractor.extract(multimediaFile, jobId);
}
private void sendCreatedFilesToDestination(List<File> multimediaFiles,
List<File> thumbnails, Long jobId) {
createdFileSender.send(multimediaFiles, thumbnails, jobId);
}
private void notifyJobResultToRequester(Long jobId) {
jobResultNotifier.notifyToRequester(jobId);
}
}
이제 다음으로 할 작업은 테스트 클래스가 TranscodingService 클래스를 사용하도록 변경할 차례이다.
@RunWith(MockitoJUnitRunner.class)
public class TranscodingServiceImplTest {
private Long jobId = new Long(1);
@Mock
private MediaSourceCopier mediaSourceCopier;
@Mock
private Transcoder transcoder;
@Mock
private ThumbnailExtractor thumbnailExtractor;
@Mock
private CreatedFileSender createdFileSender;
@Mock
private JobResultNotifier jobResultNotifier;
private TranscodingService transcodingService;
@Before
public void setup() {
transcodingService = new TranscodingService(mediaSourceCopier,
transcoder, thumbnailExtractor, createdFileSender,
jobResultNotifier);
}
@Test
public void transcodeSuccessfully() {
File mockMultimediaFile = mock(File.class);
when(mediaSourceCopier.copy(jobId)).thenReturn(mockMultimediaFile);
List<File> mockMultimediaFiles = new ArrayList<File>();
when(transcoder.transcode(mockMultimediaFile, jobId)).thenReturn(
mockMultimediaFiles);
List<File> mockThumbnails = new ArrayList<File>();
when(thumbnailExtractor.extract(mockMultimediaFile, jobId)).thenReturn(
mockThumbnails);
transcodingService.transcode(jobId);
verify(mediaSourceCopier, only()).copy(jobId);
verify(transcoder, only()).transcode(mockMultimediaFile, jobId);
verify(thumbnailExtractor, only()).extract(mockMultimediaFile, jobId);
verify(createdFileSender, only()).send(mockMultimediaFiles,
mockThumbnails, jobId);
verify(jobResultNotifier, only()).notifyToRequester(jobId);
}
}
뭔가 바꿨으니 테스트를 실행해 보자. 녹색! 오케이!
이제 진짜 하나 남았다. TranscodingService를 TranscodingServiceImpl 클래스로 바꾸고, TranscodingService 인터페이스를 만들자. 그리고, TranscodingServceImpl 클래스가 TranscodingService 인터페이스를 상속받도록 하자. 만들어진 TranscodingService 인터페이스는 다음과 같다.
public interface TranscodingService {
public void transcode(Long jobId);
}
기존의 TranscodingService 클래스는 이름을 TranscodingServiceImpl로 변경하고 TranscodingService 인터페이스를 상속받도록 구현한다.
public class TranscodingServiceImpl implements TranscodingService {
private MediaSourceCopier mediaSourceCopier;
private Transcoder transcoder;
private ThumbnailExtractor thumbnailExtractor;
private CreatedFileSender createdFileSender;
private JobResultNotifier jobResultNotifier;
public TranscodingServiceImpl(MediaSourceCopier mediaSourceCopier,
Transcoder transcoder, ThumbnailExtractor thumbnailExtractor,
CreatedFileSender createdFileSender,
JobResultNotifier jobResultNotifier) {
...
}
@Override
public void transcode(Long jobId) {
...
}
...
}
테스트 코드는 이제 TranscodingServceImpl 클래스를 사용하도록 변경하면 끝!
결과는?
결과는 다음과 같다.
- 테스트로부터 시작해서 TranscodingService 객체가 협업해야 하는 다른 객체들의 인터페이스와 기능을 점진적으로 식별했다.
- 아직 정상적으로 실행되는 경우에 대해서만 TranscodingService가 구현되어 있다. 중간에 일부가 실패하는 경우에 대한 TranscodingService 구현이 필요하다. 이걸 단위 테스트로 한 번 만들어 볼 것이다.
- 최초에 설계했던 Job 클래스나 JobRepository 등은 아직 출현하지 않았다. 이 타입들은 앞으로 구현을 진행하면서 점진적으로 출현할 것 같다.