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

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

지금까지 한 번에 한 개의 변환 결과만 생성하는 기능을 구현했다. 이번엔 한 번에 여러 형식의 변환 결과를 생성해주는 기능을 구현해보도록 하자.


두 개 이상의 OutputFormat을 입력으로 받는 테스트 추가하기


TDD니까, 테스트를 먼저 작성해보도록 하자.


public class FfmpegTranscoderTest {

    ...

    private OutputFormat mp4Format;

    private OutputFormat mp4Format2;

    private OutputFormat aviFormat;


    @Before

    public void setup() {

        outputFormats = new ArrayList<OutputFormat>();

        mp4Format = new OutputFormat(160, 120, 150, Container.MP4,

                VideoCodec.H264, AudioCodec.AAC);

        mp4Format2 = new OutputFormat(80, 60, 80, Container.MP4,

                VideoCodec.H264, AudioCodec.AAC);

        aviFormat = new OutputFormat(160, 120, 150, Container.AVI,

                VideoCodec.MPEG4, AudioCodec.MP3);

        multimediaFile = new File("src/test/resources/sample.avi");


        transcoder = new FfmpegTranscoder();

    }

    ...

    private void executeTranscoderAndAssert() {

        List<File> transcodedFiles = transcoder.transcode(multimediaFile,

                outputFormats);

        assertEquals(outputFormats.size(), transcodedFiles.size());

        for (int i = 0; i < outputFormats.size(); i++) {

            assertTrue(transcodedFiles.get(i).exists());

            VideoFormatVerifier.verifyVideoFormat(outputFormats.get(i),

                    transcodedFiles.get(i));

        }

    }


    @Test

    public void transcodeWithTwoMp4OutputFormats() {

        outputFormats.add(mp4Format);

        outputFormats.add(mp4Format2);

        executeTranscoderAndAssert();

    }

}


새로운 크기를 갖는 OutputFormat 객체인 mp4Format2를 필드로 추가하였고, transcodeWithTwoMp4OutputFormats() 메서드를 이용해서 두 개의 변환 결과에 대한 테스트를 수행하도록 했다. executeTrancoderAndAssert() 메서드는 두 개 이상의 변환 요청을 검증할 수 있도록 수정하였다.


테스트를 실행해보자. 아래와 같은 메시지와 함께 테스트에 실패한다.


java.lang.AssertionError: expected:<160> but was:<80>

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

    at org.junit.Assert.failNotEquals(Assert.java:645)

    at org.junit.Assert.assertEquals(Assert.java:126)

    at org.junit.Assert.assertEquals(Assert.java:470)

    at org.junit.Assert.assertEquals(Assert.java:454)

    at org.chimi....VideoFormatVerifier.assertVideoFile(VideoFormatVerifier.java:88)

    at org.chimi....VideoFormatVerifier.verify(VideoFormatVerifier.java:40)

    at org.chimi....VideoFormatVerifier.verifyVideoFormat(VideoFormatVerifier.java:18)

    at org.chimi.....executeTranscoderAndAssert(FfmpegTranscoderTest.java:64)

    at org.chimi....transcodeWithTwoMp4OutputFormats(FfmpegTranscoderTest.java:79)

    ... 


이유는 뭘까? FfmpegTranscoder 클래스는 다음과 같이 동일한 비디오 컨테이너에 대해 항상 동일한 파일을 생성한다. 따라서, 가장 마지막으로 요청한 OutputFormat의 결과만 최종적으로 남아있게 된다.


public class FfmpegTranscoder implements Transcoder {

    ...

    private String getFileName(OutputFormat format) {

        return "outputFile." + format.getFileExtension();

    }


}


예를 들어, 위 테스트의 경우 크기가 80*60인 비디오 파일이 최종적으로 남게 되고, 따라서 생성된 비디오를 검증하는 과정에서 첫 번째 변환 요청인 160*120 크기를 검사하는 과정에서 테스트를 통과하지 못하게 되었다.


이 테스트를 통과시키려면 FfmpegTranscoder의 getFileName() 메서드가 매번 서로 다른 파일 이름을 리턴해 주어야 한다. 변환된 결과 파일 이름의 경우 변환 시스템을 사용하는 곳에 따라 작명 규칙이 달라질 수 있을 것 같다. 그래서, 파일 이름을 생성하는 규칙을 NamingRule이라는 별도 인터페이스로 분리하고 FfmpegTranscoder는 NamingRule에 위임하도록 코드를 구성하는 게 좋을 것 같다. 아래는 그렇게 코딩한 결과이다.


public class FfmpegTranscoder implements Transcoder {


    private NamingRule namingRule;


    public FfmpegTranscoder(NamingRule namingRule) {

        this.namingRule = namingRule;

    }

    ...

    private String getFileName(OutputFormat format) {

        return namingRule.createName(format);

    }


}


NamingRule 인터페이스를 다음과 같이 만들었다.


public interface NamingRule {


    String createName(OutputFormat format);


}


FfmpegTranscoder의 생성자가 변경되었으므로, FfmpegTranscoderTest 클래스가 컴파일 에러가 난다. 컴파일 에러가 나지 않도록 NamingRule을 전달해주자.


public class FfmpegTranscoderTest {


    private Transcoder transcoder;

    ...

    private NamingRule namingRule;

    ...

    @Before

    public void setup() {

        outputFormats = new ArrayList<OutputFormat>();

        mp4Format = new OutputFormat(160, 120, 150, Container.MP4,

                VideoCodec.H264, AudioCodec.AAC);

        ...

        transcoder = new FfmpegTranscoder(namingRule); // 아직 namingRule은 null

    }


NamingRule의 Mock 객체를 만들어서 테스트를 통과시킬까 고민하다가, DefaultNamingRule이라는 걸 만드는 게 좋을 것 같다라는 결론에 도달했다. 다음과 같이 NamingRule 인터페이스에 이너 클래스로 DefaultNamingRule 클래스를 정의했고, DEFAULT 상수가 값으로 DefaultNameRule 객체를 갖도록 했다.


public interface NamingRule {


    String createName(OutputFormat format);


    public static final NamingRule DEFAULT = new DefaultNamingRule();


    public static class DefaultNamingRule implements NamingRule {


        private Random random = new Random();

        private String baseDir;


        public DefaultNamingRule() {

            baseDir = System.getProperty("java.io.tmpdir");

        }


        public void setBaseDir(String baseDir) {

            this.baseDir = baseDir;

        }


        @Override

        public String createName(OutputFormat format) {

            String fileName = getFileNameFromTime();

            File file = createFileFromFileNameAndFormat(format, baseDir,

                    fileName);

            return file.getPath();

        }


        private String getFileNameFromTime() {

            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");

            String time = dateFormat.format(new Date());

            int num = random.nextInt(1000);

            String fileName = time + "_" + num;

            return fileName;

        }


        private File createFileFromFileNameAndFormat(OutputFormat format,

                String tempDir, String fileName) {

            File file = new File(tempDir, fileName + "."

                    + format.getFileExtension());

            return file;

        }

    }

}


이제 FfmpegTranscoderTest가 NamingRule로 NamingRule.DEFAULT를 사용하도록 수정할 차례이다.


public class FfmpegTranscoderTest {


    private Transcoder transcoder;

    private File multimediaFile;

    private List<OutputFormat> outputFormats;


    private NamingRule namingRule; // 필요 없으므로 삭제


    private OutputFormat mp4Format;

    private OutputFormat mp4Format2;

    private OutputFormat aviFormat;


    @Before

    public void setup() {

        outputFormats = new ArrayList<OutputFormat>();

        mp4Format = new OutputFormat(160, 120, 150, Container.MP4,

                VideoCodec.H264, AudioCodec.AAC);

        mp4Format2 = new OutputFormat(80, 60, 80, Container.MP4,

                VideoCodec.H264, AudioCodec.AAC);

        aviFormat = new OutputFormat(160, 120, 150, Container.AVI,

                VideoCodec.MPEG4, AudioCodec.MP3);

        multimediaFile = new File("src/test/resources/sample.avi");


        transcoder = new FfmpegTranscoder(NamingRule.DEFAULT);

    }


테스트를 실행한다. 녹색! 통과다. 서로 다른 포맷을 갖는 OutputFormat을 생성하는 기능 구현이 완료되었다.


OutputFormat: 컨테이너와 코덱 옵션


OutputFormat의 생성자는 컨테이너 정보와 코덱 정보를 함께 받고 있다. 하지만, 컨테이너의 기본 코덱을 사용할 수 있기 때문에 코덱 정보를 전달하지 않을 경우 컨테이너의 기본 코덱을 사용하도록 하는 것이 편리할 것 같다. 이를 위해 OutputFormat 생성자에 코덱 정보를 전달하지 않는 테스트 코드부터 작성해보자. OutputFormat를 생성할 때 컨테이너 정보만 전달해도 정상적으로 실행되는 지 확인하면 되므로  VideoConverterTest를 이용해보도록 하겠다.


public class VideoConverterTest {

    ...

    @Test

    public void transcodeWithOnlyContainer() {

        IMediaReader reader = ToolFactory.makeReader(SOURCE_FILE);

        

        OutputFormat outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                Container.AVI); // 일치하는 생성자 없으므로 컴파일 에러

        VideoConverter writer = new VideoConverter("target/sample.avi", reader,

                outputFormat);

        reader.addListener(writer);

        while (reader.readPacket() == null)

            do {

            } while (false);

        

        VideoFormatVerifier.verifyVideoFormat(outputFormat, new File(

                "target/sample.avi"));

    }

}


컴파일 에러를 없애야 하므로 OutputFormat에 생성자를 추가해주고, 다음과 같이 구현해 주었다.


public class OutputFormat {


    private int width;

    private int height;

    private int bitrate;

    private Container container;

    private VideoCodec videoCodec;

    private AudioCodec audioCodec;


    public OutputFormat(int width, int height, int bitrate,

            Container container, VideoCodec videoCodec, AudioCodec audioCodec) {

        this.width = width;

        this.height = height;

        this.bitrate = bitrate;

        this.container = container;

        this.videoCodec = videoCodec;

        this.audioCodec = audioCodec;

    }


    public OutputFormat(int width, int height, int bitrate, Container container) {

        this(width, height, bitrate, container, null, null);

    }


컴파일 에러를 없앴으므로, VideoConverterTest를 실행해보자. transcodeWithOnlyContainer() 테스트 메서드는 통과하지 못했다. 실패 메시지 및 관련 위치는 다음과 같다.


-- 실패 메시지

java.lang.AssertionError: expected:<null> but was:<MPEG4>

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

    at org.junit.Assert.failNotEquals(Assert.java:645)

    at org.junit.Assert.assertEquals(Assert.java:126)

    at org.junit.Assert.assertEquals(Assert.java:145)

    at org....VideoFormatVerifier.assertVideoFile(VideoFormatVerifier.java:90)

    at org....VideoFormatVerifier.verify(VideoFormatVerifier.java:40)

    at org....VideoFormatVerifier.verifyVideoFormat(VideoFormatVerifier.java:18)

    at org....VideoConverterTest.transcodeWithOnlyContainer(VideoConverterTest.java:52)


-- 실패 위치

    private void assertVideoFile() {

        assertEquals(expectedFormat.getWidth(), width);

        assertEquals(expectedFormat.getHeight(), height);

        assertEquals(expectedFormat.getVideoCodec(),

                CodecValueConverter.toDomainVideoCodec(videoCodec)); // 테스트 실패

        assertEquals(expectedFormat.getAudioCodec(),

                CodecValueConverter.toDomainAudioCodec(audioCodec));

    }


OutputFormat을 생성하는 과정에서 videoCodec의 값으로 null을 전달했기 때문에, expectedFormat.getVideoCodec()가 null을 리턴했다. 동일하게 audioCodec도 null 값을 갖게 된다. 이 문제는 간단하게 처리할 수 있다. 다음과 같이 코덱을 전달받지 않는 OutputFormat 생성자에서 null 대신 Container의 기본 코덱을 전달하도록 코드를 수정해 주면 될 것 같다.


public class OutputFormat {

    ...

    public OutputFormat(int width, int height, int bitrate,

            Container container, VideoCodec videoCodec, AudioCodec audioCodec) {

        ...

        this.videoCodec = videoCodec;

        this.audioCodec = audioCodec;

    }


    public OutputFormat(int width, int height, int bitrate, Container container) {

        this(width, height, bitrate, container, 

                container.getDefaultVideoCodec(), container.getDefaultAudioCodec());

    }


다시 VideoConverterTest를 실행해 보자. 녹색! 통과다.


VideoConverterTest 작은 손질


VideoConverterTest를 정리할 타이밍이 왔다. 현재까지 만들어진 코드는 다음과 같다.


public class VideoConverterTest {


    private static final int WIDTH = 160;

    private static final int HEIGHT = 120;

    private static final int BITRATE = 150;

    private static final String SOURCE_FILE = "src/test/resources/sample.avi";

    private static final String TRANSCODED_FILE = "target/sample.mp4";


    @Test

    public void transcode() {

        IMediaReader reader = ToolFactory.makeReader(SOURCE_FILE);


        OutputFormat outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                Container.MP4, VideoCodec.H264, AudioCodec.AAC);

        VideoConverter writer = new VideoConverter(TRANSCODED_FILE, reader,

                outputFormat);

        reader.addListener(writer);

        while (reader.readPacket() == null)

            do {

            } while (false);


        VideoFormatVerifier.verifyVideoFormat(outputFormat, new File(

                TRANSCODED_FILE));

    }

    

    @Test

    public void transcodeWithOnlyContainer() {

        IMediaReader reader = ToolFactory.makeReader(SOURCE_FILE);

        

        OutputFormat outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                Container.AVI);

        VideoConverter writer = new VideoConverter("target/sample.avi", reader,

                outputFormat);

        reader.addListener(writer);

        while (reader.readPacket() == null)

            do {

            } while (false);

        

        VideoFormatVerifier.verifyVideoFormat(outputFormat, new File(

                "target/sample.avi"));

    }

}


두 테스트 메서드가 상당히 비슷하다. 우선, 두 메서드에서 사용되는 값들을 메서드의 앞 쪽에 변수로 선언해서 모으자.


public class VideoConverterTest {

    ...

    @Test

    public void transcode() {

        IMediaReader reader = ToolFactory.makeReader(SOURCE_FILE);

        OutputFormat outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                Container.MP4, VideoCodec.H264, AudioCodec.AAC);

        String outputFile = TRANSCODED_FILE;

        VideoConverter writer = new VideoConverter(outputFile, reader,

                outputFormat);

        reader.addListener(writer);

        while (reader.readPacket() == null)

            do {

            } while (false);


        VideoFormatVerifier.verifyVideoFormat(outputFormat,

                new File(outputFile));

    }


    @Test

    public void transcodeWithOnlyContainer() {

        IMediaReader reader = ToolFactory.makeReader(SOURCE_FILE);

        OutputFormat outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                Container.AVI);

        String outputFile = "target/sample.avi";

        VideoConverter writer = new VideoConverter(outputFile, reader,

                outputFormat);

        reader.addListener(writer);

        while (reader.readPacket() == null)

            do {

            } while (false);


        VideoFormatVerifier.verifyVideoFormat(outputFormat,

                new File(outputFile));

    }

}


두 메서드에서 reader, outputFormat, outputFile을 필드로 정의한다. 이클립스와 같은 IDE는 메서드 로컬 변수를 필드로 빼주는 기능을 제공하고 있으니 그 기능을 사용하면 편리하다. 아 그 전에 테스트 하는 것 잊지 말기 바란다.


public class VideoConverterTest {

    ...

    private IMediaReader reader;

    private OutputFormat outputFormat;

    private String outputFile;


    @Test

    public void transcode() {

        reader = ToolFactory.makeReader(SOURCE_FILE);

        outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                Container.MP4, VideoCodec.H264, AudioCodec.AAC);

        outputFile = TRANSCODED_FILE;

        VideoConverter writer = new VideoConverter(outputFile, reader,

                outputFormat);

        reader.addListener(writer);

        while (reader.readPacket() == null)

            do {

            } while (false);


        VideoFormatVerifier.verifyVideoFormat(outputFormat,

                new File(outputFile));

    }


    @Test

    public void transcodeWithOnlyContainer() {

        reader = ToolFactory.makeReader(SOURCE_FILE);

        outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                Container.AVI);

        outputFile = "target/sample.avi";

        VideoConverter writer = new VideoConverter(outputFile, reader,

                outputFormat);

        reader.addListener(writer);

        while (reader.readPacket() == null)

            do {

            } while (false);


        VideoFormatVerifier.verifyVideoFormat(outputFormat,

                new File(outputFile));

    }

}


이제 두 메서드에서 코드가 완전히 동일한 부분을 별도의 메서드로 분리해내자. 다음의 두 부분이 완전히 동일하다.

  • reader를 생성하는 부분 -> @Before를 붙인 setup 메서드로 이동
  • VideoConverter를 생성하고 변환하는 부분 -> testVideoConverter 메서드로 이동

public class VideoConverterTest {


    private static final int WIDTH = 160;

    private static final int HEIGHT = 120;

    private static final int BITRATE = 150;

    private static final String SOURCE_FILE = "src/test/resources/sample.avi";

    private static final String TRANSCODED_FILE = "target/sample.mp4";

    private IMediaReader reader;

    private OutputFormat outputFormat;

    private String outputFile;


    @Before

    public void setup() {

        reader = ToolFactory.makeReader(SOURCE_FILE);

    }


    @Test

    public void transcode() {

        outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE, Container.MP4,

                VideoCodec.H264, AudioCodec.AAC);

        outputFile = TRANSCODED_FILE;

        testVideoConverter();

    }


    private void testVideoConverter() {

        VideoConverter writer = new VideoConverter(outputFile, reader,

                outputFormat);

        reader.addListener(writer);

        while (reader.readPacket() == null)

            do {

            } while (false);


        VideoFormatVerifier.verifyVideoFormat(outputFormat,

                new File(outputFile));

    }


    @Test

    public void transcodeWithOnlyContainer() {

        outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE, Container.AVI);

        outputFile = "target/sample.avi";

        testVideoConverter();

    }

}


outputFormat과 outputFile을 초기화하는 부분도 비슷하다. 이 부분도 한 번 별도 메서드로 분리해보자.


public class VideoConverterTest {

    ...

    @Test

    public void transcode() {

        initOutput(Container.MP4, VideoCodec.H264, AudioCodec.AAC,

                TRANSCODED_FILE);

        testVideoConverter();

    }


    private void initOutput(Container outputContainer, VideoCodec videoCodec,

            AudioCodec audioCodec, String outputFileName) {

        if (videoCodec == null && audioCodec == null) {

            outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                    outputContainer);

        } else {

            outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                    outputContainer, videoCodec, audioCodec);

        }

        outputFile = outputFileName;

    }


    private void initOutput(Container outputContainer, String outputFileName) {

        initOutput(outputContainer, null, null, outputFileName);

        outputFile = outputFileName;

    }


    private void testVideoConverter() {

        ...

    }


    @Test

    public void transcodeWithOnlyContainer() {

        initOutput(Container.AVI, "target/sample.avi");

        testVideoConverter();

    }

}


이제 TRANSCODED_FILE 상수는 더 이상 의미가 없다. 이 상수를 사용하지 않도록 돌려 놓자.


지금까지의 작업으로 바뀐 VideoConverterTest는 다음과 같다.


public class VideoConverterTest {


    private static final int WIDTH = 160;

    private static final int HEIGHT = 120;

    private static final int BITRATE = 150;

    private static final String SOURCE_FILE = "src/test/resources/sample.avi";

    private IMediaReader reader;

    private OutputFormat outputFormat;

    private String outputFile;


    @Before

    public void setup() {

        reader = ToolFactory.makeReader(SOURCE_FILE);

    }


    @Test

    public void transcode() {

        initOutput(Container.MP4, VideoCodec.H264, AudioCodec.AAC,

                "target/sample.mp4");

        testVideoConverter();

    }


    @Test

    public void transcodeWithOnlyContainer() {

        initOutput(Container.AVI, "target/sample.avi");

        testVideoConverter();

    }


    private void initOutput(Container outputContainer, String outputFileName) {

        initOutput(outputContainer, null, null, outputFileName);

        outputFile = outputFileName;

    }


    private void initOutput(Container outputContainer, VideoCodec videoCodec,

            AudioCodec audioCodec, String outputFileName) {

        if (videoCodec == null && audioCodec == null) {

            outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                    outputContainer);

        } else {

            outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                    outputContainer, videoCodec, audioCodec);

        }

        outputFile = outputFileName;

    }


    private void testVideoConverter() {

        VideoConverter writer = new VideoConverter(outputFile, reader,

                outputFormat);

        reader.addListener(writer);

        while (reader.readPacket() == null)

            do {

            } while (false);


        VideoFormatVerifier.verifyVideoFormat(outputFormat,

                new File(outputFile));

    }

}


@Test 메서드가 단순해졌다. 새로운 컨테이너의 추가라든가, 코덱 변환 등을 테스트하고 싶을 때에 간단하게 테스트를 추가할 수 있게 되었다. testVideoConverter() 메서드와 initOutput() 메서드를 조금 더 정리하고 싶은 마음은 있지만, 일단 이 정도까지 코드를 정리하자.




Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 전홍석 2015.12.04 11:08 신고  댓글주소  수정/삭제  댓글쓰기

    안녕하세요
    스프링 4 프로그래밍 입문을 구입하여 열공중인 중년 초보개발자 입니다.
    대용량의 동영상 converter작업할일이 있어 찾아보니 저자님 글이 있어
    학습하여 익히려 합니다.
    염치 불구하고 위 전체 소스를 받아서 도움받을수 있도록 부탁드립니다.


    • 최범균 madvirus 2015.12.04 14:31 신고  댓글주소  수정/삭제

      아,,, 이게 그냥 막 연습삼아서 하던거라서,,,, 코드가 있는지 모르겠네요. 업무용 장비에는 코드가 없네요. 아무래도 내용 보시면 관련 코드가 나오니까, 그 코드를 참조하셔야 할 것 같아요.

앞서 작업까지의 결과물인 FfmpegTranscoder 클래스의 코드를 보자.


public class FfmpegTranscoder implements Transcoder {


    @Override

    public List<File> transcode(File multimediaFile,

            List<OutputFormat> outputFormats) {

        List<File> results = new ArrayList<File>();

        for (OutputFormat format : outputFormats) {

            results.add(transcode(multimediaFile, format));

        }

        return results;

    }


    private File transcode(File sourceFile, OutputFormat format) {

        IMediaReader reader = ToolFactory.makeReader(sourceFile

                .getAbsolutePath());


        String outputFile = "outputFile.mp4"; // 항상 확장자, 파일 명이 같음

        VideoConverter converter = new VideoConverter(outputFile, reader,

                format);

        ...

    }


}


원하는 파일이 MP4인지 AVI인지에 상관없이 항상 파일명은 outputFile.mp4 이다. 이제 이 부분을 건드려볼 차례이다.


우선, 확장자는 멀티미디어 컨테이너에 따라 다르다. 예를 들어, AVI, MP4, MOV, FLV 등이 컨테이너이며, 보통 이 이름을 파일의 확장자로 사용한다. 또한, 각 컨테이너의 특징에 따라 사용가능한 코덱에 제한이 있다고도 한다. 하튼, 필자는 이 부분의 전문가는 아니기 때문에 컨테이너 포맷이나 코덱 등의 정보가 궁금한 사람은 따로 정보를 검색해보시라.


컨테이너 모델 추가


확장자를 검사하도록 테스트를 먼저 수정하자. 확장자를 검사하기 적당한 위치는 VideoFormatVerifier 클래스이다. 이 클래스에 확장자를 확인하는 기능을 추가해 넣도록 하자.


public class VideoFormatVerifier {

    ...

    public VideoFormatVerifier(OutputFormat expectedFormat, File videoFile) {

        this.expectedFormat = expectedFormat;

        this.videoFile = videoFile;

    }


    public void verify() {

        try {

            assertExtension();

            makeContainer();

            extractMetaInfoOfVideo();

            assertVideoFile();

        } finally {

            closeContainer();

        }

    }


    private void assertExtension() {

        assertEquals(expectedFormat.getFileExtenstion()fileExtenstion());

    }


    private String fileExtenstion() {

        String filePath = videoFile.getAbsolutePath();

        int lastDotIdx = filePath.lastIndexOf(".");

        String extension = filePath.substring(lastDotIdx + 1);

        return extension;

    }


OutputFormat의 getFileExtension() 메서드가 파일 확장자를 제공한다고 했다. 이 메서드가 없으니 아래와 같이 추가해서 기존의 테스트가 통과되도록 하자.


public class OutputFormat {


    private int width;

    private int height;

    private int bitrate;

    ...

    public String getFileExtension() {

        return "mp4"; //일단 테스트가 통과하도록

    }


    public VideoCodec getVideoCodec() {

        return videoCodec;

    }


    public AudioCodec getAudioCodec() {

        return audioCodec;

    }


}


FfmpegTranscoderTest를 실행해 보자. 녹색! 통과다.


이제 본격적으로 컨테이너 정보를 추가해 보자. 파일의 확장자를 결졍짓는 것은 컨테이너이므로, 코덱과 별개로 컨테이너 정보를 추가하자.


public enum Container {


    MP4(VideoCodec.H264, AudioCodec.AAC, "mp4"),

    AVI(VideoCodec.MPEG4,AudioCodec.MP3, "avi");


    private VideoCodec defaultVideoCodec;

    private AudioCodec defaultAudioCodec;

    private String fileExtension;


    private Container(VideoCodec defaultVideoCodec,

            AudioCodec defaultAudioCodec, String fileExtenstion) {

        this.defaultVideoCodec = defaultVideoCodec;

        this.defaultAudioCodec = defaultAudioCodec;

        this.fileExtension = fileExtenstion;

    }


    public VideoCodec getDefaultVideoCodec() {

        return defaultVideoCodec;

    }


    public AudioCodec getDefaultAudioCodec() {

        return defaultAudioCodec;

    }


    public String getFileExtension() {

        return fileExtension;

    }


}


Container는 enum 타입으로 각 값은 해당 컨테이너의 기본 코덱 정보와 확장자 정보를 갖도록 했다.


Container를 추가했으므로, 변환 결과물 정보를 담는 OutputFormat에 추가할 차례이다. OutputFormat이 Container를 갖도록 필드를 추가혹, 생성자를 통해서 Container를 전달받도록 수정하자.


public class OutputFormat {


    private int width;

    private int height;

    private int bitrate;

    private Container container;

    private VideoCodec videoCodec;

    private AudioCodec audioCodec;


    public OutputFormat(int width, int height, int bitrate,

            Container container, VideoCodec videoCodec, AudioCodec audioCodec) {

        this.width = width;

        this.height = height;

        this.bitrate = bitrate;

        this.container = container;

        this.videoCodec = videoCodec;

        this.audioCodec = audioCodec;

    }

    ...

    public String getFileExtension() {

        return container.getFileExtension();

    }

    ...

}


OutputFormat에 getFileExtension() 메서드가 추가되었다. 이제 VideoFormatVerifier 테스트에 추가했던 확장자 확인 코드가 정상적으로 컴파일된다.


public class VideoFormatVerifier {

    ...

    ...

    private void assertExtension() {

        assertEquals(expectedFormat.getFileExtenstion(), fileExtenstion()); // 컴파일 됨

    }



위 코드는 정상적으로 컴파일되지만, OutputFormat 생성자에 새로운 파라미터가 추가되었기 때문에, OutputFormat 객체를 생성하는 테스트 코드에서는 컴파일 에러가 발생한다. 


public class VideoConverterTest {

    ...

    @Test

    public void transcode() {

        IMediaReader reader = ToolFactory.makeReader(SOURCE_FILE);


        OutputFormat outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                VideoCodec.H264, AudioCodec.AAC);

        VideoConverter writer = new VideoConverter(TRANSCODED_FILE, reader,

                outputFormat);

        ...

    }

}



public class FfmpegTranscoderTest {

    ...

    @Test

    public void transcodeWithOneOutputFormat() {

        File multimediaFile = new File("src/test/resources/sample.avi");

        List<OutputFormat> outputFormats = new ArrayList<OutputFormat>();

        outputFormats.add(new OutputFormat(160, 120, 150, VideoCodec.H264,

                AudioCodec.AAC));

        List<File> transcodedFiles = transcoder.transcode(multimediaFile,

                outputFormats);

        ...

    }

}


컴파일 에러가 나지 않도록 OutputFormat 생성자에 Container를 값으로 전달해 준다.


public class VideoConverterTest {


    @Test

    public void transcode() {

        IMediaReader reader = ToolFactory.makeReader(SOURCE_FILE);


        OutputFormat outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                Container.MP4, VideoCodec.H264, AudioCodec.AAC);

        VideoConverter writer = new VideoConverter(TRANSCODED_FILE, reader,

                outputFormat);

        ...

    }

}



public class FfmpegTranscoderTest {

    ...

    @Test

    public void transcodeWithOneOutputFormat() {

        File multimediaFile = new File("src/test/resources/sample.avi");

        List<OutputFormat> outputFormats = new ArrayList<OutputFormat>();

        outputFormats.add(new OutputFormat(160, 120, 150, Container.MP4

       VideoCodec.H264, AudioCodec.AAC));

        ...

    }

}


위와 같이 변경했으면, 두 테스트가 정상적으로 동작하는지 다시 테스트 해 본다. 녹색! 통과다.


AVI 변환 추가


이제 AVI 파일로 변환해주는 기능을 추가해 넣고 테스트를 해 보자. 이를 위해 FfmpegTranscoderTest를 다음과 같이 작성하였다. (음,, 바로 이전 코드하고 좀 많이 변경되었는데, 하나 하나 설명하지 않고 한 번에 넘어간 점 양해 바란다. 중복된 부분을 별도 메서드로 분리하고 setup에서 초기화를 진행하도록 수정했다.)


public class FfmpegTranscoderTest {


    private Transcoder transcoder;

    private File multimediaFile;

    private List<OutputFormat> outputFormats;


    private OutputFormat mp4Format;

    private OutputFormat aviFormat;


    @Before

    public void setup() {

        outputFormats = new ArrayList<OutputFormat>();

        mp4Format = new OutputFormat(160, 120, 150, Container.MP4,

                VideoCodec.H264, AudioCodec.AAC);

        aviFormat = new OutputFormat(160, 120, 150, Container.AVI,

                VideoCodec.MPEG4, AudioCodec.MP3);

        multimediaFile = new File("src/test/resources/sample.avi");


        transcoder = new FfmpegTranscoder(namingRule);

    }


    @Test

    public void transcodeWithOneMp4OutputFormat() {

        outputFormats.add(mp4Format);

        executeTranscoderAndAssert();

    }


    private void executeTranscoderAndAssert() {

        List<File> transcodedFiles = transcoder.transcode(multimediaFile,

                outputFormats);

        assertEquals(1, transcodedFiles.size());

        assertTrue(transcodedFiles.get(0).exists());

        VideoFormatVerifier.verifyVideoFormat(outputFormats.get(0),

                transcodedFiles.get(0));

    }


    @Test

    public void transcodeWithOneAviOutputFormat() {

        outputFormats.add(aviFormat);

        executeTranscoderAndAssert();

    }

}


AVI 변환에 대한 테스트 코드를 넣었다. 테스트 실행! 실패다. 실패 이유는 다음과 같다.


실패 메시지:

org.junit.ComparisonFailure: expected:<[avi]> but was:<[mp4]>

    at org.junit.Assert.assertEquals(Assert.java:123)

    at org.junit.Assert.assertEquals(Assert.java:145)

    at org.chimi.s4t.infra.ffmpeg.VideoFormatVerifier.assertExtension(VideoFormatVerifier.java:47)

    at org.chimi.s4t.infra.ffmpeg.VideoFormatVerifier.verify(VideoFormatVerifier.java:37)

    ...


실재 난 테스트 메서드:

    @Test

    public void transcodeWithOneAviOutputFormat() {

        outputFormats.add(aviFormat);

        executeTranscoderAndAssert();

    }

}


파일명이 AVI이길 기대했는데 실제 파일 이름은 MP4 여서 테스트를 통과하지 못했다. 이제 테스트를 통과시켜 보자. FfmpegTranscoder가 OutputFormat을 이용해서 파일의 확장자를 결정하도록 수정했다.


public class FfmpegTranscoder implements Transcoder {


    @Override

    public List<File> transcode(File multimediaFile,

            List<OutputFormat> outputFormats) {

        List<File> results = new ArrayList<File>();

        for (OutputFormat format : outputFormats) {

            results.add(transcode(multimediaFile, format));

        }

        return results;

    }


    private File transcode(File sourceFile, OutputFormat format) {

        IMediaReader reader = ToolFactory.makeReader(sourceFile

                .getAbsolutePath());


        String outputFile = getFileName(format);

        VideoConverter converter = new VideoConverter(outputFile, reader,

                format);

        reader.addListener(converter);

        while (reader.readPacket() == null)

            do {

            } while (false);

        return new File(outputFile);

    }


    private String getFileName(OutputFormat format) {

        return "outputFile." + format.getFileExtension(); // 기존 "outputFile.mp4"

    }


}


테스트를 실행해보자. 녹색! 통과다!


지금까지 한 번에 1개의 변환을 처리하는 부분에 집중했는데, 다음 연습 8에서는 여러 형식으로 변환하는 기능을 추가해 보도록 하자.








Posted by 최범균 madvirus

댓글을 달아 주세요

구현 진도를 확 빼고 싶지만, 글을 남기면서 진행하다보면 코드가 더디게 나간다. 답답함이 좀 있지만 몇 가지 영역에 대해서는 글로 남기는 게 좋을 것 같다.


Ffmpeg을 이용한 Transcoder 구현체 만들기! 테스트부터 작성


지금까지 TranscodingService 부터 시작해서 Job까지 점진적으로 구현을 만들어왔다. 다음 작업 대상은 Job 객체에서 호출할 Transcoder의 구현체이다. ffmpeg를 사용할 것이므로 구현체의 이름을 FfmpegTranscoder로 정리해 봤다. 구현의 시작은 테스트부터이다. 테스트부터 만들어보자.


public class FfmpegTranscoderTest {


    private Transcoder transcoder;


    @Before

    public void setup() {

        transcoder = new FfmpegTranscoder();

    }


    @Test

    public void transcodeWithOnfOutputFormat() {

        File multimediaFile = new File(".");

        List<OutputFormat> outputFormats = new ArrayList<OutputFormat>();

        outputFormats.add(new OutputFormat(640, 480, 300, "h264", "aac"));

        List<File> transcodedFiles = transcoder.transcode(multimediaFile,

                outputFormats);

        assertEquals(1, transcodedFiles.size());

        assertTrue(transcodedFiles.get(0).exists());

    }

}


위 테스트는 한 개의 비디오 파일에 대해 한 개의 변환 포맷을 입력으로 지정하고, 변환 결과로 생성된 파일이 존재하는 지 확인한다. 마음 같아서 생성된 파일을 분석해서 그 파일이 지정한 형식으로 올바르게 변환되었는지 확인하고 싶지만, 지금은 일단 위 수준에서 넘어가보자. (이 부분은 나중에 한 번 시도해 볼 것이다.)


FFmpegTranscoder 클래스가 없으니 컴파일 에러가 발생한다. 일단 이 클래스를 만들어서 컴파일 에러부터 없애자. 


다음으로 할 작업은 테스트를 통과시키는 것이다. 테스트 통과는 어렵지 않다. 빠르게 테스트를 통과시키기 위해 아래와 같이 코드를 작성한다.


public class FfmpegTranscoder implements Transcoder {


    @Override

    public List<File> transcode(File multimediaFile,

            List<OutputFormat> outputFormats) {

        List<File> results = new ArrayList<File>();

        for (OutputFormat format : outputFormats) {

            results.add(new File("."));

        }

        return results;

    }


}


이제 FfmpegTranscoderTest를 실행하자. 녹색, 통과다.


다음으로 할 작업은 테스트 코드를 좀 더 의미있게 변경하는 것이다. 지금은 단순히 파일이 존재하는지 여부로 검증하고 있는데, 더 정확하게 검증하려면 생성된 파일이 지정한 비디오 포맷과 크기를 갖는지의 여부로 검증해야 한다.


사용법을 익히기 위한 테스트 코드


생성된 비디오 결과물을 확인하기 위한 용도로 xuggler를 사용해 보기로 했다. xuggler는 ffmpeg을 자바에서 사용할 수 있도록 래핑해 놓은 라이브러리이다. 관련 네이티브 모듈을 모두 포함하고 있어서 jar 파일이 다소 크지만, ffmpeg를 로컬에 설치하고 네이티브를 만들거나 별도 프로세스로 호출해서는 사용하는 것 보다 편리하다가 판단되어 xuggler를 사용하기로 결정했다.


이 프로젝트는 Maven을 사용하고 있는데, 다음과 같은 설정으로 xuggler를 사용할 수 있다.


    <repositories>

        <repository>

            <id>xuggle repo</id>

            <url>http://xuggle.googlecode.com/svn/trunk/repo/share/java/</url>

        </repository>

    </repositories>


    <dependencies>

        <!-- ffmpeg -->

        <dependency>

            <groupId>xuggle</groupId>

            <artifactId>xuggle-xuggler</artifactId>

            <version>5.3</version>

        </dependency>

        ...


Xuggler를 클래스패스에 추가했으므로, xuggler의 이용방법을 익힐 차례이다. 이를 위해 Xuggler 관련 샘플 코드를 좀 검색한 뒤에 그 코드를 복사해서 다음과 같은 테스트를 만들었다.


public class XugglerTest {


    @Test

    public void getMetadataOfExistingAVIFile() {

        IContainer container = IContainer.make();

        int openResult = container.open("src/test/resources/sample.avi",

                IContainer.Type.READ, null);

        if (openResult < 0) {

            throw new RuntimeException("Xuggler file open failed: "

                    + openResult);

        }

        int numStreams = container.getNumStreams();

        System.out.printf("file \"%s\": %d stream%s; ",

                "src/test/resources/sample.avi", numStreams,

                numStreams == 1 ? "" : "s");

        System.out.printf("bit rate: %d; ", container.getBitRate());

        System.out.printf("\n");


        for (int i = 0; i < numStreams; i++) {

            IStream stream = container.getStream(i);

            IStreamCoder coder = stream.getStreamCoder();


            System.out.printf("stream %d: ", i);

            System.out.printf("type: %s; ", coder.getCodecType());

            System.out.printf("codec: %s; ", coder.getCodecID());

            System.out.printf("duration: %s; ",

                    stream.getDuration() == Global.NO_PTS ? "unknown" : ""

                            + stream.getDuration());

            System.out.printf("start time: %s; ",

                    container.getStartTime() == Global.NO_PTS ? "unknown" : ""

                            + stream.getStartTime());

            System.out.printf("timebase: %d/%d; ", stream.getTimeBase()

                    .getNumerator(), stream.getTimeBase().getDenominator());

            System.out.printf("coder tb: %d/%d; ", coder.getTimeBase()

                    .getNumerator(), coder.getTimeBase().getDenominator());


            if (coder.getCodecType() == ICodec.Type.CODEC_TYPE_AUDIO) {

                System.out.printf("sample rate: %d; ", coder.getSampleRate());

                System.out.printf("channels: %d; ", coder.getChannels());

                System.out.printf("format: %s", coder.getSampleFormat());

            } else if (coder.getCodecType() == ICodec.Type.CODEC_TYPE_VIDEO) {

                System.out.printf("width: %d; ", coder.getWidth());

                System.out.printf("height: %d; ", coder.getHeight());

                System.out.printf("format: %s; ", coder.getPixelType());

                System.out.printf("frame-rate: %5.2f; ", coder.getFrameRate()

                        .getDouble());

            }

            System.out.printf("\n");

        }

        container.close();

    }

}


위 코드를 이용해서 테스트를 돌려보고 어떤 결과값들이 출력되는지 확인한다. 몇 차례 코드를 수정하면서 테스트를 더 해 보면서 Xuggler를 이용해서 정보를 읽어오는 방법을 숙지해 나간다.


다시 FfmpegTranscoderTest로, 결과값 검증하는 코드 만들기


앞의 FfmpegTranscoderTest 코드에 변환 결과로 생성된 비디오 파일을 검증하는 코드를 추가해 보자.


public class FfmpegTranscoderTest {

    ...

    @Test

    public void transcodeWithOnfOutputFormat() {

        File multimediaFile = new File("src/test/resources/sample.avi");

        List<OutputFormat> outputFormats = new ArrayList<OutputFormat>();

        outputFormats.add(new OutputFormat(640, 480, 300, "h264", "aac"));

        List<File> transcodedFiles = transcoder.transcode(multimediaFile,

                outputFormats);

        assertEquals(1, transcodedFiles.size());

        assertTrue(transcodedFiles.get(0).exists());

        verifyTranscodedFile(outputFormats.get(0), transcodedFiles.get(0));

    }


    private void verifyTranscodedFile(OutputFormat outputFormat, File file) {

        IContainer container = IContainer.make();

        int openResult = container.open(file.getAbsolutePath(),

                IContainer.Type.READ, null);

        if (openResult < 0) {

            throw new RuntimeException("Xuggler file open failed: "

                    + openResult);

        }

        int numStreams = container.getNumStreams();


        int width = 0;

        int height = 0;

        ICodec.ID videoCodec = null;

        ICodec.ID audioCodec = null;


        for (int i = 0; i < numStreams; i++) {

            IStream stream = container.getStream(i);

            IStreamCoder coder = stream.getStreamCoder();


            if (coder.getCodecType() == ICodec.Type.CODEC_TYPE_AUDIO) {

                audioCodec = coder.getCodecID();

            } else if (coder.getCodecType() == ICodec.Type.CODEC_TYPE_VIDEO) {

                videoCodec = coder.getCodecID();

                width = coder.getWidth();

                height = coder.getHeight();

            }

        }

        container.close();


        assertEquals(outputFormat.getWidth(), width);

        assertEquals(outputFormat.getHeight(), height);

        assertEquals(outputFormat.getVideoFormat(), videoCodec.toString()); // ???

        assertEquals(outputFormat.getAudioFormat(), audioCodec.toString()); // ???

    }

}


verifyTranscodedFile() 메서드는 Xuggler를 이용해서 지정한 비디오 파일의 메타 정보가 OutputFormat에 지정된 값과 일치하는 지 여부를 검증한다. 테스트 코드의 마지막에서는 코덱을 비교하는 부분이 있는데, 다음의 두 가지가 걸린다.
  • OutputFormat의 메서드 이름이 getVideoFormat()/getAudioFormat()이다. 이건 getVideoCodec()과 같이 코덱의 의미로 바꿔줘야 할 것 같다.
  • OutputFormat은 String으로 값을 갖고 있는 반면에 비교하려는 값은 ICodec.ID 열거 타입이다. 이 둘 간에 변환 방법이 필요할 것 같다.
위 두 가지 중에서 첫 번째 것은 비교적 쉽다. 일단 먼저 처리하자. getVideoFormat과 getAudioFormat 이름을  getVideoCodec, getAutidoCodec 으로 변경한다.

두 번째는 Xuggler의 코덱 정보 표현 방법과 OutputFormat의 코덱 정보 표현 방법의 불일치로부터 발생하는 것이다. 이 부분은 뒤에서 다시 논의해 보자.

테스트 코드를 만들었으니 테스트를 실행한다. 빨간색. 통과에 실패했다. 실패를 발생시킨 코드는 아래와 같다.

// FfmpegTranscoderTest.java
    private void verifyTranscodedFile(OutputFormat outputFormat, File file) {
        IContainer container = IContainer.make();
        int openResult = container.open(file.getAbsolutePath(),
                IContainer.Type.READ, null);
        if (openResult < 0) { // 파일이 없으므로, 익셉션 발생됨
            throw new RuntimeException("Xuggler file open failed: "
                    + openResult);
        }


// FfmpegTranscoder.java
public class FfmpegTranscoder implements Transcoder {

    @Override
    public List<File> transcode(File multimediaFile,
            List<OutputFormat> outputFormats) {
        List<File> results = new ArrayList<File>();
        for (OutputFormat format : outputFormats) {
            results.add(new File(".")); // 디렉터리 정보임
        }
        return results;
    }

}

실패가 발생된 이유는 검증을 하기 위해 생성된 파일을 여는 과정에서 파일이 없어 발생한 것이다. 그 에러를 통과하려면 FfmpegTranscoder#transcode() 메서드에서 실제 파일을 리턴해 주어야 한다. 뿐만 아니라 폭, 높이 등에 대한 검증을 통과하려면 실제 비디오 파일을 생성해서 리턴해 주어야 한다.

아,, 비디오 변환 기능이 필요한 시점이 왔다. 음,,, 다시 Xuggler 테스트로 돌아가서 비디오 파일 변환 기능을 테스트 해 보자.

테스트 코드에서 Xuggler로 비디오 포맷 변환 기능 실험하기

Xuggler로 비디오 포맷을 변환하는 작업은 간단하다. 아래는 최초 테스트 코드이다.

public class XugglerTest {

    @Test
    public void transcode() {
        IMediaReader reader = ToolFactory
                .makeReader("src/test/resources/sample.avi");

        IMediaWriter writer = ToolFactory.makeWriter("target/sample.mp4",
                reader);
        reader.addListener(writer);
        while (reader.readPacket() == null)
            do {
            } while (false);
    }

아주 간단하게 비디오 변환을 할 수 있기에, 처음에는 '우~와' 했다. 그런데, 우리는 동영상의 크기도 변경시켜야 하고 비트레이트도 변경시킬 수 있어야 하고, MP4 컨테이너 정보 외에 인코딩할 때 사용할 비디오와 오디오의 코덱을 지정할 수 있어야 한다. 앗, 잠깐 컨테이너? 이런, 컨테이너를 설정하는 정보가 없다. 음, 일단 컨테이너에 대한 정보는 추후에 추가하자.

Xuggler가 다행히 오픈소스다. Xuggler의 소스 코드들을 탐색하고 테스트를 진행하면서, Xuggler를 이용한 VideoConverter를 완성했다. 더불어 VideoConverter를 만들어나가는 과정에서 다음을 함께 진행했다.

  • 테스트를 위한 VideoConverterTest 작성
  • OutputFormat에서 코덱 정보를 표현하기 위해 VideoCodec과 AudioCodec 타입이 도메인 모델에 추가
VideoConverterTest를 작성하면서 생성된 비디오의 내용물이 올바른지 확인하는 기능이 필요하다. 앞서 살펴봤던 FfmpegTranscoderTest의 verifyTranscodedFile() 메서드와 동일하다. 그러니, 이 메서드를 별도 헬퍼 클래스로 빼서 두 개의 테스트 코드에서 함께 사용할 수 있도록 하자.

헬러 클래스를 다음과 같이 작성해 보았다. 

public class VideoFormatVerifier {

    public static void verifyVideoFormat(OutputFormat expectedFormat,
            File videoFile) {
        new VideoFormatVerifier(expectedFormat, videoFile).verify();
    }

    private IContainer container;
    private int width;
    private int height;
    private ICodec.ID videoCodec;
    private ICodec.ID audioCodec;

    private OutputFormat expectedFormat;
    private File videoFile;

    public VideoFormatVerifier(OutputFormat expectedFormat, File videoFile) {
        this.expectedFormat = expectedFormat;
        this.videoFile = videoFile;
    }

    public void verify() {
        try {
            makeContainer();
            extractMetaInfoOfVideo();
            assertVideoFile();
        } finally {
            closeContainer();
        }
    }

    private void makeContainer() {
        container = IContainer.make();
        int openResult = container.open(videoFile.getAbsolutePath(),
                IContainer.Type.READ, null);
        if (openResult < 0) {
            throw new RuntimeException("Xuggler file open failed: "
                    + openResult);
        }
    }

    private void extractMetaInfoOfVideo() {
        int numStreams = container.getNumStreams();
        for (int i = 0; i < numStreams; i++) {
            IStream stream = container.getStream(i);
            IStreamCoder coder = stream.getStreamCoder();
            if (coder.getCodecType() == ICodec.Type.CODEC_TYPE_AUDIO) {
                audioCodec = coder.getCodecID();
            } else if (coder.getCodecType() == ICodec.Type.CODEC_TYPE_VIDEO) {
                videoCodec = coder.getCodecID();
                width = coder.getWidth();
                height = coder.getHeight();
            }
        }
    }

    private void closeContainer() {
        if (container != null)
            container.close();
    }

    private void assertVideoFile() {
        assertEquals(expectedFormat.getWidth(), width);
        assertEquals(expectedFormat.getHeight(), height);
        assertEquals(expectedFormat.getVideoCodec(),
                CodecValueConverter.toDomainVideoCodec(videoCodec));
        assertEquals(expectedFormat.getAudioCodec(),
                CodecValueConverter.toDomainAudioCodec(audioCodec));
    }

}

VideoConverterTest 클래스는 다음과 같이 VideoFormatVerifier를 이용해서 변환 결과를 검증하도록 했다.

public class VideoConverterTest {

    @Test
    public void transcode() {
        IMediaReader reader = ToolFactory
                .makeReader("src/test/resources/sample.avi");

        OutputFormat outputFormat = new OutputFormat(160, 120, 150,
                VideoCodec.H264, AudioCodec.AAC);
        VideoConverter writer = new VideoConverter("target/sample.mp4", reader,
                outputFormat);
        reader.addListener(writer);
        while (reader.readPacket() == null)
            do {
            } while (false);

        VideoFormatVerifier.verifyVideoFormat(outputFormat, new File("target/sample.mp4"));
    }
}

위 테스트가 통과할 때 까지 VideoConverter를 지속적으로 수정해 나갔고, 최종적으로 녹색이 들어왔다.

테스트 코드의 몇 가지 값들을 상수로 바꾸면서 의미를 명확하게 바꾸자.

public class VideoConverterTest {

    private static final int WIDTH = 160;
    private static final int HEIGHT = 120;
    private static final int BITRATE = 150;
    private static final String SOURCE_FILE = "src/test/resources/sample.avi";
    private static final String TRANSCODED_FILE = "target/sample.mp4";

    @Test
    public void transcode() {
        IMediaReader reader = ToolFactory.makeReader(SOURCE_FILE);

        OutputFormat outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,
                VideoCodec.H264, AudioCodec.AAC);
        VideoConverter writer = new VideoConverter(TRANSCODED_FILE, reader,
                outputFormat);
        reader.addListener(writer);
        while (reader.readPacket() == null)
            do {
            } while (false);

        VideoFormatVerifier.verifyVideoFormat(outputFormat, new File(TRANSCODED_FILE));
    }
}

다시 테스트. 녹색! 통과다.

FfmpegTranscoderTest로 돌아와 FfmpegTranscoder 구현하기

앞에서 작성한 VideoConverterTest 클래스의 transcode() 메서드는 FfmpegTranscoder#transcode() 메서드에서 비디오를 변환하기 위한 코드로 그대로 사용된다. 알맞게 FfmpegTranscoder#transcode()에 넣어보자.

public class FfmpegTranscoder implements Transcoder {

    @Override
    public List<File> transcode(File multimediaFile,
            List<OutputFormat> outputFormats) {
        List<File> results = new ArrayList<File>();
        for (OutputFormat format : outputFormats) {
            results.add(transcode(multimediaFile, format)); // 기존 new File(".")로 처리
        }
        return results;
    }

    private File transcode(File sourceFile, OutputFormat format) {
        IMediaReader reader = ToolFactory.makeReader(sourceFile
                .getAbsolutePath());

        String outputFile = "outputFile.mp4"; // 음, 일단 테스트 통과 위한 코딩
        VideoConverter converter = new VideoConverter(outputFile, reader,
                format);
        reader.addListener(converter);
        while (reader.readPacket() == null)
            do {
            } while (false);
        return new File(outputFile);
    }

}

FfmpegTranscoder 클래스를 작성했다. 음, 일단 테스트를 통과시키기 위해 결과 파일 이름을 지정했다. 마음에 안 드는 부분이다.

FfmpegTranscoder 클래스가 실제로 비디오 파일 변환을 처리하도록 구현했으니, 테스트를 실행해 보자. FfmpegTranscoderTest 클래스는 앞서 작성했던 VideoFormatVerifier를 이용해서 변환된 결과를 확인하도록 했다.

public class FfmpegTranscoderTest {

    private Transcoder transcoder;

    @Before
    public void setup() {
        transcoder = new FfmpegTranscoder();
    }

    @Test
    public void transcodeWithOnfOutputFormat() {
        File multimediaFile = new File("src/test/resources/sample.avi");
        List<OutputFormat> outputFormats = new ArrayList<OutputFormat>();
        outputFormats.add(new OutputFormat(160, 120, 150, VideoCodec.H264,
                AudioCodec.AAC));
        List<File> transcodedFiles = transcoder.transcode(multimediaFile,
                outputFormats);
        assertEquals(1, transcodedFiles.size());
        assertTrue(transcodedFiles.get(0).exists());
        VideoFormatVerifier.verifyVideoFormat(outputFormats.get(0),
                transcodedFiles.get(0));
    }
}

테스트 실행... 녹색이다. 야호!

다음에 할 것
  • 변환 결과로 생성되는 파일의 이름을 만들 정책이 필요하다.
  • MP4, AVI 등 컨테이너에 대한 정보 추가가 필요하다.
FfmpegTranscoder 클래스는 변환 내용에 상관없이 항상 outputFile.mp4라는 파일을 생성한다. AVI나 MP4, WMV 등 컨테이너에 종류를 지정하는 방법이 필요할 것 같고, 컨테이너 종류에 따라 생성되는 파일의 확장자가 변경되도록 하는 기능도 필요할 것 같다.

다음 TDD 연습 7에서는 이 두가지를 진행해보자.


Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 백명석 2012.11.05 14:33 신고  댓글주소  수정/삭제  댓글쓰기

    "사용법을 익히기 위한 테스트 코드"
    이런 걸 study test라고 한다고 함. - 토비의 스프링3에서.
    사용법을 익히는 데는 실제로 실행해 보는 것이 가장 좋은 방법.
    자세한 사용법은 필요한 경우 찾아서 보게됨. 처음부터 한글자도 안 빼고 읽어보는 개발자는 없을 듯.
    이런 테스트도 사용법을 익혔고 별로 의미 없을 것 같다고 지운면 안됨.
    후에 다른 개발자가 동일한 라이브러리 사용법을 배우거나 업그레이드로 인한 문제점 파악에 도움이 됨.

TDD 연습은 오늘도 계속된다. 지난 금요일 점심 때 살짝 작업해 본 코드를 GitHub에 커밋하는 걸 깜빡해서 주말에 진행하지 못했다. 음, 그덕에 오늘 밤에 진행한다.


Job에 새로운 모델 추가


오늘은 Job에 모델을 추가해볼 거다. 비디오 변환을 처리하면 변환된 결과 비디오 파일과 썸네일 파일이 생성되는데, 그 파일을 작업 요청자가 지정한 위치에 저장해야 한다. 이 위치는 FTP가 될 수도 있고, HTTP 파일 업로드가 될 수도 있고, 단순히 로컬 파일 시스템이 될 수도 있다. 자, 일단, 구현의 상세함은 뒤로 미루고 다음과 같이 결과 파일이 보관될 위치를 추상화해서 표현할 수 있을 것이다. 


public class Job {


    private Long id;

    private MediaSourceFile mediaSourceFile;

    private DestinationStorage destinationStorage;

    private State state;

    private Exception occurredException;


    public Job(Long id, MediaSourceFile mediaSourceFile,

            DestinationStorage destinationStorage) {

        this.id = id;

        this.mediaSourceFile = mediaSourceFile;

        this.destinationStorage = destinationStorage;

    }


뭔가 이름이 적당한 게 떠오르지 않는다. (이럴 땐 영어공부를 게을리한 필자가 안타깝다.) 하튼, 이름은 DestinationStorage로 가기로 했다. 나중에 더 좋은 이름이 떠오르면 이클립스의 힘을 빌려 쉽게 바꿀 수 있다.


새로운 타입이 출현했다. 아무것도 없으므로 컴파일 에러가 난다. DestinationStorage 타입을 생성해주자. 그리고, Job 클래스의 생성자가 변경되었으므로 테스트 코드에서 Mock을 이용해서 DestinationStorage를 전달해준다.


@RunWith(MockitoJUnitRunner.class)

public class TranscodingServiceImplTest {


    private Long jobId = new Long(1);

    @Mock

    private MediaSourceFile mediaSourceFile;

    @Mock

    private DestinationStorage destinationStorage;


    private Job mockJob;

    ...

    

    @Before

    public void setup() {

        mockJob = new Job(jobId, mediaSourceFile, destinationStorage);

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


        transcodingService = new TranscodingServiceImpl(transcoder,

                thumbnailExtractor, createdFileSender, jobResultNotifier,

                jobRepository);


바꿨으니 당연히 뭘 해야 하는지 알 거다. 테스트를 실행해보자. 기존 코드에 문제가 없음을 확인한다.


앞서 Job에 MediaSourceFile을 추가했던 과정을 생각해보자. (기억이 안 나면 http://javacan.tistory.com/221 글을 다시 보자.) 그 과정과 마찬가지로 다음과 같은 작업을 진행할 것이다.

  1. 목적지를 알고 있는 것은 DestinationStorage이므로, Job이 DestinationStorage에 파일을 저장해 달라고 요청하도록 코드를 수정한다.
  2. 이렇게 되면 CreatedFileSaver가 필요없지므로 삭제한다.
자 일단 Job이 DestinationStorage에게 파일을 저장해달라고 요청하도록 코드를 수정한다.

public class Job {

    private Long id;
    private MediaSourceFile mediaSourceFile;
    private DestinationStorage destinationStorage;
    ...
    public void transcode(Transcoder transcoder,
            ThumbnailExtractor thumbnailExtractor,
            CreatedFileSaver createdFileSaver,
            JobResultNotifier jobResultNotifier) {
        try {
            ...
            changeState(Job.State.STORING);
            storeCreatedFilesToStorage(multimediaFiles, thumbnails,
                    createdFileSaver);
            changeState(Job.State.NOTIFYING);
            notifyJobResultToRequester(jobResultNotifier);
            changeState(Job.State.COMPLETED);
        } catch (RuntimeException ex) {
            exceptionOccurred(ex);
            throw ex;
        }
    }

    private void storeCreatedFilesToStorage(List<File> multimediaFiles,
            List<File> thumbnails, CreatedFileSaver createdFileSaver) {
        destinationStorage.save(multimediaFiles, thumbnails);
        createdFileSaver.store(multimediaFiles, thumbnails, id);
    }

createdFileSaver를 호출하는 코드는 필요없으므로 삭제한다. destinationStorage는 save 메서드가 없어서 컴파일 에러가 난다. 컴파일 에러가 나지 않도록 save() 메서드를 추가해 준다. 이클립스의 만능키 Ctrl+1을 누루면 바로 추가되므로 쉽게 컴파일 에러를 제거할 수 있다.


public interface DestinationStorage {


    void save(List<File> multimediaFiles, List<File> thumbnails);


}


오케이 일단 뭔가 바꿨다. 테스트 코드를 돌려보자. .... 빨간불. 통과에 실패했다. 원인은 아래와 같다.


Wanted but not invoked:

createdFileSender.store([], [], 1);

-> at org...TranscodingServiceImplTest.verifyCollaboration(TranscodingServiceImplTest.java:117)

at org...TranscodingServiceImplTest.verifyCollaboration(TranscodingServiceImplTest.java:117)

...


실패 원인은 createdFileSender.store()가 호출되지 않은 것 때문이다. 앞서 Job 클래스의 storeCreatedFilesToStorage() 메서드에서 createFileSaver 객체의 store() 메서드를 호출하는 부분을 삭제했기 때문에 발생한 것이다. 이제 이 부분은 createdFileSender가 아니라 destinationStorage.save() 메서드가 호출되었는지 검증하는 코드로 바꿔야 한다. 그래서, 아래와 같이 테스트 코드에서 협업 객체들의 메서드 호출 여부를 검증하는 부분을 변경하였다.


    private void verifyCollaboration(VerifyOption verifyOption) {

        if (verifyOption.transcoderNever)

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

        else

            verify(transcoder, only()).transcode(mockMultimediaFile, jobId);


        if (verifyOption.thumbnailExtractorNever)

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

                    anyLong());

        else

            verify(thumbnailExtractor, only()).extract(mockMultimediaFile,

                    jobId);


        if (verifyOption.destinationStorageNever) // createdFileSaverNever를 이름 변경

            verify(destinationStorage, never()).save(anyListOf(File.class),

                    anyListOf(File.class));

        else

            verify(destinationStorage, only()).save(mockMultimediaFiles,

                    mockThumbnails);


        if (verifyOption.createdFileSenderNever)

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

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

        else

            verify(createdFileSender, only()).store(mockMultimediaFiles,

                    mockThumbnails, jobId);


        if (verifyOption.jobResultNotifierNever)

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

        else

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

    }


destinationStorage Mock 객체의 save() 메서드가 호출되는 지의 여부로 변경했으므로,  다시 테스트를 실행해 보자. 6개의 테스트 메서드 중에서 1개가 실패가 난다. 실패한 테스트는 아래 메서드이다.


    @Test

    public void transcodeFailBecauseExceptionOccuredAtCreatedFileSender() {

        doThrow(mockException).when(createdFileSender).store(

                mockMultimediaFiles, mockThumbnails, jobId);


        executeFailingTranscodeAndAssertFail(Job.State.STORING);


        VerifyOption verifyOption = new VerifyOption();

        verifyOption.jobResultNotifierNever = true;


        verifyCollaboration(verifyOption);

    }


    private void executeFailingTranscodeAndAssertFail(State expectedLastState) {

        try {

            transcodingService.transcode(jobId);

            fail("발생해야 함"); // 익셉션이 발생하지 않아서 실패

        } catch (Exception ex) {

            assertSame(mockException, ex);

        }

        ...

    }



검증하는 코드에서는 익셉션이 발생해야 통과되는데, Job 객체를 수정하는 바람에 더 이상 createdFileSaver 객체가 호출되지 않게 되었다. 위 코드는 createdFileSaver 대신 새롭게 추가된 모델인 destinationStorage의 save() 메서드에서 익셉션을 발생하도록 수정해주면 통과 시킬 수 있을 것 같다. 메서드 이름도 함께 변경해 주자.


    @Test

    public void transcodeFailBecauseExceptionOccuredAtDestinationStorage() {

        doThrow(mockException).when(destinationStorage).save(

                mockMultimediaFiles, mockThumbnails);


        executeFailingTranscodeAndAssertFail(Job.State.STORING);


        VerifyOption verifyOption = new VerifyOption();

        verifyOption.jobResultNotifierNever = true;


        verifyCollaboration(verifyOption);

    }


다시 테스트 코드 실행. 모두 녹색. 통과다.


이제 필요 없어진 코드가 있다. 바로 CreatedFileSaver 이다. 이제 이 타입을 사용하는 코드를 모두 과감하게 삭제한다. (주석으로 막아서 테스트를 실행하고 통과되면 지우는 방법을 점진적으로 삭제한다.)


Job 클래스의 작은 리팩토링


필자와 개인적으로 친분이 있는 분께서 http://javacan.tistory.com/221#comment9371876와 같은 의견을 주셨다. 그 중 첫 번째 Job 클래스 부분에 대한 피드백은 아래 코드에서 구조적인 반복이 나타난다는 점이었다. 상태를 변경하고 작업 실행하고, 또 상태를 변경하고 작업을 실행하는 구조를 갖고 있다.


    public void transcode(Transcoder transcoder,

            ThumbnailExtractor thumbnailExtractor,

            JobResultNotifier jobResultNotifier) {

        try {

            changeState(Job.State.MEDIASOURCECOPYING);

            File multimediaFile = copyMultimediaSourceToLocal();

            changeState(Job.State.TRANSCODING);

            List<File> multimediaFiles = transcode(multimediaFile, transcoder);

            changeState(Job.State.EXTRACTINGTHUMBNAIL);

            List<File> thumbnails = extractThumbnail(multimediaFile,

                    thumbnailExtractor);

            changeState(Job.State.STORING);

            storeCreatedFilesToStorage(multimediaFiles, thumbnails);

            changeState(Job.State.NOTIFYING);

            notifyJobResultToRequester(jobResultNotifier);

            changeState(Job.State.COMPLETED);

        } catch (RuntimeException ex) {

            exceptionOccurred(ex);

            throw ex;

        }

    }


우선, 상태 변경과 작업 실행 순서 관리를 transcode()가 처리하고 있는데, 이는 transcode()의 역할을 잘못 잡은 것 같다. 실제 상태의 변화는 각각의 메서드에서 처리하고 transcode() 메서드는 순서대로 작업이 실행되도록 하는 것이 적당한 역할을 수행하는 것으로 생각된다. 그래서, 상태 변경은 각 메서드에서 처리하도록 수정했다. 그리고 완료 상태로 변경하는 부분은 completed()라는 메서드를 만들어서 그 메서드에서 처리하도록 했다.


public class Job {

    ...

    public void transcode(Transcoder transcoder,

            ThumbnailExtractor thumbnailExtractor,

            JobResultNotifier jobResultNotifier) {

        try {

            File multimediaFile = copyMultimediaSourceToLocal();

            List<File> multimediaFiles = transcode(multimediaFile, transcoder);

            List<File> thumbnails = extractThumbnail(multimediaFile,

                    thumbnailExtractor);

            storeCreatedFilesToStorage(multimediaFiles, thumbnails);

            notifyJobResultToRequester(jobResultNotifier);

            completed();

        } catch (RuntimeException ex) {

            exceptionOccurred(ex);

            throw ex;

        }

    }


    private File copyMultimediaSourceToLocal() {

        changeState(Job.State.MEDIASOURCECOPYING);

        return mediaSourceFile.getSourceFile();

    }


    private List<File> transcode(File multimediaFile, Transcoder transcoder) {

        changeState(Job.State.TRANSCODING);

        return transcoder.transcode(multimediaFile, id);

    }


    private List<File> extractThumbnail(File multimediaFile,

            ThumbnailExtractor thumbnailExtractor) {

        changeState(Job.State.EXTRACTINGTHUMBNAIL);

        return thumbnailExtractor.extract(multimediaFile, id);

    }


    private void storeCreatedFilesToStorage(List<File> multimediaFiles,

            List<File> thumbnails) {

        changeState(Job.State.STORING);

        destinationStorage.save(multimediaFiles, thumbnails);

    }


    private void notifyJobResultToRequester(JobResultNotifier jobResultNotifier) {

        changeState(Job.State.NOTIFYING);

        jobResultNotifier.notifyToRequester(id);

    }


    private void completed() {

        changeState(Job.State.COMPLETED);

    }


}


이제 Job은 작업을 순서대로 실행하는 한 가지 일만을 책임지게 되었으므로 정리가 된 느낌이다. 하지만, 여전히 각각의 메서드들은 상태변경-작업실행 요청의 반복적 구조를 갖고 있다. 이런 구조를 제거할 수 있는 다른 방법이 있을 것도 같다. 예를 들어, 각 단계를 State 같은 걸로 추상화하고 State가 지정된 순서대로 전이되도록 StateChain 같은 걸 만들어서 지정한 순서대로 각 단계의 작업이 실행되도록 할 수 있을 것 같다. 하지만, 이런 구조를 갖는 것은 현재로서는 유연함에서 오는 이점보다 복잡함에서 오는 단점이 더 많은 것 같다.


그래서, 위의 구조적 반복이 여전히 남아 있긴 하지만, 일단 이쯤에서 정리한다. 혹시 Job의 하위 타입이 생긴다거나 새로운 상태가 생긴다거나 실행 순서의 변경이 발생하게 된다면, 그 때 가서 다시 리팩토링을 해 보도록 하자. (이 정도로 지인분의 요구가 충족될지는........ 이 정도로 만족하시지요!)


테스트의 VerifyOption에 기능 주기


지인분께서 http://javacan.tistory.com/221#comment9371876에 남기신 두 번째 피드백은 verifyOption이 검증과 관련된 정보를 갖고 있으니, VerifyOption을 단순 데이터 구조가 아닌 기능을 제공하는 객체로 만들어보자는 내용이다. 음, 이 부분은 고민이 좀 된다. 테스트 코드에서 사용되는 데이터인데 그것까지 객체로 만들 필요가 있을까 하는 고민이었다. 그래도 변경하는게 어렵지 않으니 바꿔보자.


뭔가 협업 객체가 올바르게 호출되는지 여부를 확인하는 코드는 아래와 같은 방식으로 사용된다.


    @Test

    public void transcodeFailBecauseExceptionOccuredAtTranscoder() {

        when(transcoder.transcode(mockMultimediaFile, jobId)).thenThrow(

                mockException);


        executeFailingTranscodeAndAssertFail(Job.State.TRANSCODING);


        VerifyOption verifyOption = new VerifyOption();

        verifyOption.thumbnailExtractorNever = true;

        verifyOption.destinationStorageNever = true;

        verifyOption.jobResultNotifierNever = true;


        verifyCollaboration(verifyOption);

    }


    private void verifyCollaboration(VerifyOption verifyOption) {

        if (verifyOption.transcoderNever)

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

        else

            verify(transcoder, only()).transcode(mockMultimediaFile, jobId);


        if (verifyOption.thumbnailExtractorNever)

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

                    anyLong());

        else

            verify(thumbnailExtractor, only()).extract(mockMultimediaFile,

                    jobId);


        if (verifyOption.destinationStorageNever)

            verify(destinationStorage, never()).save(anyListOf(File.class),

                    anyListOf(File.class));

        else

            verify(destinationStorage, only()).save(mockMultimediaFiles,

                    mockThumbnails);


        if (verifyOption.jobResultNotifierNever)

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

        else

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

    }


    public class VerifyOption {

        public boolean transcoderNever;

        public boolean thumbnailExtractorNever;

        public boolean destinationStorageNever;

        public boolean jobResultNotifierNever;

    }

}


우선, verifyCollaboration 메서드를 VerifyOption의 public 메서드로 이동시킨다. 물론, 옮기면서 this를 사용하도록 변경한다. 그리고, 각 테스트 메서드에서는 VerifyOption의 verifyCollaboration() 메서드를 호출하도록 변경한다. 그럼 코드는 아래와 같은 바뀐다.


    @Test

    public void transcodeFailBecauseExceptionOccuredAtDestinationStorage() {

        doThrow(mockException).when(destinationStorage).save(

                mockMultimediaFiles, mockThumbnails);


        executeFailingTranscodeAndAssertFail(Job.State.STORING);


        VerifyOption verifyOption = new VerifyOption();

        verifyOption.jobResultNotifierNever = true;


        verifyOption.verifyCollaboration();

    }


    public class VerifyOption {

        public boolean transcoderNever;

        public boolean thumbnailExtractorNever;

        public boolean destinationStorageNever;

        public boolean jobResultNotifierNever;


        public void verifyCollaboration() {

            if (this.transcoderNever)

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

                        anyLong());

            else

                verify(transcoder, only()).transcode(mockMultimediaFile, jobId);


            if (this.thumbnailExtractorNever)

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

                        anyLong());

            else

                verify(thumbnailExtractor, only()).extract(mockMultimediaFile,

                        jobId);


            if (this.destinationStorageNever)

                verify(destinationStorage, never()).save(anyListOf(File.class),

                        anyListOf(File.class));

            else

                verify(destinationStorage, only()).save(mockMultimediaFiles,

                        mockThumbnails);


            if (this.jobResultNotifierNever)

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

            else

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

        }

    }


바꿨으니 테스트 실행이다. 녹색! 통과되었다.


이름이 마음에 안 든다. 이름을 바꾸자. VerifyOption은 더 이상 단순 옵션이 아니다. 실제 검증을 수행하는 역할을 갖게 되었으므로, CollaborationVerifier와 같이 역할에 맞는 이름을 부여한다. verifyCollaboration() 메서드는 순히 verify()로 바꿔도 될 것 같다. 웃, verify()로 이름을 변경하려고 했더니 메서드 내부에서 사용한 Mockito의 verify()가 컴파일 에러가 난다. 음, 다른 이름을 찾아보자. 음.. 딱히 이름이 안 떠오른다. 일단 verifyCollaboration()으로 놔두자.


    @Test

    public void transcodeFailBecauseExceptionOccuredAtDestinationStorage() {

        doThrow(mockException).when(destinationStorage).save(

                mockMultimediaFiles, mockThumbnails);


        executeFailingTranscodeAndAssertFail(Job.State.STORING);


        CollaborationVerifier colVerifier = new CollaborationVerifier();

        colVerifier.jobResultNotifierNever = true;


        colVerifier.verifyCollaboration();

     }


    public class CollaborationVerifier {

        public boolean transcoderNever;

        public boolean thumbnailExtractorNever;

        public boolean destinationStorageNever;

        public boolean jobResultNotifierNever;


        public void verifyCollaboration() {

            if (this.transcoderNever)

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

                        anyLong());

            else

                verify(transcoder, only()).transcode(mockMultimediaFile, jobId);


            ...

        }


    }

}


이름을 바꿨으니 다시 테스트! 녹색! 통과되었다.


CollaborationVerifier와 관련된 다음 고민은 아래와 같다.

  • transcoderNever, thumbnailExtractorNever 등의 필드를 private으로 바꿔야 하나?
  • verifyCollaboration() 메서드에서 접근하는 transcoder 등의 객체를 필드로 정의해야 하나?
  • CollaborationVerifier 클래스를 private으로 바꿔야 하나?
음, 아주 짧게만 고민해 봤는데 (슬슬 졸리기 시작한다), 현재로서 CollaborationVerifier 클래스는 TranscodingServiceImplTest 내부에서만 사용되므로, 필드가 public이어도 상관없을 것 같고 TranscodingServiceImplTest 클래스의 필드에 직접 접근해도 문제가 없다. 일단 지금은 CollaborationVerifier를 private으로 하는 것만으로 충분할 것 같다.


Posted by 최범균 madvirus

댓글을 달아 주세요