우아한형제들/우아한테크코스

[우아한테크코스] 2주차 후기 - 어서와 TDD는 처음이지?

Jay Tech 2019. 5. 20. 10:36
반응형

어서와 TDD는 처음이지?

우아한테크코스 2주차 후기

날짜 TODO 추가 공부
5/13 (월) 자동차 경주게임 피드백 -
5/14 (화) 강의 (TDD란 무엇인가) 버전 관리
5/15 (수) 페어 프로그래밍 (계산기, 사다리 타기) 책-객체지향과 디자인패턴
5/16 (목) 페어 프로그래밍 (사다리 타기), 사물함 배치 프로그램 책-객체지향과 디자인패턴
5/17 (금) 강의 (피드백), 페어 프로그래밍 (과제 마무리) 일급 컬렉션
5/18 (토) 테니스 시합 참가, 피드백 수정, 캡스톤 개발 -
5/19 (일) 피드백 수정, 캡스톤 미팅 -

 

2주차 진행 테이블이다. 이번 주는 주말까지 쉴 시간이 없어서 조금 피곤했다. 주말에 추가 공부를 하고 싶었지만 도저히 시간이 되지 않았다. (물론 잠을 더 안자면 되긴 하는데 체력 관리를 위해서...) 금요일까지 페어를 진행하고 각자의 브랜치로 나눠진 후에 헤어지고 집에 왔다. 토요일 오전에는 서울에서 테니스 경기가 있어서 참가를 했다. 그래도 월요일은 1시 출근! 잠을 좀 더 자야겠다.

본격적인 후기 시작~

강의: TDD란 무엇인가

이번 시간에도 자바지기님의 명강의를 들을 수 있었다.

TDD = TFD (Test First Development) + 리팩토링

TDD라 하면 테스트를 중점적으로 생각하는 사람들이 많은데 사실 여기에 리팩토링이라는 중요한 것이 추가가 되어야 진정한 TDD라고 할 수 있다. 정의를 하자면 TDD란 프로그래밍 의사결정과 피드백 사이의 간극을 의식하고 이를 제어하는 기술이라고 한다. (한국말인데 무슨소리지?)

참고- TDD와 단위테스트는 다르다! TDD는 분석 기술 혹은 설계 기술이라고 한다.

먼저 TFD라는 것을 보자. TFD란 Tet First Development의 약자로 프로덕션 코드가 없는데 테스트를 먼저 만드는 것이다. 그러면 시뻘건 글자와 컴파일을 할 수 없다는 메세지가 사방에 도배될 것이다. 그러고 나서 필요한 것들을 하나씩 만들면 TFD가 된다. 일전에 나는 처음에 거꾸로 작성을 한다라고 이해를 했지만 사실 거꾸로가 아니다. 오히려 이게 정상 수순이라고 할 수 있다. 왜냐하면 우리가 어떤 것을 만들어야 하는지! 에 대해 먼저 생각하기 때문이다. 무턱대고 프로덕션 코드부터 작성하면 갑자기 배가 산으로 가거나 허공에 칼질하는 느낌이 들 수 있다. 그래서 먼저 테스트를 작성을 한다면 '아 이런상황에는 이런 값이 나오는 기능을 만들어야지!'라고 계획을 세우게 되며 프로덕션 코드에서 정말 필요한 기능들만 작성할 수 있게 된다.

TFD에서 리팩토링이 추가가 되야 TDD라고 했다. 작성한 테스트코드 뿐만 아니라 프로덕션 코드도 재 점검을 실시하여 클린한 코드를 작성해야 한다. 조그만 기능을 추가할 때마다 이러한 사이클을 돌려 기능 하나 하나 예쁘게 완성해 나가는 것이다.

TDD를 하는 이유

  • 디버깅 시간을 줄여준다.
  • 동작하는 문서 역할을 한다.
  • 변화에 대한 두려움을 줄여준다.

TDD 사이클

먼저 실패하는 테스트를 구현한다. 그리고 테스트가 성공하도록 프로덕션 코드를 구현한다. 마지막으로 프로덕션 코드와 테스트 코드를 리팩토링 한다.

TDD 원칙

원칙1 - 실패하는 단위 테스트를 작성할 때 까지 프로덕션 코드를 작성하지 않는다.

원칙2 - 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.

원칙3 - 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

습관적으로 테스트 코드를 먼저 작성하는 연습을 병행하였다. 처음에는 어렵지만 하다 보면 조금씩 적응할 수 있게 되었다. 그리고 원칙3번에서 실패하는 테스트를 통과할 정도로만 실제 코드를 작성하게 되면 당연히 커버리지가 매우 높아진다. 저 말은 다시 말해 모든 프로덕션 코드는 테스트를 거친 코드라고 할 수 있다. 그래서 안정적인 코드라고 말할 수 있게 된다.

참고로 자바지기님께서 todo를 만드는 이유도 tdd원리 중 하나라고 하셨다. 그리고 테스트의 중요성을 매우 강조하셨는데 특히 백엔드에서 더 중요하다고 하셨다. 장애에 있어서 백엔드 장애가 프런트 장애보다 치명적일 수 있기 때문이라고 한다.

페어 프로그래밍 - 사다리 타기

프로그램은 매 주 페어가 바뀌는 형태로 진행된다. 새로운 페어와 함께 새로운 미션을 시작하였다. 미션은 계산기 프로그램과 사다리 타기 프로그램이다.

이 날은 계산기를 구현하였고 본격적인 미션인 사다리 타기 게임을 진행하였다. 하면서 TDD연습에 포커스를 맞췄다. 이번 미션에서 해야됐던 페어의 역할은 한 쪽이 프로덕션 코드를 무의식적으로 작성하면 옆에서 테스트 부터 작성하라고 야단 치는 것이었다. 나와 페어가 서로 테스트 봇이 되어 혹시라도 먼저 java 의 src를 누르는 순간 바로 야단을 쳤다.

일단 직접적인 원시 타입을 쓰는 것을 지양했다. 모든 자료형, 그리고 Collection을 포장했다. 예를들어, 자연수를 사용하기 위해 자연수를 정의했다.

package com.woowacourse.laddergame.util;

public class NaturalNumber {
    private static final int NATURAL_NUM_BOUNDARY = 0;
    private int number;

    public NaturalNumber(int number) {
        if (number <= NATURAL_NUM_BOUNDARY) {
            throw new IllegalArgumentException("자연수가 아닙니다.");
        }

        this.number = number;
    }

    public int convertIndex() {
        return number - 1;
    }

    public int getNumber() {
        return number;
    }
}

그리고 사다리를 구성하는 도메인에 대한 설계를 하였다. 사다리 부터 생각을 하게 되면 어렵기 때문에 한 단계씩 안으로 들어가서 제일 작은 단위를 생각했다. 사다리 -> 사다리 한줄 (라인) -> 라인의 한 점 (포지션). 그래서 제일 작은 단위부터 만들었다. 포지션에 대한 도메인을 구상하던 중 페어가 enum을 쓰는 것이 좋겠다는 의견을 냈다.

테스트를 먼저 작성한다.

class PositionTest {
    private int currentIdx = 3;
    private int maxIdx = 4;

    @Test
    public void 왼쪽으로_이동하는경우() {
        assertThat(Position.LEFT.move(currentIdx, maxIdx)).isEqualTo(currentIdx - 1);
    }

    @Test
    public void 오른쪽으로_이동하는경우() {
        assertThat(Position.RIGHT.move(currentIdx, maxIdx)).isEqualTo(currentIdx + 1);
    }

    @Test
    public void 정지_하는경우() {
        assertThat(Position.NONE.move(currentIdx, maxIdx)).isEqualTo(currentIdx);
    }

    @Test
    void 이동_불가한경우_자연수가_아닌_인덱스() {
        int zeroIndex = 0;

        assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {
            Position.LEFT.move(zeroIndex, maxIdx);
        }).withMessage("인덱스는 1부터 시작해야 합니다");
    }

    @Test
    void 이동_불가한경우_최대_인덱스를_넘는경우() {
        int currentIdx = 5;

        assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {
            Position.LEFT.move(currentIdx, maxIdx);
        }).withMessage("최대 인덱스보다 큰 인덱스입니다");
    }
}

Position 에서 나올 수 있는 모든 경우를 정의한다. 물론 저 테스트코드를 한 번에 다 쓴건 아니다. 하나씩 쓰고 그에 맞는 프로덕션 코드를 하나씩 추가하는 방식으로 진행하였다.

실제 코드이다.

package com.woowacourse.laddergame.domain;

import java.util.function.Function;

public enum Position {
    LEFT((index) -> index - 1),
    RIGHT((index) -> index + 1),
    NONE((index) -> index);

    private static final int LADDER_INDEX_BOUNDARY = 0;

    Function<Integer, Integer> function;

    Position(Function<Integer, Integer> function) {
        this.function = function;
    }

    int move(int index, int maxIndex) {
        if (index <= LADDER_INDEX_BOUNDARY) {
            throw new IllegalArgumentException("인덱스는 1부터 시작해야 합니다");
        }
        if (index > maxIndex) {
            throw new IllegalArgumentException("최대 인덱스보다 큰 인덱스입니다");
        }
        return this.function.apply(index);
    }
}

참고 글 : http://woowabros.github.io/tools/2017/07/10/java-enum-uses.html

 

Java Enum 활용기 - 우아한형제들 기술 블로그

안녕하세요? 우아한 형제들에서 결제/정산 시스템을 개발하고 있는 이동욱입니다.이번 사내 블로그 포스팅 주제로 저는 Java Enum 활용 경험을 선택하였습니다. 이전에 개인 블로그에 Enum에 관해 알게 된 점들을 정리했음에도 선택한 이유는 제가 Enum을 통해 많은 도움을 얻었기...

woowabros.github.io

각 점은 왼쪽으로 이동, 오른쪽으로 이동, 정지 (사다리가 그려지지 않은 부분) 이렇게 3가지로 표현가능하다. 그래서 본인만의 계산식을 가질수가 있다.

사다리 한 라인을 어떻게 정의할 것인가

사다리 한 줄에는 여러 포지션들이 있고 각 포지션은 다리를 놓을 수 있다. 하지만 연속되게는 놓을 수 없다. (사다리의 규칙)

그렇다면 테스트를 어떻게 시작해야 될까?

public class LineTest {
    private int countOfPerson;
    private Line line;

    @BeforeEach
    void setUp() {
        countOfPerson = 4;
        line = new Line(new NaturalNumber(countOfPerson));
    }
    /// 중략
    @Test
    void 정상적_사다리_초기화() {
        //    |-----|     |-----|
        line.putBridge(new NaturalNumber(1));
        line.putBridge(new NaturalNumber(3));

        assertThat(line.isBridgeExist(1)).isTrue();
        assertThat(line.isBridgeExist(3)).isTrue();
    }

    @Test
    void 비정상적_사다리_초기화() {
        assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() ->
                line.putBridge(new NaturalNumber(4))).withMessage("다리를 놓을 수 없습니다");
    }

    @Test
    void 연속해서_다리를_놓는_예외처리() {
        line.putBridge(new NaturalNumber(2));

        assertThrows(IllegalArgumentException.class, () -> {
            line.putBridge(new NaturalNumber(3));
        });
    }

    /// 중략

먼저 테스트에 사용될 라인을 미리 정의해 놓는다. 제일 핵심이 되는 로직을 먼저 테스트한다. assertThrows와 assertThatExceptionOfType을 비교하기 위해서 각각 써 보았는데 예외 처리 메세지까지 한 번에 할 수 있도록 하는 것이 더 효율적이라고 생각되어 첫 번째 예외검증 방식을 선호했다.

정상적_사다리_초기화()는 주석으로 어떤 모양이 그려지는지 표시를 해 놓았다. 메서드 이름 그대로 1번에 다리를 놓고 3번에 다리를 놓는다. 그렇다면 테스트는 1번위치에 다리가 존재하는지? 3번위치에 다리가 존재하는지 보면 될것이다.

그렇다면 프로덕션 코드는 이렇게 생기면 될 것이다.

이렇게 테스트를 먼저하고 프로덕션 코드를 작성한다. 물론 위의 테스트들이 전부가 아니다. 라인의 여러 모양들을 테스트 하였고 각종 예외상황들을 모두 정의했다. (중략)

사다리를 타보자

@BeforeEach
void setUp() {
    height = new NaturalNumber(3);
    countOfPerson = new NaturalNumber(4);
    testBooleanData = new boolean[]{true, true, false, true, false, false, true};
    booleanGenerator = new TestBooleanGenerator(testBooleanData);
    ladder = new Ladder(height, countOfPerson, booleanGenerator);
}

테스트 부터 작성한다. 테스트에 사용될 사다리를 미리 정의한다. 사다리는 랜덤으로 만들어지므로 그에 따른 테스트사다리를 만들어야 하므로 TestBooleanGenerator를 넘겨 의도된 사다리로 나오게 한다.

@Test
void 사다리타기_1번사람() {
    // 1번 사람은 4번 인덱스로 반환 되어야 함
    int firstIdx = ladder.takeLadder(new NaturalNumber(1));

    assertThat(firstIdx).isEqualTo(4);
}

저 사다리 모양 대로 초기화가 되었으면 1번 사람은 4번 인덱스로 반환되어야 할 것이다. 그것을 테스트 하는 부분이다. 이제 이대로 프로덕션 코드를 하나씩 추가해나갔다.

미션의 자세한 내용은 피드백 후기에서 살펴보겠다.

사물함 배치 프로그램

어느 날 아침, 자바지기님께서 이벤트가 있다고 하셨다. 하지만 그것은 전혀 예상치 못한 이벤트였다.

코치님들께서 우리 교육생들을 위해서 사물함을 새로 들여오셨는데 그것을 어떻게 공평하게 배정할 지 결정하는 프로그램을 짜는 이벤트였다.

코드 품질을 전혀 신경쓰지않고 일단 제일 빨리 돌아가게 만든 팀에게 사물함 자리 우선권을 주는 형식이었다.

이벤트

갑자기 요구사항이 pdf로 왔다. 바로 날코딩을 시작했다.

알고리즘 연습할 때 인덱스 뻑나는게 제일 골칫거리였는데 페어가 수학을 잘해서 거의 인덱스를 가지고 놀았다. 다했다고 생각해서 앞으로 나갔다. 근데 우리가 너무 급하게 했는지 요구사항 하나를 빠트려서 리젝당했다. "각 pair는 자신이 원하는 자리를 선점할 수 있어야 한다" 라는 것을 빠트렸다. 다시 부랴부랴 서로 닉네임을 찾아서 인덱스에 배정을 하는 사이 다른 팀이 먼저 완성을해서 이벤트는 끝이 났다.

근데 신기한 건 다른 팀의 프로그램으로 랜덤 배정된 나의 사물함 자리가 나와 페어의 프로그램에서 내가 지정한 내 자리였다. 세상에.

강의: 사다리 게임 TDD 추가 설명

한 번의 완벽한 설계는 없다

자바지기님께서 빨리 짜는게 중요한게 아니라고 하셨다. 한 번에 완벽하게 설계할 필요는 없으며 초기에는 도메인에 대한 이해도가 낮아서 설계품질이 낮을수 밖에 없다고 하셨다.

private 생성자

한 번도 해보지 못한 객체 초기화 방법을 목격했다.

public Direction first(boolean b) {
    return new Direction(false, b);
}

public Direction next(boolean b) {
    return new Direction(hasRight, b);
}

... 등등

변수명 말고 방법에 집중해보자. 생성자를 private 으로 만든다는 것은 외부에서 접근할 수 없다는 것을 의미한다. 대신에 여러 가지 경우의 초기화 방법을 메서드로 노출시켜서 그 안에서 생성자를 부르는 방법이다. 그렇게 되면 생성자 로직도 줄게 되고 조금 더 유연한 구조를 갖출수 있는 것 같다.

일급 컬렉션

일급 컬렉션에 대한 내용을 참고한 블로그이다. 자세한 내용은 이 블로그를 참고하자.

https://jojoldu.tistory.com/412

 

일급 컬렉션 (First Class Collection)의 소개와 써야할 이유

최근 클린코드 & TDD 강의의 리뷰어로 참가하면서 많은 분들이 공통적으로 어려워 하는 개념 한가지를 발견하게 되었습니다. 바로 일급 컬렉션인데요. 왜 객체지향적으로, 리팩토링하기 쉬운 코드로 갈려면 일급..

jojoldu.tistory.com

일급 컬렉션은 컬렉션의 불변을 보장한다고 한다. 자세한 피드백은 피드백 후기에서 확인해보자.

간단히 보면,

public class Players {
    private final List<Player> players;

    public Players(List<Player> players) {
        checkDuplicateName(players);
        this.players = players;
    }
    // 중략

자료구조를 final로 선언해주고 저 Players 객체는 생성 직후 Player를 add할 수있게 노출시켜주는 메서드가 존재하지 않는다.

저것을 지키지 못하여 첫 번째 피드백에서 지적을 받았다.

VO와 DTO

강의 시간에 VO와 DTO의 차이점에 대해서 질문하였다. 내가 알고 있었던 것들과 많이 차이가 있어서 잘못된 개념을 바로 잡을 수 있는 좋은 기회였다.

VO는 한 번 초기화 되면 변경할 수 없는 특징을 가졌다. 나는 이것이 최앞단에서 사용자에게 보여줄 값들을 전달할때 쓰이는 줄 알았지만 사실은 반대였다. 오히려 domain이 vo라고 할 수 있다. 하지만 Dto는 도메인과 같을 수도 있고 다를 수도있다. 근데 대부분 같다면 domain으로 앞단까지 통신하는 경우도 있다고 하셨다. 하지만 그리 좋은 설계는 아니라고 언급하시며 domain과 dto가 달라야 좋은 설계라고 했다. 그리고 dto에서는 getter, setter를 허용한다.

이 문제에 대해서는 내가 제출한 과제의 피드백에서 자세한 내용을 볼 수 있다.

반응형