ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TDD 기반 야구 게임 개발하기 1. 프로젝트 개요
    24년 11월 이전/레거시-야구 게임 (Feat. TDD) 2018. 8. 2. 22:11
    반응형

    * 먼저 이 프로젝트의 지적 자산은 '코드 스쿼드'에 있음을 밝힙니다. 필자의 프로젝트의 소스코드는 https://github.com/gurumee92/baseballtdd/ 에 있습니다.


    테스트 주도 개발 기반 야구 게임 만들기

    1단계. 프로젝트 개요


    1. 프로젝트 동기


    최근에 '코드 스쿼드'라는 교육기관에서 마스터즈 강의가 열렸습니다. 코드 스쿼드는 제가 개인적으로 제일 취직하고 싶은 회사인 우아한 형제들과 함께 작년, 올해 같이 인턴십을 교육을 진행하는 교육 기관입니다. 전 NHN Next 프로그래밍 전문 대학교의 강사진들이 모여서 만든 회사이기도 합니다. 아무튼 마스터즈 강의를 들으면 개인적으로도 실력 향상이 있을 것 같아 신청해보려 했는데 알아보니까 주어진 문제를 풀어야 하더라구요. 뭐 야구게임을 간단히 만들어보는 건데 이 문제의 URL은 적어 두었으니 관심있으신 분들은 확인해보세요.


    코드 스쿼드 마스터즈 강의 레벨 테스트 : 

    https://github.com/code-squad/test-item-pool/blob/master/level2-common/level2.md


    최근 한 달간 계속 알고리즘만 공부했더니 개인적으로 재미도 없고 뭐하는것 같지 않기도 해서 코드스쿼드 강의를 신청을 하지 않더라도 야구 게임을 한 번 만들어봐야겠다 생각해서 프로젝트를 시작하게 되었습니다. 개인적으로 계속 관심 있게 살펴보았던 테스트 주도 개발 방법으로 개발을 진행할 예정입니다.(2018. 08. 02 오늘 확인해보니까 이미 마감되었네요.... 분명 어제 열린건데.....OTL)


    잠깐! 테스트 주도 개발이란?

    일명 TDD (Test Driven Development)라고 일종의 개발 방법론입니다. 다음과 같은 주기에 따라서 개발을 진행합니다.


    1. 테스트 코드 작성
    2. 실패
    3. 성공을 위한 최소한의 코드 작성
    4. 리팩토링

    기능이 완성될 때까지 이 주기를 반복적으로 순회해서 코드를 작성하는 것이지요. 놀랍게도 이렇게 하면 정말 깔끔하게 변한답니다. 물론 TDD는 만능 도구가 아닙니다. 러닝커버도 높고 어느정도 훈련이 필요합니다. 저 역시도 클린 코더스 영상 강의도 보고 책도 살펴보고 진행해봤지만 솔직히 잘 하고 있는지 판단이 잘 안서더라구요.(그래서 코드 스쿼드에서 제대로 배워보고 싶었는데 ㅠㅠ 다시 열려라...) 처음 TDD를 시도하시는 분들은 일단 최소한의 정적 모델링을 먼저 한 상태에서 개발을 진행하면 훨씬 더 수월하게 진행 할 수 있습니다.


    2. 야구 게임 요구 사항 분석


    먼저 프로그래밍 제약 사항이 있습니다.


    • 함수(또는 메소드) 하나의 크기가 최대 10라인을 넘지 않도록 구현한다.

    • 함수(또는 메소드)가 한 가지 일만 하도록 최대한 작게 만들어라.

    • indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다. 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.

    • 전역 변수를 사용하지 않는다.

    그리고 제가 야구게임을 분석한 결과 다음의 요구사항이 있음을 알았습니다.


    1. 먼저 1~9까지 서로 다른 3개의 숫자를 뽑아야 합니다
    2. 그 3개의 숫자와 사용자가 입력한 3개의 숫자를 비교하여 결과를 출력합니다.


    위에서 TDD를 위해 최소한의 정적 모델링이 필요하다고 언급하였는데요. 일반적으로 모델링을 할 때 요구사항, 기능 분석등을 통하여 명사, 동사를 뽑아 클래스와 메소드로 추출하는 것이 정적 모델링입니다. 저는 이 과정을 생략하였습니다. 보다 간단한 정적 모델링을 위해 요구 사항을 조금 더 세밀하게 분류하였습니다. 먼저 두 번째 요구 사항은 다음과 같이 3가지의 기능이 결합되어 있습니다.


    1. 사용자에게 1~9까지 3개의 서로 다른 숫자들을 입력 받습니다.
    2. 임의로 만들어진 숫자3개와 사용자가 입력한 숫자3개를 비교합니다.
    3. 비교 결과를 사용자에게 보여줍니다.

    그러면 야구게임은 총 4가지의 큰 기능이 있겠군요! 따라서 분류한 4가지 큰 기능들을 클래스로 만들었습니다. 밑에 그림은 제가 짠 클래스 다이어그램입니다.



    설명을 덧붙이자면 랜덤 숫자 생성기와, 숫자 비교기는 게임에 종속되어 있는 기능들입니다. 어떤 환경이든지 게임 속에서 똑같이 동작해야하는 기능들이지요. 반면 사용자 입력기와 결과 출력기는 시스템에 영향을 받습니다. 콘솔에서는 키보드로 숫자를 입력하고 콘솔로 출력을 하게 되겠고 GUI 환경에서는 다른 마우스 클릭 등의 이벤트로 입력을 받고 환경에 맞는 출력 형식으로 결과를 출력하게 될 겁니다. 기능 상세는 같으나 구현이 달라지게 된다는 말이지요. 사실 이 부분은 그냥 넘어가도 됩니다.(아니 넘어가는게 맞을 것 같습니다.) 중요한 것은 자신이 분석한 요구사항에 맞는 기능들이 클래스로 표현됐는가이죠. 아무튼 정적 모델링까지 끝났으니 바로 프로젝트 설정과 깃 연동을 해보도록 하죠.


    3. 프로젝트 생성과 깃(헙) 연동을 위한 개발 환경 만들기


    일단 개발 환경부터 체크하도록 하죠. 저의 개발환경은 다음과 같습니다.


    • 언어 : 자바
    • IDE : 인텔리J 커뮤니티 에디션
    • 빌드 툴 : 그래들
    • OS : 윈도우
    • SVN : 깃헙

    일단, 이번 절은 다른 사람들의 블로그 내용을 참고하였습니다. 따라서 다른 사람의 포스팅 URL을 남기고 넘어가겠습니다. 절대! 귀찮아서가 아닙니다^^


    인텔리J의 TDD를 위한 Gradle 프로젝트 생성: http://jojoldu.tistory.com/138

    인텔리J 깃헙 연동 : https://nesoy.github.io/articles/2017-01/Intellj-Git


    개발 환경 설정 중에 잘 모르신다면 댓글을 남겨주세요! 최대한 도와드리겠습니다!


    4. TDD 기반 개발 시작.


    1. 가장 먼저 해야할 테스트! JUnit 잘 동작하니?


    자바의 단위 테스트 프레임워크 JUnit 은 Gradle 혹은 Maven으로 프로젝트를 생성하면 기본적으로 들어 있는 라이브러리입니다.(JUnit은 자기 자신을 프레임워크라고 표현하고 있습니다만... 저는 라이브러리라고 생각합니다) 일단 GameTest라는 클래스를 src/test/java 디렉토리에 만들고 다음 코드를 작성합니다.


    [src/test/java/GameTest.java]

    import org.junit.Test;

    public class GameTest {
    @Test
    public void nothing(){

    }
    }

    여기서 Run을 누르면 정상적으로 개발환경이 설정 되었다면 아래 콘솔에 다음과 같이 화면이 뜰 것입니다.



    에러가 뜬다면 다시 프로젝트 설정부터 해보시는게 저는 자바10을 인식을 못한건지 빌드 패스를 잘못 잡은건지 모르겠는데 계속 에러가 떴었습니다. 결국 자바8으로 변경하고 프로젝트를 다시 빌드하니까 되더라고요. 안되는 분들은 참고하세요! 아 그리고 nothing 메소드는 의미가 없고 JUnit이 잘 빌드되었는지 확인하는 아이입니다.


    2. 무엇을 테스트할까? 가장 쉬운 것부터 해!


    TDD를 처음 시작하면 제일 어려운게 무엇부터 시작할지 입니다. 많은 서적들은 가장  쉬운 것부터 테스트를 해보라고 권고하고 있습니다. 음 그렇다면 제일 쉬운 것은 무엇일까요? 일단 게임은 만드는 것이 제일 쉽겠죠? 이제 GameTest.java를  다음과 같이 변경하겠습니다. 


    [src/test/java/GameTest.java]

    import org.junit.Test;

    public class GameTest {
    @Test
    public void isGameCreateAvailable(){
    Game game = new Game();
    }
    }

    그러면 컴파일이 되지 않을겁니다. 왜냐하면 Game이라는 클래스가 없기 때문이지요. 위에 언급했던 테스트 주기를 기억하시나요? 

    1. 테스트 코드 작성
    2. 실패
    3. 성공을 위한 최소한의 코드 작성
    4. 리팩토링

    지금은 2단계인 실패입니다. 이제 성공을 위한 최소한의 코드 작성이 필요합니다. 무엇을 해야 테스트를 통과할까요? 바로 Game 클래스를 만드는 겁니다. 다만 Game 클래스는 실제 사용되는 클래스이기 때문에 test 디렉토리가 아닌 main 디렉토리에 작성합니다. IDE를 쓰신다면 단축키 혹은 코드 네비게이터를 통해서 간단하게 만들 수 있을겁니다.


    [src/main/java/Game.java] 

    public class Game {
    }

    자 이제 테스트를 실행하면 문제 없이 테스트가 통과되는 것을 확인할 수 있을 겁니다. 이제 전체 코드에서 중복되는 코드가 없는지 살펴봅시다. 2~3개 중복되는 코드가 있다면 메소드를 빼는것을 TDD에서 리팩토링이라고 합니다. 중복되는 것이 없군요. 다시 이제 테스트를 작성해봐야 할 것 같습니다. 잠깐! 좀 개발해보신 분들이라면 어차피 게임은 3개의 숫자를 가지니까 쉽게 길이라는 필드를 가질거라고 생각하실 것 같습니다. 그래서 바로 Game에 maxLen 이런 필드를 넣을 수 있겠지요. 그러나 TDD에서는 절대적인 규칙이 있는데 바로 이것입니다.


    "실제 테스트 코드가 아닌 소스 코드를 작성할 때 테스트 코드의 실패 없이 작성하지 말라!"


    실제로 테스트가 실패해야 그 실패를 성공시키기 위해서 소스 코드를 건드려야 한다는 것이지요. 자 이제 길이를 갖는 게임을 만들어봅시다. 


    [src/test/java/GameTest.java]

    import org.junit.Test;

    public class GameTest {
    @Test
    public void isGameCreateAvailable(){
    Game game = new Game();
    }

    @Test
    public void isGameCreateAvailableWhenMaxLenIs3(){
    Game game = new Game(3);
    }
    }


    뭐 이렇게 무식하게 메소드명이 길어?라고 하실 수 있습니다. 테스트 코드는 메소드 자체가 어떤 테스트를 하는지 메소드 이름이 말해주는 것이 좋습니다. 이는 소스코드의 메소드도 마찬가지입니다. 자 이제 테스트 실패가 될 겁니다. 저런 Game을 만드는 생성자가 없으니까요. 이제 Game을 고쳐 볼까요?


    [src/main/java/Game.java] 

    public class Game {
    private int maxLen;

    public Game(int maxLen){
    this.maxLen = maxLen;
    }
    }

    이렇게 하면 위의 테스트가 실패합니다. 따라서 두개의 생성자가 필요하죠.


    [src/main/java/Game.java] 

    public class Game {
    private int maxLen;

    public Game(){
    this(3);
    }

    public Game(int maxLen){
    this.maxLen = maxLen;
    }
    }

    자 이제 컴파일은 통과가 되었습니다. 테스트가 통과되는지 확인해 봅시다. 물론 통과가 될겁니다. 자 이제 중복이 있는지 살펴보도록 하죠. 먼저 테스트 코드에서 Game을 생성하는 것이 중복이 됩니다. 실제로 테스트 코드 작성시에 Game을 모두 만들어서 테스트해볼텐데 이럴때는 GameTest 내에 필드로 Game을 가지는게 더 좋죠. 그럼 이제 코드를 다음과 같이 바꿔봅시다.


    [src/test/java/Game.java] 

    import org.junit.Before;
    import org.junit.Test;

    public class GameTest {

    private Game game;

    @Before
    public void setUp(){

    game = new Game(3);
    }
    }

    네! 리팩토링을 통해서 GameTest 내 중복 코드를 제거하였습니다. 여기서 애노테이션 @Before은 테스트를 구동하기 전에 수행하는 작업들을 수행하는 메소드를 위한 애노테이션입니다. 물론 설명은 안했지만 @Test는 테스트가 구동되는 메소드들에 쓰이는 애노테이션이구요. 아직 한 가지 더 리팩토링할게 남았습니다. 전체적으로 중복되는게 없는데 이게 뭔 소리야? 라고 하실 수 있겠지만 GameTest와 Game에서 중복되는게 딱 하나 있습니다. 바로 3이라는 숫자죠. 이는 DEFAULT_LEN이라는 Game 클래스 내 상수로 따로 빼 두겠습니다. 이렇게요.


    [src/main/java/Game.java] 

    public class Game {
    public static final int DEFAULT_LEN = 3;

    private int maxLen;

    public Game(){
    this(DEFAULT_LEN);
    }

    public Game(int maxLen){
    this.maxLen = maxLen;
    }
    }

    테스트 코드에 3도 Game.DEFAULT_LEN으로 바꿔줍시다. 이제 다른 테스트로 넘어가기 전에 깃헙에 올려줍시다. 코멘트 메세지는 "Game 클래스 테스트 및 생성 완료"라고 하죠. (Git 연동 하는것은 위의 포스팅 URL에서 확인해주세요!) 원래는 테스트 하나 실패할 때마다 Git에 올리고 실패->성공, 리팩토링 마친 후에 또 올리는 것이 좋습니다. Game은 워낙 간단하니까 이렇게 한 것입니다. 이제 게임 클래스의 테스트는 일단 멈추도록 하죠. 왜냐하면 테스트를 하고 싶어도 내부의 기능이 만들어진게 없기 때문에 테스트가 쉽지 않기 때문입니다. 따라서 내부의 기능들을 먼저 테스트 및 개발하도록 하겠습니다.


    5. 마치며...

    이번 포스팅에서는 프로젝트 동기, TDD의 정의, (저의)야구게임의 간단한 정적 모델링, 인텔리J로 TDD 환경 세팅, 간단한 테스트 코드 작성에 대해서 진행해보았습니다. 다음 포스팅에서는 숫자 비교기를 기능 개발을 진행해보겠습니다.

    728x90
Designed by Tistory.