본문 바로가기
우아한테크코스 프리코스

[우아한테크코스 6기] 프리코스 2차 과제: 레이싱 게임(JUnit5, System.setIn, System.setOut)

by shyun00 2023. 10. 31.

레이싱에 참여할 참여자의 명단과 레이싱 횟수를 입력받아 중간/최종 결과를 출력하는 게임을 구현해야 한다.

1. 구현할 기능 목록 작성

구현해야하는 기능은 아래와 같이 정리해보았다.

게임 진행 흐름에 따라 어떤 기능이 필요할지 생각해보고 혹시 해당 과정에서 생길 수 있는 에러 조건에 대해서도 생각해보았다.

2. 구조 설계

구현해야하는 기능에 따라 어떤 도메인이 필요할지, 어떤 메서드가 필요할지 먼저 고민해보았다.

자동차 게임이므로 참가자(Player)와 게임진행자(Manager)가 필요하다고 생각했다.

또한 MVC패턴을 고려해 입력/출력을 담당하는 클래스, 그리고 게임 전체를 총괄하는 Game 클래스를 구상했다.

3. 비즈니스 로직 코드 작성

이번에는 각 메서드가 최대한 하나의 기능을 하도록, depth가 2를 초과하지 않도록 하는것이 요구사항이었다.

필요한 기능별로 메서드 목록을 먼저 나열하고 구현하는 방식으로 코드 작성을 진행했다.

세부 비즈니스 로직은 아래 링크에 기술되어있다.

2차 과제 코드: (링크

* 부족한 부분이 많은 코드입니다. 코드 리뷰는 언제든지 환영입니다.^^

4. 테스트 코드 작성

이번 과제에서 가장 많은 시간을 투자한 부분이다.

이전에 프로젝트들을 하면서도 테스트 코드 작성은 막연히 어려운 부분이라고 생각했었는데

하나씩 차근차근 찾아보면서 진행하다보니 할 수 있겠다는 생각이 들었다.

 

우선 각 클래스, 메서드 별로 단위 테스트를 진행하기로 했다.

데이터를 주고받는 코드의 경우 임의의 객체를 통해 테스트 코드에 반영할 수 있었다.

하지만 이번 과제는 콘솔창에 데이터를 입력 / 출력하는 내용이 많아서 해당 부분을 어떻게 테스트에 담을지가 가장 큰 고민이었다.

과제에 이미 구현되어있던 테스트 코드들을 타고타고 들어가보면서 어떻게 테스트 코드를 구현했는지 확인했다.

 

1) System.setIn

System.setIn(new ByteArrayInputStream(buf));

위와 같이 System.setIn으로 표준 입력 스트림을 규정하는 클래스에 사용자가 원하는 값(buf)을 넣어주고있었다.

이 때 setIn()에 사용되는 매개변수 타입은 InputStream으로,

이를 상속받는 ByteArrayInputSteam을 생성해 매개변수로 넣어주면 된다.

setIn(), BuyteArrayInputStream에 대한 설명

 

최종적으로, 아래와 같이 콘솔 입력을 대체할 setInput(String input) 메서드를 정의하고 

각 테스트에서 필요한 데이터를 담아서 설정하는 방식을 사용했다.

public class InputViewTest {
    private void setInput(String input) {
        // 표준 입력 스트림을 임의의 입력 문자로 설정하는 역할을 함
        System.setIn(new ByteArrayInputStream(input.getBytes()));
    }

    @Test
    void 플레이어_이름_입력_확인() {
        setInput("aaa,bbb,ccc");
    
        List<String> validPlayerList = inputView.inputPlayerList();
        assertThat(validPlayerList).contains("aaa", "bbb", "ccc");
    }
    
    @AfterEach
    void tearDown() {
        // 사용한 리소스 권한을 정리함. Scanner 객체를 null로 설정하고 닫음
        Console.close();
    }
}
    
    
// 참고 Concole.close()메서드 내용
public static void close() {
    if (scanner != null) {
        scanner.close();
        scanner = null;
    }
}

 

2) System.setOut()

콘솔 입력이 있었다면 콘솔 출력과 관련된 메서드가 있다. System.setOut()이다.

마찬가지로 OutputStream과 ByteOutputStream을 사용한다.

ByteArrayOutputStream으로 할당된 값은 바로 확인이 불가능하다. 실제 값과의 비교를 위해 .toString()을 통해 값을 비교해주었다.

public class OutputViewTest {
    // System.out으로부터 나오는 출력을 담기 위한 객체. 출력된 내용이 이 스트림에 저장됨
    private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
    // 표준 스트림을 보존하기 위한 변수
    private final PrintStream originalOut = System.out;

    @BeforeEach
    void setUp() {
        // 표준 출력 스트림을 outContent로 재할당함. 출력 내용이 outContent가 됨
        System.setOut(new PrintStream(outContent));

        // 테스트용 객체 생성 로직(생략)
    }

    @AfterEach
    void tearDown() {
        System.setOut(originalOut); // 표준 스트림을 원래 상태로 복원함
    }

    @Test
    void 중간_결과_출력_확인() {
        outputView.printPartialResults(playerList);
        String consoleOutput = outContent.toString().trim(); // 원하는 내용 추출

        assertThat(consoleOutput).contains("aaa : -", "bbb : ", "ccc : -");
    }
    ...
}

 

비즈니스 로직 관련해서 총 9개의 테스트를 구현했고 모두 정상적으로 통과하는것을 확인할 수 있었다.


[2차 과제 회고]

 

며칠만에 또 한걸음 성장한 기분이다. 특히 지난주 다른분들의 코드를 본것이 많은 도움이 되었다.

다른분들의 코드를 보면서 어떻게 도메인을 구분했는지, 메서드는 어떻게 분리했는지, 어떤 방식으로 로직이 수행되는지 등 여러가지를 확인해볼 수 있었다.

그리고 다른분들의 코드리뷰를 보면서 생각하지 못했던 부분들도 많이 발견할 수 있었다.

enum을 사용해 추상화를 수행하는분도 있었고 요구되지 않았으나 테스트코드를 직접 짠 분들도 있었다.

 그분들을 보면서 더 열심히해야겠다는 자극도 받고 동기부여가 된것같다.

 

이번에 코드를 구현하면서 가장 크게 느낀 부분은 테스트코드와 관련이 있다.

테스트 코드 작성은 막연하게 어렵다고만 생각했는데 알아보고 하다보니 할 수 있겠다는 자신감이 생겼다.

특히, 테스트코드를 작성하고나서 리팩토링을 진행했을때 그 진가가 느껴졌다.

리팩토링이 잘못됐을때 테스트가 실패되면서 어느부분에 문제가 있는지 바로 알 수 있었다.

그리고 해당 부분들을 수정하면서 다시 리팩토링을 완료할 수 있었다.

이번 경험을 통해 아직은 어렵지만 TDD방식을 제대로 사용한다면 유용하겠다는 생각이 들었다.

 

짧은 기간이지만 생각보다 많은것들을 느끼고 배울 수 있어서 유익한 시간들이다.

 

참고자료

JUnit5 User Guide

JUnit5 Parameterized Test 사용하기