본문 바로가기
Education/우아한테크캠프Pro

[우아한테크캠프Pro] 2주차 - TDD

by 잭피 2020. 12. 20.

 

 

 

안녕하세요~ 잭코딩입니다!

 

(해당 업체에서 광고를 받지 않았으며, 교육비를 직접 내고 교육을 받고 있습니다)

교육을 받으며 느낀점을 적기 위한 포스팅입니다


벌써 우아한테크캠프 Pro 3주차 수업이 끝났습니다

2주차 미션(로또)을 완료하여 드디어 2주차 리뷰를 작성하네요

 

2주차까지는 단위 테스트, TDD를 통한 도메인 설계 미션이었고,

3주차 수업부터는 JPA 기반 미션이 진행되고 있습니다 

(실무에서 JPA를 사용하고 있는데 더 깊게 공부할 수 있는 좋은 기회인 거 같아요)

 

우선 2주차 수업에서는 1주차 미션(racingcar)에 관련된 내용이었습니다

포비가 실제 TDD로 구현하고 리팩터링 하는 과정을 라이브 코딩으로 지켜볼 수 있었습니다

 

1주차 미션을 수행할 때 고민했던 것들을 포비는 어떻게 구현하는지 라이브를 통해 볼 수 있어서 좋았습니다

또한, 궁금했던 내용을 대화방에서 물어볼 수도 있었습니다

 

이번 수업에서는 3가지를 정리했습니다

1. TDD

우선 기능 목록을 작성한 후, 테스트 가능한 부분을 찾아 TDD로 도전해야 합니다

(먼저 기능을 작은 단위로 쪼개는 연습을 해야 합니다)

 

 

NextStep 미션1 그림

 

1단계 - Unit 성격의 기능이 TDD로 도전하기 좋습니다

 

2단계 - 테스트 가능한 부분에 대해 먼저 TDD를 도전합시다

 

3단계 - 테스트하기 어려운 부분을 찾아 가능한 구조로 개선합시다

(객체 그래프에서 다른 객체와 의존관계를 가지지 않는 마지막 객체를 찾습니다)

 

4단계 - 아래 규칙을 힌트로 리팩토링 해봅니다

- 규칙 1 : 한 메서드에 오직 한 단계의 들여쓰기(indent)를 한다

- 규칙 2 : else 예약어를 쓰지 않는다

- 규칙 3 : 모든 원시값과 문자열을 포장한다

- 규칙 8 : 일급 콜렉션을 사용한다

 

미션1(레이싱카), 미션2(로또)를 수행할 때 위와 같은 단계로 연습를 했는데, 이제 조금 익숙해진 것 같네요

 

2. 레거시 코드를 안전하게 리팩토링 할 수 있는 방법

실무를 하다보면 다른 사람이 작성한 코드를 리팩토링 해야하는 경우가 생깁니다

다른 사람이 작성한 메서드를 잘못 수정했다가 큰 이슈가 발생할 수 있습니다

이런 경우 이 방법을 사용하면 좋을 것 같습니다

 

메소드 시그니처를 바꾸지 않고 테스트하는 방법

포비가 레거시 코드 활용 전략이라는 책을 추천해주면서 말해주신 방법입니다

(책의 내용이 어려우니 스터디를 통해 학습하기를 추천하셨습니다)

 

레거시 코드 활용 전략

 

이 책에서 소개된 Seam을 사용하여 리팩토링하는 방법을 알려주셨습니다

간단한 예제를 통해서 한번 봐보겠습니다

    public void move() {
        if (getRandomNo() > 5) {
            position++;
        }
    }

    private int getRandomNo() {
        return new Random().nextInt(8);
    }

현재 move()라는 메소드는 getRandomNo() 메소드에 의존하고 있습니다

 

여기서 메소드 시그니쳐를 수정할 수 없을 때,떻게 리팩토링을 해야할까요?

먼저 getRandomNo()를 protected로 변경합니다

 protected int getRandomNo() {
        return new Random().nextInt(8);
    }

이제 getRandomNo() 메소드는 테스트 코드에서 변경 가능한 Seam이 생깁니다

    @Test
    @DisplayName("전진 조건에 만족하면 전진")
    void move() {
        Car car = new Car("jack") {
            @Override
            protected int getRandomNo() {
                // 리팩토링하려는 메소드 로직 작성
                return 4;
            }
        };
        car.move();
        assertThat(car.getPosition()).isEqualTo(1);
    }

이제 getRandomNo()를 Override하여 로직을 테스트해볼 수 있습니다 

 

3. 디미터 법칙

객체의 내부 구조에 강하게 결합되지 않도록 제한을 하는 것을 말합니다

 

객체는 내부적으로 값을 가지거나 메시지를 통해 확보한 정보만 가지고 결정을 내려야 하고, 다른 객체를 탐색해 뭔가를 일어나게 해서는 안됩니다

 

그 결과 불필요한 어떤 것도 다른 객체에 보여주지 않고, 다른 객체에 의존하지 않게 됩니다

즉, 메시지 전송자와 수신자 사이에는 낮은 결합도를 유지할 수 있습니다

 

이 수업을 듣고 로또미션을 진행할 때는 사소한 정보까지 객체로 포장하여 자신이 값을 가지거나 메시지를 통해 정보를 확보할 수 있도록 작성하려고 노력해보았습니다

그리고 일급 컬렉션에 관련된 내용도 설명해주셨는데, 이전 리뷰에 포함되어있어서 생략하겠습니다

 

지난 미션(로또) 리팩토링

로또 미션을 수행하면서 기억나는 리팩토링은 로또 생성 클래스입니다

처음 스텝에서는 자동만 있었고 마지막 스텝에서 수동 로또생성 기능이 필요했습니다

 

기존 자동만 필요했을 경우는 따로 인터페이스로 구현하지 않고 클래스로 만들었습니다

public class LottoNumberGenerator {
    public static final int LOTTO_COUNT = 6;
    public static final int START_NUMBER = 1;
    public static final int END_NUMBER = 45;

    private List<LottoNumber> lottoNumbers;

    public LottoNumberGenerator() {
        this.lottoNumbers = new ArrayList<>();
        for (int i=START_NUMBER; i<=END_NUMBER; i++) lottoNumbers.add(new LottoNumber(i));
    }

    public Lotto generate() {
        Collections.shuffle(lottoNumbers);
        return new Lotto(
                lottoNumbers.stream()
                        .limit(LOTTO_COUNT)
                        .sorted(Comparator.comparing(LottoNumber::getNumber))
                        .collect(Collectors.toList()));
    }
}

기능은 단순합니다

1~45까지 무작위로 섞어서 generate() 호출시, 6개만 가져오는 기능입니다

 

마지막 Step4에서 수동으로 로또를 생성하는 방식을 추가하라는 미션이 주어졌습니다

자동의 경우는 따로 매개변수가 필요 없지만, 수동은 로또번호라는 매개변수가 필요했습니다

 

따라서 공통 인터페이스를 하나 만들고 추상 메서드의 매개변수는 가변 인수를 받도록 하였습니다

public interface LottoGenerator {
    int LOTTO_COUNT = 6;
    int START_NUMBER = 1;
    int END_NUMBER = 45;

    Lotto generate(String... varargs);
    boolean isNotMatchArgs(String... varargs);
}

자동 로또 생성기에서는 매개변수를 받지 않도록, 수동 로또 생성기에서는 매개변수를 1개만 받도록 체크하는 isNotMatchArgs()라는 추상 메서드도 함께 만들었습니다

 

로또 자동 생성기

public class LottoAutoGenerator implements LottoGenerator {
    private final int AUTO_VARARGS_SIZE = 0;

    private List<LottoNumber> lottoNumbers;

    public LottoAutoGenerator() {
        this.lottoNumbers = new ArrayList<>();
        for (int i=START_NUMBER; i<=END_NUMBER; i++) lottoNumbers.add(new LottoNumber(i));
    }

    @Override
    public Lotto generate(String... varargs) {
        if(isNotMatchArgs(varargs)) throw new IllegalArgumentException();

        Collections.shuffle(lottoNumbers);
        return new Lotto(
                lottoNumbers.stream()
                        .limit(LOTTO_COUNT)
                        .sorted(Comparator.comparing(LottoNumber::getNumber))
                        .collect(Collectors.toList()));
    }

    @Override
    public boolean isNotMatchArgs(String... varargs) {
        return varargs.length != AUTO_VARARGS_SIZE;
    }
}

 

로또 수동 생성기

public class LottoManualGenerator implements LottoGenerator {
    private final int MANUAL_VARARGS_SIZE = 1;

    @Override
    public Lotto generate(String... varargs) {
        if (isNotMatchArgs(varargs)) throw new IllegalArgumentException();

        String[] numbersArray = varargs[0].split(",");
        List<LottoNumber> lotto = new ArrayList<>();

        for (String number : numbersArray) {
            lotto.add(new LottoNumber(Integer.parseInt(number)));
        }
        return new Lotto(lotto);
    }

    @Override
    public boolean isNotMatchArgs(String... varargs) {
        return varargs.length != MANUAL_VARARGS_SIZE;
    }
}

각각 가변인수를 통해 매개변수를 제어하고 generate()라는 메소드를 통해 로또 번호를 생성할 수 있도록 구현해보았습니다

 

로또를 구매할 때마다 매개변수로 받은 구현체의 generator로 로또를 구입합니다

public void buyLotto(LottoGenerator lottoGenerator, String... varargs) {
	this.lottos.getLottos().add(lottoGenerator.generate(varargs));
}

 

실제 메인에서는 자동과 수동을 모두 buyLotto로 호출할 수 있습니다

원하는 로또 생성기 구현체(자동 or 수동)와 로또 번호(수동인 경우)를 넘겨주면 됩니다

public class LottoGameMain {

    public static void main(String[] args) {
        int amount = inputMessageLottoAmount();
        int manualCount = inputMessageLottoManualCount();

        LottoGame lottoGame = new LottoGame(BigDecimal.valueOf(amount), manualCount);
        LottoCount lottoCount = lottoGame.getLottoCount();
        
        // 수동 로또번호 구매
        for (int i=0; i<lottoCount.getManualCount(); i++) {
            lottoGame.buyLotto(new LottoManualGenerator(), inputLottoManual());
        }
        
        // 자동 로또번호 구매
        for (int i=0; i<lottoCount.getAutoCount(); i++) {
            lottoGame.buyLotto(new LottoAutoGenerator());
        } 
    }
}

이렇게 2주차 로또미션까지 끝냈습니다

 

3주차부터는 JPA 관련 미션이 진행되네요

JPA 미션부터는 김영한님의 JPA 강의를 들으면서 학습과 미션을 동시에 진행해보려고 합니다

 

그러면 3주차 미션을 완료하고 리뷰 작성하러 오겠습니다! 

댓글