저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

'실패 시나리오 테스트'에 해당되는 글 1건

  1. 2012.10.24 TDD 연습 2. TranscodingService의 실패 시나리오 테스트를 통한 점진적 구현 추가 (2)

* TDD 과정을 공유하기 위해 연습하는 과정을 글로 남긴다.


실패 상황 시나리오 구현하기


앞서 http://javacan.tistory.com/entry/TDD-exercise-1-Design-communication-using-tdd 글에 이어 실패 상황을 구현해 보자. 우선 원본 소스로부터 파일을 복사해 오는 과정이 실패났을 경우를 가정해서 테스트 코드를 만든다.


@RunWith(MockitoJUnitRunner.class)

public class TranscodingServiceImplTest {

    private Long jobId = new Long(1);


    @Mock

    private MediaSourceCopier mediaSourceCopier;

    ....

    private TranscodingService transcodingService;


    @Test

    public void transcodeSuccessfully() {

        ....

    }


    @Test

    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {

        try {

            transcodingService.transcode(jobId);

            fail("발생해야 함");

        } catch (Exception ex) {

        }


        Job job = jobRepository.findById(jobId);

        assertFalse(job.isSuccess());

        assertEquals(Job.State.MEDIASOURCECOPYING, job.isLastState());


        verify(mediaSourceCopier, only()).copy(jobId);

        verify(transcoder, never()).transcode(any(File.class), anyLong());

        verify(thumbnailExtractor, never()).extract(any(File.class), anyLong());

        verify(createdFileSender, never()).store(anyListOf(File.class),

                anyListOf(File.class), anyLong());

        verify(jobResultNotifier, never()).notifyToRequester(jobId);

    }

}


위 테스트 메서드는 MediaSourceCopier가 copy 과정에서 문제가 발생한 경우 그걸 검증하는 코드로 작성되었다. 먼저 transcodingService.transcode() 메서드는 익셉션을 발생시켜야 한다. 그렇지 않을 경우 테스트가 실패하도록 했다. 그리고, mediaSourceCopier를 제외한 나머지 협업 객체는 호출되어서는 안 되므로, 호출 검증 값을 never()로 지정하였다.


드디어 Job과 jobRepository가 출현했다. 먼저 연습 1에서 했던 것 처럼, jobRepository를 필드로 만들고 JobRepository 인터페이스를 생성한다. 그리고, Job 클래스도 함께 만들어준다.


@RunWith(MockitoJUnitRunner.class)

public class TranscodingServiceImplTest {

    ...

    @Mock

    private JobRepository jobRepository;

    

    private TranscodingService transcodingService;

    ...

    @Test

    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {

        try {

            transcodingService.transcode(jobId);

            fail("발생해야 함");

        } catch (Exception ex) {

        }


        Job job = jobRepository.findById(jobId);

        assertFalse(job.isSuccess());

        assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());

        ...

    }

}


컴파일 에러다. 컴파일 에러가 나지 않도록 다음과 같이 메서드/enum 타입 등을 추가해 주자.


public interface JobRepository {

    Job findById(Long jobId);

}



public class Job {


    public static enum State {

        MEDIASOURCECOPYING

    }


    public boolean isSuccess() {

        return false;

    }


    public State getLastState() {

        return null;

    }


}


컴파일 에러를 제거했으므로 테스트를 실행한다. 테스트가 실패한다. 원인은 다음과 같다.


java.lang.AssertionError: 발생해야 함

at org.junit.Assert.fail(Assert.java:91)

at org.chimi.s4t.domain.transcode.TranscodingServiceImplTest.transcodeFailBecauseExceptionOccuredAtMediaSourceCopier(TranscodingServiceImplTest.java:74)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

        ...


아! transcodingService.transcode() 메서드가 익셉션을 발생시켜야 하는데, 무사히 실행되어서 fail()에 걸렸다. 이 테스트는 원본 미디어 파일을 복사해 오는 도중에 에러가 발생한 경우를 가정한 것이므로 mediaSourceCopier Mock 객체가 익셉션을 발생시키도록 하자.


    @Test

    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {

        RuntimeException mockException = new RuntimeException();

        when(mediaSourceCopier.copy(jobId)).thenThrow(mockException);

        

        try {

            transcodingService.transcode(jobId);

            fail("발생해야 함");

        } catch (Exception ex) {

            assertSame(mockException, ex);

        }


        Job job = jobRepository.findById(jobId);

        assertFalse(job.isDone());

        assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());

        ...

    }

    



다시 테스트 실행.... 빨간색이다. NullPointerException이다. 스택트레이스를 보니 아래 줄에서 job이 null이어서 발생한 것이다.


assertFalse(job.isSuccess());


jobRepository가 Job 객체를 리턴하도록 Mock 객체를 설정하자.


    @Test

    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {

        when(jobRepository.findById(jobId)).thenReturn(new Job());

        

        RuntimeException mockException = new RuntimeException();

        when(mediaSourceCopier.copy(jobId)).thenThrow(mockException);

        

        try {

            transcodingService.transcode(jobId);

            fail("발생해야 함");

        } catch (Exception ex) {

            assertSame(mockException, ex);

        }


        Job job = jobRepository.findById(jobId);

        assertFalse(job.isSuccess());

        assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());



다시 테스트 실행. 에러가 났다. job.getLastState()가 MEDIASOURCECOPYING이 아니라는 에러다. 그런데, job.isSuccess()가 false인지 확인하는 코드는 통과되었다. 이런! 잠시 고민하다가, 앞에서 작성했었던 transcodeSuccessfully() 테스트에 다음과 같이 Job에 대한 검증 코드를 추가하였다.



    @Test

    public void transcodeSuccessfully() {

        when(jobRepository.findById(jobId)).thenReturn(new Job());

        ...

        transcodingService.transcode(jobId);


        Job job = jobRepository.findById(jobId);

        assertTrue(job.isSuccess());

        assertEquals(Job.State.COMPLETED, job.getLastState());

        ... // Mock 객체들의 상호작용 확인

    }


    @Test

    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {

        when(jobRepository.findById(jobId)).thenReturn(new Job());

        

        RuntimeException mockException = new RuntimeException();

        when(mediaSourceCopier.copy(jobId)).thenThrow(mockException);

        

        try {

            transcodingService.transcode(jobId);

            fail("발생해야 함");

        } catch (Exception ex) {

            assertSame(mockException, ex);

        }


        Job job = jobRepository.findById(jobId);

        assertFalse(job.isSuccess());

        assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());

        ... // Mock 객체들의 상호작용 확인

    }


Job.State enum 타입에 COMPLETED를 추가한 뒤에 다시 테스트를 실행해 보자. 에러다! 두 테스트 메서드에서 모두 실패가 발생했다.

  • transcodeSuccessfully() 메서드의 실패 지점
    • assertTrue(job.isSuccess()) 
    • 아직 assertEquals(Job.State.COMPLETED, job.getLastState()) 는 확인도 못한다.
  • transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() 메서드의 실패 지점
    • assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());

이제 이 두 테스트 메서드가 모두 통과되도록 TranscodingServiceImpl을 수정해 보자.


TranscodingServiceImpl 구현 고민 그리고 새로운 협업 객체 등장


테스트를 통과시키기 위해서 TranscodingServiceImpl 클래스에 다음의 처리를 해야 한다.

  • 성공적으로 실행되면 Job의 상태를 COMPLETED로 바꾸고 성공 여부를 true로 지정한다.
  • 원본 파일 복사 전에 Job의 상태를 MEDIASOURCECOPYING로 바꾸고, 복사 과정 중 익셉션이 발생하면 Job의 성공 여부를 false로 지정한다.
위의 내용을 보면 뭔가 Job의 상태를 변경해주는 기능이 필요해 보인다. 또한, 익셉션이 발생하면 에러 상태로 변경해 주어야 할 것 같기도 하다.

우선, TranscodingServceImpl 클래스에 상태를 변경해주는 기능을 아래와 같이 별도의 메서드인 changeJobState()로 추상화하고, transcode()는 그 메서드를 호출하도록 했다.

public class TranscodingServiceImpl implements TranscodingService {
    ...
    @Override
    public void transcode(Long jobId) {
        changeJobState(jobId, Job.State.MEDIASOURCECOPYING);
        File multimediaFile = copyMultimediaSourceToLocal(jobId);
        List<File> multimediaFiles = transcode(multimediaFile, jobId);
        List<File> thumbnails = extractThumbnail(multimediaFile, jobId);
        storeCreatedFilesToStorage(multimediaFiles, thumbnails, jobId);
        notifyJobResultToRequester(jobId);
        changeJobState(jobId, Job.State.COMPLETED);
    }

    private void changeJobState(Long jobId, State newJobState) {
        jobStateChanger.chageJobState(jobId, newJobState); // 컴파일 에러
    }


위 코드에서 changeJobState() 메서드는 Job의 상태 변경 처리를 jobStateChanger 객체에 위임하고 있다. (이런 위임은 이미 copyMultimediaSourceToLocal() 메서드나 extractThumbnail() 메서드 등에서 동일하게 사용하고 있으며, 위임을 하면 Mock 객체를 사용해서 빠르게 테스트를 통과시킬 수 있게 된다.) jobStateChange 필드가 없어서 컴파일 에러가 나므로, jobStateChanger 필드를 추가하고 JobStateChanger 인터페이스를 만들고, 그 인터페이스를 changeJobState() 메서드를 정의해서 컴파일 에러를 제거한다. 또한, 생성자를 통해서 JobStateChanger 객체를 전달받도록 수정하자.

public class TranscodingServiceImpl implements TranscodingService {
    private MediaSourceCopier mediaSourceCopier;
    private Transcoder transcoder;
    private ThumbnailExtractor thumbnailExtractor;
    private CreatedFileSaver createdFileSaver;
    private JobResultNotifier jobResultNotifier;
    private JobStateChanger jobStateChanger;

    public TranscodingServiceImpl(MediaSourceCopier mediaSourceCopier,
            Transcoder transcoder, ThumbnailExtractor thumbnailExtractor,
            CreatedFileSaver createdFileSaver,
            JobResultNotifier jobResultNotifier, JobStateChanger jobStateChanger) {
        ...
        this.jobStateChanger = jobStateChanger;
    }

    @Override
    public void transcode(Long jobId) {
        changeJobState(jobId, Job.State.MEDIASOURCECOPYING);
        File multimediaFile = copyMultimediaSourceToLocal(jobId);
        List<File> multimediaFiles = transcode(multimediaFile, jobId);
        List<File> thumbnails = extractThumbnail(multimediaFile, jobId);
        storeCreatedFilesToStorage(multimediaFiles, thumbnails, jobId);
        notifyJobResultToRequester(jobId);
        changeJobState(jobId, Job.State.MEDIASOURCECOPYING);
    }

    private void changeJobState(Long jobId, State newJobState) {
        jobStateChanger.chageJobState(jobId, newJobState);
    }

생성자가 수정되었으므로, 기존에 생성자를 사용한 테스트 코드에서 컴파일 에러가 발생한다. 컴파일 에러가 발생하지 않도록 JobStateChanger에 대한 Mock 객체를 만들고, 생성자에 전달해주도록 하자.

@RunWith(MockitoJUnitRunner.class)
public class TranscodingServiceImplTest {
    ...
    @Mock
    private JobRepository jobRepository;
    @Mock
    private JobStateChanger jobStateChanger;

    private TranscodingService transcodingService;

    @Before
    public void setup() {
        transcodingService = new TranscodingServiceImpl(mediaSourceCopier,
                transcoder, thumbnailExtractor, createdFileSender,
                jobResultNotifier, jobStateChanger);
    }

새로운 협업 객체가 생겼다. 이제 첫 번째로 할 작업은 정상적으로 실행된 경우의 테스트를 통과시키는 것이다. 이를 위해 해야 할 작업은 JobStateChanger의 Mock 객체가 알맞게 Job의 상태를 변경시키도록 하는 것이다. 아래 코드는 테스트를 통과시키기 위해 Mock 객체에 기능을 부여한 코드이다.

@RunWith(MockitoJUnitRunner.class)
public class TranscodingServiceImplTest {
    ...
    @Mock
    private JobStateChanger jobStateChanger;

    private Job mockJob = new Job();
    
    private TranscodingService transcodingService;

    @Before
    public void setup() {
        transcodingService = new TranscodingServiceImpl(mediaSourceCopier,
                transcoder, thumbnailExtractor, createdFileSender,
                jobResultNotifier, jobStateChanger);

        doAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                Job.State newState = (State) invocation.getArguments()[1];
                mockJob.changeState(newState);
                return null;
            }
        }).when(jobStateChanger).chageJobState(anyLong(), any(Job.State.class));
    }

    @Test
    public void transcodeSuccessfully() {
        when(jobRepository.findById(jobId)).thenReturn(mockJob);
        ...
        Job job = jobRepository.findById(jobId);
        assertTrue(job.isSuccess());
        assertEquals(Job.State.COMPLETED, job.getLastState());
        ...
    }

    @Test
    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {
        when(jobRepository.findById(jobId)).thenReturn(mockJob);
        ...
        Job job = jobRepository.findById(jobId);
        assertFalse(job.isSuccess());
        assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());
        ...
    }
}

위 코드에서 주의할 점은 Job 객체를 mockJob으로 필드로 빼고, 각 테스트 메서드에서 jobRepository Mock 객체의 findById(jobId) 메서드가 호출되면 mockJob을 리턴하도록 구현했다는 것이다.

Job 클래스에 changeState() 메서드가 없어서 컴파일 에러가 발생하고 있으므로, changeState() 메서드를 구현할 차례이다. 이 메서드를 알맞게 구현해서 위 테스트가 통과되도록 만들면 된다. 통과시키려면 다음과 같이 changeState() 메서드를 구현해 주면 될 것 같다.

public class Job {

    public static enum State {
        MEDIASOURCECOPYING, COMPLETED
    }

    private State state;

    public boolean isSuccess() {
        return state == State.COMPLETED;
    }

    public State getLastState() {
        return state;
    }

    public void changeState(State newState) {
        this.state = newState;
    }

}

이제 테스트를 실행해 보자. 오, 예! 녹색바다.

실패의 의미가 없다.

미디어 원본 파일을 복사하는 것 뿐만 아니라 변환 처리, 썸네일 추출, 결과 전송, 통지에 대해서도 동일하게 실패 과정에 대한 테스트를 넣으려고 했으나, 마음에 안 드는 부분을 발견했다. transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() 메서드의 Job에 대한 검증 부분을 다시 살펴보자.

    @Test
    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {
        ...
        Job job = jobRepository.findById(jobId);
        assertFalse(job.isSuccess());
        assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());
        ...
    }

job.isSuccess()는 작업의 성공 여부는 알려주는데, 작업이 중간에 실패했는지 아니면 작업이 진행중인지의 여부는 알려주지 못한다. 그렇다, 우리에겐 다음이 필요하다.
  • 작업이 시작 전 / 진행 중 / 끝남 여부
  • 작업의 성공/실패 여부
    • 실패했다면, 어느 단계에서 실패했는 지 그리고 실패 원인
몇 가지 assert를 떠올려 봤는데, 아래와 같이 하면 좋을 것 같다.

    @Test
    public void transcodeSuccessfully() {
        ...
        Job job = jobRepository.findById(jobId);
        assertTrue(job.isWaiting());
        transcodingService.transcode(jobId);

        job = jobRepository.findById(jobId);
        assertTrue(job.isFinished());
        assertTrue(job.isSuccess());
        assertEquals(Job.State.COMPLETED, job.getLastState());
        assertNull(job.getOccurredException());
        ... // verify 들
    }

    @Test
    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {
        ...
        try {
            transcodingService.transcode(jobId);
            fail("발생해야 함");
        } catch (Exception ex) {
            assertSame(mockException, ex);
        }

        Job job = jobRepository.findById(jobId);
        assertTrue(job.isFinished());
        assertFalse(job.isSuccess());
        assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());
        assertNotNull(job.getOccurredException());
        ... // verify 들
    }

추가된 메서드는 다음과 같다.
  • Job.isWaiting(): 작업이 대기 중인지의 여부
  • Job.isFinished(): 작업이 완료되었는지의 여부
  • Job.getOccuredException(): 진행 과정 중 발생한 익셉션을 구한다.
Job.isSuccess()는 작업이 성공/실패 여부를 의미하도록 변경했다. 컴파일 에러가 발생하므로, 각각의 메서드를 추가해 주고 점진적으로 테스트를 통과시켜 나갈 차례이다. transcodeSuccessfully() 테스트가 통과될 때 까지 테스트 실행/Job 클래스 수정을 반복해서 Job 클래스를 다음과 같이 수정하였다.

public class Job {

    public static enum State {
        MEDIASOURCECOPYING, COMPLETED
    }

    private State state;
    private Exception occurredException;

    public void changeState(State newState) {
        this.state = newState;
    }

    public boolean isWaiting() {
        return state == null;
    }

    public boolean isFinished() {
        return isSuccess() || isExceptionOccurred();
    }

    public boolean isSuccess() {
        return state == State.COMPLETED;
    }

    private boolean isExceptionOccurred() {
        return occurredException != null;
    }

    public State getLastState() {
        return state;
    }

    public Exception getOccurredException() {
        return occurredException;
    }

}

이제 남은 작업은 transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() 테스트를 통과시킬 차례이다. 실패 케이스의 테스트를 실행해보면 아래 코드를 통과하지 못한다.

assertTrue(job.isFinished()); // 통과 실패
assertFalse(job.isSuccess());
assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());
assertNotNull(job.getOccurredException());

job.isFinished() 메서드는 작업이 성공했거나 익셉션이 발생한 경우 true를 리턴하도록 구현하였다. 따라서, 필요한 것은 TranscodingServiceImpl이 처리 도중 익셉션이 발생하면 그 사실을 Job 객체에 알려주는 것이다. 뭔가 Job 객체에게 알려주는 기능이 필요해졌다. 음,, 지금까지 글을 따라 읽어왔다면 이 쯤에서 새로운 협업 객체가 등장할 것 같은 느낌을 받을 것이다.

익셉션 발생을 Job에 알리기 위한 새로운 협업 객체의 등장

각 처리 과정에서 익셉션이 발생하면 그 사실을 Job에게 알려주는 기능을 추가하자. 약간의 고민을 한 끝에 아래와 같이 해 보기로 결정했다.

public class TranscodingServiceImpl implements TranscodingService {
    ...
    @Override
    public void transcode(Long jobId) {
        changeJobState(jobId, Job.State.MEDIASOURCECOPYING);
        File multimediaFile = copyMultimediaSourceToLocal(jobId);
        ...
    }

    private void changeJobState(Long jobId, State newJobState) {
        jobStateChanger.chageJobState(jobId, newJobState);
    }

    private File copyMultimediaSourceToLocal(Long jobId) {
        try {
            return mediaSourceCopier.copy(jobId);
        } catch (RuntimeException ex) {
            transcodingExceptionHandler.notifyToJob(jobId, ex);
            throw ex;
        }
    }
    ... // 나머지 메서드도 동일하게 transcodingExceptionHandler 사용 코드 추가

transcodingExceptionHandler라는 새로운 협업 객체가 등장했다. 이 객체의 notifyToJob() 메서드는 Job 객체에게 익셉션이 발생했다는 사실을 알린다. 그리고 익셉션이 사라지지 않도록 재전파한다.

transcodingExceptionHandler 협업 객체가 없으므로, 필드로 정의하고 transcodingExceptionHandler 객체를 위한 인터페이스를 생성하고, 그 인터페이스에 notifyToJob() 메서드를 정의하자. 물론, 생성자를 통해서 추가된 협업 객체를 전달받도록 수정하는 것도 잊지 말자.

public class TranscodingServiceImpl implements TranscodingService {
    ...
    private JobStateChanger jobStateChanger;
    private TranscodingExceptionHandler transcodingExceptionHandler;

    public TranscodingServiceImpl(MediaSourceCopier mediaSourceCopier,
            Transcoder transcoder, ThumbnailExtractor thumbnailExtractor,
            CreatedFileSaver createdFileSaver,
            JobResultNotifier jobResultNotifier,
            JobStateChanger jobStateChanger,
            TranscodingExceptionHandler transcodingExceptionHandler) {
        ...
        this.transcodingExceptionHandler = transcodingExceptionHandler;
    }

    @Override
    public void transcode(Long jobId) {
        changeJobState(jobId, Job.State.MEDIASOURCECOPYING);
        File multimediaFile = copyMultimediaSourceToLocal(jobId);
        ...
        changeJobState(jobId, Job.State.COMPLETED);
    }

    private void changeJobState(Long jobId, State newJobState) {
        jobStateChanger.chageJobState(jobId, newJobState);
    }

    private File copyMultimediaSourceToLocal(Long jobId) {
        try {
            return mediaSourceCopier.copy(jobId);
        } catch (RuntimeException ex) {
            transcodingExceptionHandler.notifyToJob(jobId, ex);
            throw ex;
        }
    }

TranscodingServiceImpl의 생성자를 변경했으므로, 테스트 코드도 함께 수정해 준다.

@RunWith(MockitoJUnitRunner.class)
public class TranscodingServiceImplTest {
    ...
    @Mock
    private TranscodingExceptionHandler transcodingExceptionHandler;

    private Job mockJob = new Job();

    private TranscodingService transcodingService;

    @Before
    public void setup() {
        transcodingService = new TranscodingServiceImpl(mediaSourceCopier,
                transcoder, thumbnailExtractor, createdFileSender,
                jobResultNotifier, jobStateChanger, transcodingExceptionHandler);
        ...
    }


앞서 실패했던 부분을 다시 보자.

    @Test
    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {
        ...
        try {
            transcodingService.transcode(jobId);
            fail("발생해야 함");
        } catch (Exception ex) {
            assertSame(mockException, ex);
        }

        Job job = jobRepository.findById(jobId);

        assertTrue(job.isFinished()); // 통과 실패
        assertFalse(job.isSuccess());
        assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());
        assertNotNull(job.getOccurredException());
...

    }

이제 남은 작업은 새로 추가된 협업 객체를 이용해서 위 테스트과 통과되도록 만드는 것이다. 우선, 테스트 클래스에 새롭게 추가된 Mock 객체인 transcodingExceptionHandler의 notifyToJob() 메서드가 호출되면 mockJob객체에 발생한 익셉션을 전달할 수 있도록 exceptionOccured() 메서드를 호출한다.

    @Before
    public void setup() {
        transcodingService = new TranscodingServiceImpl(mediaSourceCopier,
                transcoder, thumbnailExtractor, createdFileSender,
                jobResultNotifier, jobStateChanger, transcodingExceptionHandler);

        ...

        doAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                RuntimeException ex = (RuntimeException) invocation.getArguments()[1];
                mockJob.exceptionOccurred(ex);
                return null;
            }
        }).when(transcodingExceptionHandler).notifyToJob(anyLong(),
                any(RuntimeException.class));
    }

물론, 아직 Job 클래스에 exceptionOccurred() 메서드는 없다. 이제부터 만들어야 한다. 이 메서드를 만드는 건 어렵지 않을 것 같다. 다음과 같이 이 메서드를 통해서 전달받은 익셉션 객체를 필드에 보관하기만 하면 된다.

package org.chimi.s4t.domain.job;

public class Job {
    ...
    public boolean isFinished() {
        return isSuccess() || isExceptionOccurred();
    }

    public boolean isSuccess() {
        return state == State.COMPLETED;
    }

    private boolean isExceptionOccurred() {
        return occurredException != null;
    }

    public Exception getOccurredException() {
        return occurredException;
    }

    public void exceptionOccurred(RuntimeException ex) {
        occurredException = ex;
    }

}

필요한 구현을 한 것 같으므로 테스트를 실행해 보자. 야호! 녹색바다!

이제 나머지 실패 시나리오에 대해서 추가적으로 테스트 클래스를 만들고 비슷한 과정을 반복하면 된다. 아, 잠깐! 비슷한 과정을 반복하다보면 각각의 테스트 메서드에 중복된 코드가 출현하게 된다. 중복은 악의 축이므로, 이를 없애야 한다. 테스트 코드도 유지보수가 가능하도록 리팩토링을 해 주어야 하는데, 이 과정은 연습 3에서 진행해 본다.


결과는?

  • 드디어 Job과 JobRepository가 출현했다.
  • 두 개의 협업 객체가 새롭게 추가되었다. (최초에 필자가 머리속으로 상상했던 것 보다 더 많은 협업 객체가 출현했다.)
  • 성공 시나리오와 실패 시나리오에 대한 테스트도 만들어졌다.

연습3에서는 앞서 말한데로 테스트 코드의 중복을 제거해 보도록 하자.

Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 백명석 2012.10.25 07:16 신고  댓글주소  수정/삭제  댓글쓰기

    . service, repository, controller 등에는 위임(상호작용)을 제외하고는 테스트가 필요할 수준의 코드가 없는 것이 좋다고 생각함. 그에 비해 위 테스트는 너무 자세함. exception이 발생해야 할 경우 발생했나만 확인하면 되지 굳이 catch로 막고, assert/verify 등을 할 필요가 있을까 ?
    . TranscodingService의 생성자에 인자의 수가 너무 많은 듯(6개). 이럴 경우 setter가 더 보기 좋을 수도. 그리고 이렇게 많아야만 하는지가 가장 의문시 됨.
    . Job#changeState에서 state를 바로 변경하기 보다 start, end 등의 trigger 메소드를 추가하고 해당 메소드에서 state 객체에게 위임(this를 넘겨서 state가 자신을 변경하고 job의 상태도 변경하도록)하는 것이 좋을 듯.

    • 최범균 madvirus 2012.10.25 09:24 신고  댓글주소  수정/삭제

      TranscodingService와 Job의 State는 다음과 같은 고민의 결과입니다. 아직까지 완전한 답을 찾지 못했고, 그래서 일단 제가 하기 쉬운 방법으로 구현을 진행하게 되었어요.

      첫 번째 고민거리는 Job에 execute() 메서드를 넣고, 그 execute() 메서드에서 변환 처리를 수행하는 것에 대한 부분이였어요. Job이 작업을 처리하면, TranscodingService는 리포지토리에서 Job을 구하고, 그 Job의 execute() 메서드를 호출하는 걸로 끝나게 되죠.

      처음에는 이 방법으로 약간의 구현을 생각해봤는데, 다음과 같은 것들이 걸렸어요.
      - 각 단계가 끝나면 Job의 상태가 변경되어서 작업 요청자가 확인할 수 있어야 함
      - State 패턴을 적용해서 State별로 자기 작업을 수행하고, 실패를 처리하는 등의 것을 생각해 봄
      - 그런데, 향후에 뭐가 되었든 Job이 Persistence 영역과 엮이게 되면, 각 State 객체의 메서드가 내부적으로 트랜잭션을 처리해 주어야 하고, State의 전이가 발생하면 Job의 물리적 데이터가 해당 State로 변경되어야 함

      그런데, 제가 좋아하는 ORM과 스프링을 사용할 경우, (사실, 이게 문제죠! 제가 구현을 너무 빨리 고려했다는 것),
      - Job은 DB 세션과 연결되어 있어서 State의 전이가 일어날 때 그걸 바로 반영하려면 뭔가 Persistece 관련 기능에 의존할 것 같고
      - State의 메서드마다 별도로 트랜잭션을 처리하려면 뭔가 Transaction을 추상화해서 Inject 해 줘야 할 것 같고,
      - State의 전이는 새로운 State 객체의 생성과도 연결되니 객체의 생성 시점에 Inject 하는 방법이 필요할 것 같고

      음,, 이렇게 생각이 뻗어나가면서 해답을 찾지 못했어요. 이건 좋은 아이디어 있으면 좀 주셔요.

      하튼, 그래서 트랜잭션을 쉽게 분리할 수 있는 방법을 찾았고, 그 결과로 각 단계마다 별도 협업 객체를 출현시키는 방법이 출현하게 되었어요.

      이렇게 되다보니 Job의 상태가 단순 값으로 바뀌었고, 상태 전이 부분은 향후 리팩을 기약하며(^^;) 먼저 동작하는 코드를 만들었어요.

      시간이 많으면 더 고민하겠는데, 저녁에 짬짬히 하다보니 (드라마를 병행하면서....) 생각이 잘 안 떠오르네요.