주요글: 도커 시작하기

연습 4-1(http://javacan.tistory.com/220)에 이어 계속해서 Job 부분을 만들어 나가자. 


새로운 모델의 출현: MediaSourceFile


Job은 원본 미디어 파일 정보를 갖고 있어야 한다. 미디어 원본 파일을 MediaSourceFile로 추상화하고, 아래와 같이 이 타입을 필드로 갖도록 코드를 만들었다.


public class Job {

    ...

   private MediaSourceFile mediaSourceFile;

   ...

   public Job(Long id, MediaSourceFile mediaSourceFile) {

       this.id = id;

       this.mediaSourceFile = mediaSourceFile;

   }

   ...

}


빨간 줄이 나오니까 컴파일 에러가 나지 않도록 변경해 주자. 먼저 MediaSourceFile을 생성할 것이다. 아직 MediaSourceFile의 정확한 구현을 알 수 없으므로 일단 인터페이스로 만든다. 인터페이스만 있고 메서드는 없다.


// 아직 기능 없음

public interface MediaSourceFile {


}


Job 클래스의 컴파일 에러는 사라졌는데, 대신 TranscodingServiceImplTest 코드에서 컴파일 에러가 발생한다.


@RunWith(MockitoJUnitRunner.class)

public class TranscodingServiceImplTest {


    private Long jobId = new Long(1);

    private Job mockJob = new Job(jobId);


Job의 생성자가 변경되어서 컴파일 에러가 발생하고 있다. Job 객체를 생성하려면 MediaSourceFile 구현체가 필요하다. 일단, MediaSourceFile에 대한 Mock 객체를 만들고, 그 Mock 객체를 받도록 변경한다. Job 객체를 생성하는 위치는 setup() 메서드로 이동시킨다.


@RunWith(MockitoJUnitRunner.class)

public class TranscodingServiceImplTest {


    private Long jobId = new Long(1);

    @Mock

    private MediaSourceFile mediaSourceFile;

    

    private Job mockJob;

    ...


    @Before

    public void setup() {

        mockJob = new Job(jobId, mediaSourceFile);

        ...

뭔가 바꾸면 해야 할 게 있다. 그렇다, 바로 테스트 실행이다. 테스트 실행 ... 녹색바! 통과다.


MediaSourceFile은 미디어 파일을 복사하는 것과 관련되었으므로, Job 클래스의 copyMultimediaSourceToLocal() 메서드를 보자.


public class Job {


    private Long id;

    private MediaSourceFile mediaSourceFile;

    ...


    public Job(Long id, MediaSourceFile mediaSourceFile) {

        this.id = id;

        this.mediaSourceFile = mediaSourceFile;

    }


    public void transcode(MediaSourceCopier mediaSourceCopier,

            Transcoder transcoder, ThumbnailExtractor thumbnailExtractor,

            CreatedFileSaver createdFileSaver,

            JobResultNotifier jobResultNotifier) {

        try {

            changeState(Job.State.MEDIASOURCECOPYING);

            File multimediaFile = copyMultimediaSourceToLocal(mediaSourceCopier);

            ...

        } catch (RuntimeException ex) {

            exceptionOccurred(ex);

            throw ex;

        }

    }


    private File copyMultimediaSourceToLocal(MediaSourceCopier mediaSourceCopier) {

        return mediaSourceCopier.copy(id); // mediaSourceFile을 줘야 하나?

    }



copyMultimediaSourceToLocal() 메서드는 mediaSourceCopier 객체의 copy() 메서드를 호출할 때 id 필드를 값으로 주고 있다. 읽어올 미디어 원본 파일 정보가 mediaSourceFile 필드에 있으므로, id 대신 mediaSourceFile을 건내주면 될 것 같다.


음... 잠깐! MediaSourceFile에 다음과 같은 메서드를 정의하면 어떨까?


public interface MediaSourceFile {

    public File getSourceFile();

}


또는


public interface MediaSourceFile {

    public void writeTo(File file);

}


두 가지 중 어떤 것을 선택해도 mediaSourceCopier의 필요성이 사라진다. 오호~ 그렇군. 조금 고민하다가 파일이 어디에 보관될지 여부는 Job이 스스로 결정하는 게 좋을 듯 싶어, getSourceFile() 메서드를 사용하기로  결정했다. 결정했으니 바꿔보자.

  • MediaSourceFile 인터페이스에 getSourceFile() 메서드를 추가한다.
  • copyMultimediaSourceToLocal() 메서드가 mediaSourceFile.getSourceFile()를 사용하도록 변경한다.
  • 이렇게 바꾸면, copyMultimediaSourceToLocal() 메서드가 MediaSourceCopier를 필요로 하지 않는다. 그러니 파라미터에서 mediaSourceCopier를 제거한다.
Job에 적용한 결과는 아래와 같다.

public class Job {

    public void transcode(MediaSourceCopier mediaSourceCopier,
            Transcoder transcoder, ThumbnailExtractor thumbnailExtractor,
            CreatedFileSaver createdFileSaver,
            JobResultNotifier jobResultNotifier) {
        try {
            changeState(Job.State.MEDIASOURCECOPYING);
            File multimediaFile = copyMultimediaSourceToLocal(); // mediaSourceCopier 빠짐
            ...
        } catch (RuntimeException ex) {
            exceptionOccurred(ex);
            throw ex;
        }
    }

    private File copyMultimediaSourceToLocal() { // mediaSourceCopier 파라미터 제거
        return mediaSourceFile.getSourceFile();
    }

테스트를 실행해 보자. 앗! 모두 실패다. 실패나는 이유를 봤더니 mediaSourceCopier Mock 객체가 더 이상 호출되지 않아서 발생하는 문제였다. 단위 테스트 코드에서 mediaSourceCopier를 verify 하는 부분을 삭제한다.

    private void verifyCollaboration(VerifyOption verifyOption) {
        verify(mediaSourceCopier, only()).copy(jobId);

        if (verifyOption.transcoderNever)
            verify(transcoder, never()).transcode(any(File.class), anyLong());
        else
            verify(transcoder, only()).transcode(mockMultimediaFile, jobId);

        ...
    }

다시 실행해 보자. 앗! 그래도 모두 실패다. 위 코드에서 trancoder.trancode() 메서드가 호출되었는지 여부를 확인하는 부분에서 실패가 난다. trancoder.trancode()의 호출 여부를 검증할 때 사용한 파라미터는 mockMultimediaFile인데 실제로 Job 객체에서 mediaSourceFile.getSourceFile()은 null을 리턴한다. 그래서 검증에 실패하였다.

통과하게 하는 방법은 간단하다. 테스트 코드의 setup() 메서드에서 mediaSourceFile을 위해 생성한 Mock 객체의 getSourceFile() 메서드가 mockMultimediaFile을 리턴하도록 설정해주면 된다.

@RunWith(MockitoJUnitRunner.class)
public class TranscodingServiceImplTest {
    ...
@Before
public void setup() {
mockJob = new Job(jobId, mediaSourceFile);
when(mediaSourceFile.getSourceFile()).thenReturn(mockMultimediaFile);
transcodingService = new TranscodingServiceImpl(mediaSourceCopier,
transcoder, thumbnailExtractor, createdFileSender,
jobResultNotifier, jobRepository);

...

다시 테스트. 6개의 테스트 메서드 중에 한 개가 실패했다. 그 메서드는 다음과 같다.

    @Test
    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {
        when(mediaSourceCopier.copy(jobId)).thenThrow(mockException);

        executeFailingTranscodeAndAssertFail(Job.State.MEDIASOURCECOPYING);

        VerifyOption verifyOption = new VerifyOption();
        verifyOption.transcoderNever = true;
        verifyOption.thumbnailExtractorNever = true;
        verifyOption.createdFileSenderNever = true;
        verifyOption.jobResultNotifierNever = true;

        verifyCollaboration(verifyOption);
    }

MediaSourceCopier는 더 이상 호출되지 않기 때문에 mediaSourceCopier가 익셉션을 발생시키도록 만들고, 실제로 그렇게 되는지 확인하는 건 의미가 없다. mediaSourceCopier 대신 mediaSourceFile가 익셉션을 발생시키는 경우를 테스트 하도록 수정하는 것이 맞다. 그래서 아래와 같이 테스트를 바꾼다.

    @Test
    public void transcodeFailBecauseExceptionOccuredAtMediaSourceFile() {
        when(mediaSourceFile.getSourceFile()).thenThrow(mockException);

        executeFailingTranscodeAndAssertFail(Job.State.MEDIASOURCECOPYING);

        VerifyOption verifyOption = new VerifyOption();
        verifyOption.transcoderNever = true;
        verifyOption.thumbnailExtractorNever = true;
        verifyOption.createdFileSenderNever = true;
        verifyOption.jobResultNotifierNever = true;

        verifyCollaboration(verifyOption);
    }

테스트 실행,,, 녹색! 모두 통과다. 이로서 Job과 관련된 새로운 모델인 MediaSourceFile을 추가하고 MediaSourceFile이 원본으로부터 파일을 가져오도록 설계를 변경하는데 성공했다.


MediaSourceCopier는 이제 필요 없어요!


MediaSourceFile의 등장으로 MediaSourceCopier는 필요 없어졌다. 이제 과감하게 MediaSourceCopier를 소스 코드에서 제거할 차례이다. MediaSourceCopier 타입을 없애고, MediaSourceCopier 타입의 필드, 초기화 코드, 파라미터 등을 모두 제거한다.


// Job 클래스에서 제거

public class Job {

    ....

    public void transcode(MediaSourceCopier mediaSourceCopier,

            Transcoder transcoder, ThumbnailExtractor thumbnailExtractor,

            CreatedFileSaver createdFileSaver,

            JobResultNotifier jobResultNotifier) {

        try {



// TranscodingServceImpl 클래스에서 제거

public class TranscodingServiceImpl implements TranscodingService {

    private MediaSourceCopier mediaSourceCopier;

    private Transcoder transcoder;

    ...


    public TranscodingServiceImpl(MediaSourceCopier mediaSourceCopier,

            Transcoder transcoder, ThumbnailExtractor thumbnailExtractor,

            CreatedFileSaver createdFileSaver,

            JobResultNotifier jobResultNotifier,

            JobRepository jobRepository) {

        this.mediaSourceCopier = mediaSourceCopier;

        this.transcoder = transcoder;

        ...

    }


    @Override

    public void transcode(Long jobId) {

        Job job = jobRepository.findById(jobId);

        job.transcode(mediaSourceCopier, transcoder, thumbnailExtractor,

                createdFileSaver, jobResultNotifier);

    }



// 테스트 코드에서 제거

@RunWith(MockitoJUnitRunner.class)

public class TranscodingServiceImplTest {

    ...

    @Mock

    private MediaSourceCopier mediaSourceCopier;

    @Mock

    private Transcoder transcoder;

    ...


    @Before

    public void setup() {

        mockJob = new Job(jobId, mediaSourceFile);

        when(mediaSourceFile.getSourceFile()).thenReturn(mockMultimediaFile);

        

        transcodingService = new TranscodingServiceImpl(mediaSourceCopier,

                transcoder, thumbnailExtractor, createdFileSender,

                jobResultNotifier, jobRepository);


        when(jobRepository.findById(jobId)).thenReturn(mockJob);

        when(mediaSourceCopier.copy(jobId)).thenReturn(mockMultimediaFile);

        ...

    }


코드를 수정했으니까, 다음으로 할 작업은 테스트 실행이다. 테스트 실행~.. 녹색! 모두 통과다.


테스트 코드를 이용한 안정적인 리팩토링


TDD를 이용해서 TranscodingServce의 구현부터 Job의 일부 구현까지 진행되었다. 한 번 정리해 보자.


최초의 TDD 결과물은 아래와 같았다.



하지만, 결과물이 마음에 안 들었고 그래서 Job으로 기능을 옮겼다. 물론, 테스트는 그대로 유지하면서. 그래서 다음과 같이 Job이 출현하고 불필요해진 두 개의 타입이 사라졌다.



그리고, 새로운 도메인 모델인 MediaSourceFile을 추가하는 과정에서 일부 로직이 MediaSourceFile로 이동했고, 이 과정에서 MediaSourceCopier 타입이 또 사라졌다.



그리고, 무엇보다도 중요한 건, 테스트 코드를 통해서 이러한 변화 과정을 안정적으로 진행했다는 점이다.



  1. 백명석 2012.10.26 20:11

    verifyCollaboration(verifyOption); -> verifyOption.verify();가 어떨까 ?
    글고
    dosomething(); changeState(); 의 구조적 반복 제거...
    ㅎㅎ
    재밌는데. 근데 이런걸 github에서 했으면 정말 재밌었을 듯.
    블로그에 댓글 놀이로는...

+ Recent posts