[우아한테크코스 7기 프리코스] 2주차 회고록

1. 1주차 피드백 정리

피드백은 공통 피드백, 코드 리뷰, 대면 스터디에서 얻은 피드백을 모두 정리했다. 뿐만 아니라 다른 사람들의 코드를 보며 나도 고려해보면 좋을 것 같은 내용들도 정리했다.

 

  • Git으로 관리할 자원을 고려하자.
  • 커밋 메시지를 의미 있게 작성하자.
  • 공백 라인을 의미 있게(4개의 빈 칸을 들여쓰기 단위로) 사용하자.
  • 각 클래스와 메서드의 행위를 더 작게 분리하자.
  • 메서드 네이밍에 더 많은 시간을 할애하자.
  • 확장성을 위해 MVC 패턴을 적용해보자.
  • 확장성을 위해 Output을 담당하는 클래스에서 안내 메시지 출력 기능을 담당해보자.
  • 입・출력 객체 생성은 Controller 계층으로 분리해보자.
  • 요구사항을 더 꼼꼼하게 읽고, 이에 대한 검증을 확실히 구현하자.
  • 예외 케이스를 더 꼼꼼하게 많이 생각해보고, 이를 테스트해보자.
  • 유지보수성을 위해 매직 넘버 대신 상수화를 사용하자.
  • enum으로 상수를 관리하자.
  • 가독성을 위해 stream()과 같은 Collection을 사용하자. for문을 사용하게 되었다면, 해당 메서드의 기능이 많은 책임을 맡은 것은 아닌지 생각해보자.
  • 기본 제공된 테스트처럼 equals() 대신 contains()를 사용해보자.
  • 객체 생성을 통한 메서드 호출과 static 메서드 호출 중 어떤 것이 더 좋을지 고민해보자.

 


 

2. 2주차 목표

  • Java Collection 사용하기
  • 더 많은 테스트 케이스 생각해보고, 작성하기
  • 요구사항 꼼꼼히 읽고, 이를 만족하는지 수차례 확인하기
  • 모던 자바 인 액션, 이펙티브 자바, 좋은 코드 나쁜 코드 서적 적극 활용하기
  • 1주차 피드백을 꼼꼼히 살펴보고 적용한 후, 2주차에는 새로운 피드백 얻기

 


 

3. 피드백 반영 과정

3-1. 커밋 메시지를 의미있게 작성하자.

커밋 메시지 형식은 기본적으로 3가지 영역(제목, 본문, 꼬리말)으로 나누어진다. 이에 맞게 feat과 같은 type과 함께 어떤 점이 변화되었는지를 body에 같이 담고자 했다. 제목에는 간결하게 요점만 서술하였고, 본문에는 무엇을/왜 진행했는지 서술했다. 꼬리말에는 주로 커밋 이슈 ID를 작성하는데, 이번 미션에서는 작성하지 않는 게 좋다는 피드백을 받아 생략했다. 이렇게 커밋 메시지를 의미있게 작성하니, 해당 커밋을 확인하는 동료 입장에서 변경 사항을 쉽게 이해할 수 있다는 것을 깨달았다.

 

type: Subject

body

footer

 

3-2. Java 컬렉션을 이용해보자.

1주차 코드 리뷰에서 Stream을 사용하면 훨씬 가독성이 좋다는 리뷰를 많이 받았다. 또한 배열 대신 컬렉션을 사용해보기 미션을 부여받았다!

  • for문 사용
private static int findMaxPosition(List<Car> cars) {
    int maxPosition = Integer.MIN_VALUE;
    for (Car car : cars) {
        if (car.getPosition() > maxPosition) {
            maxPosition = car.getPosition();
        }
    }
    return maxPosition;
}

private static List<Car> filterWinners(List<Car> cars, int maxPosition) {
    List<Car> winners = new ArrayList<>();
    for (Car car : cars) {
        if (car.getPosition() == maxPosition) {
            winners.add(car);
        }
    }
    return winners;
}

 

  • stream() 사용
private static int findMaxPosition(List<Car> cars) {
	return cars.stream()
		.mapToInt(Car::getPosition)
		.max()
		.orElseThrow();
}

private static List<Car> filterWinners(List<Car> cars, int maxPosition) {
	return cars.stream()
		.filter(car -> car.getPosition() == maxPosition)
		.collect(Collectors.toList());
}

 

배열보다는 리스트를 사용하라.
- 이펙티브 자바 : 아이템 28 -

 

Stream API는 선언형으로 코드가 간결해지고,
조립할 수 있고, 병렬화로 인해 성능이 좋아진다.
- 모던 자바 인 액션 : Chapter 4 -

 

리스트를 사용하니 크기가 고정적이지 않아 유연하다는 느낌을 받았다. 또한 stream()을 사용하니 for문을 사용했을 때보다 훨씬 가독성이 좋고, Depth가 1로 유지되는 것을 확인할 수 있었다. 복잡한 연산이 아니기에 성능 측면에서도 큰 차이가 없다. 하지만 mapToInt, filter, collect 등의 연산은 체이닝으로 연결되기 때문에 추가적인 객체 생성과 메모리 할당이 발생할 수 있으니, 이 점을 항상 유의하고 사용해야겠다.

 

3-3. MVC 패턴을 적용해보자.

 

1주차 종료 후 다른 지원자분들의 코드 리뷰를 하다보니, MVC 패턴을 많이 적용하신 것을 볼 수 있었다. 1주차 미션에서는 클래스 분리만 행했고, 클래스가 깊은 구조를 띄는 것 같다는 피드백을 받았다. 따라서 이번 2주차 미션에서는 MVC 패턴을 도입해보았다. 이후 1주차 코드와 비교해보니 각 클래스의 결합도는 낮고, 응집도는 더 높아진 것을 확인할 수 있었다.

 

  • 패키지 구조
더보기
 _______ controller
|            |
|            |____ RaceController.java
|
|
|_________ model
|            |
|            |___ domain
|            |      |
|            |      |___ Car.java
|            |      |
|            |      |___ AttemptNumber.java
|            |      |
|            |      |___ Race.java
|            |      |
|            |      |___ Winner.java
|            |      |
|            |      |___ Delimiter.java
|            |      |
|            |      |___ Validation.java
|            |      |
|            |      |___ ErrorMessage.java
|            |
|            |
|            |___ service
|            |      |
|            |      |___ RaceService.java
|            |
|            |___ util _ Parse.java
|
|_______ view
|          |
|          |___ InputView.java
|          |
|          |___ OutputView.java
|          |
|          |___ OutputMessage.java
|            
|
|_______Application.java

 

하나의 클래스가 하나의 기능만 담당하도록 구현하고 싶어 Domain은 아래와 같이 정하였다.

  • Car : 자동차의 이름/위치를 관리하고, 이동하는 기능을 담당하는 객체
  • AttemptNumber : 시도 횟수를 관리하는 객체
  • Race : randomNumber를 생성해, 한 번의 경주를 행하는 기능을 담당하는 객체
  • Winner : 우승자를 선별하는 기능을 담당하는 객체

 

View는 InputView와 OutputView로 구분하였다.

  • InputView : 입력 기능을 담당하는 객체
  • OutputView : 출력 기능을 담당하는 객체

 

Controller와 Service는 다음과 같다.

  • RaceController : 안내 문구 출력, 사용자 입력 처리, 유효값 검증, Service 메서드 호출을 담당하는 객체
  • RaceService : 실행 결과 및 우승자 반환을 담당하는 객체

 

이외에 클래스는 다음과 같다.

  • Validation : 입력 유효값 검증 및 예외 처리를 담당하는 객체
  • Parse : int로 변환, Car 객체로 변환하는 기능을 담당하는 객체

 

3-4. 상수, enum을 사용해보자.

다른 지원자분들의 코드 리뷰를 하는 과정에서 Enum을 사용한 것을 볼 수 있었다. 또한 상수화를 통해 매직 넘버를 제거하는 것이 좋다는 피드백을 받았다.

 

Enum은 int 상수보다 더 읽기 쉽고, 안전하다.
- 이펙티브 자바 -

 

따라서 Delimiter, OutputMessage, ErrorMessage를 Enum으로 관리하니 코드의 가독성과 유지보수성이 높아진다는 것을 확인할 수 있었다. 또한 private static final을 통해 기호를 상수로 정의하니, 불변성을 유지하면서 코드의 중복을 줄일 수 있었다.

 

  • OutputMessage
public enum OutputMessage {
	INPUT_CAR_NAME("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"),
	INPUT_ATTEMPT_NUMBER("시도할 횟수는 몇 회인가요?"),
	RACE_RESULT("\n실행 결과"),
	FINAL_WINNER("최종 우승자 : ");

	private final String message;

	OutputMessage(String message) {
		this.message = message;
	}

	public String getMessage() {
		return message;
	}
}

 

  • OutputView
public class OutputView {
	private static final String SEMI_COLON = " : ";
	private static final String HYPHEN = "-";
}

 

  • Delimiter
public enum Delimiter {
	COMMA(",");

	private final String delimiter;

	Delimiter(String delimiter) {
		this.delimiter = delimiter;
	}

	public String getDelimiter() {
		return delimiter;
	}

	public List<String> splitCarNames(String carNames) {
		return Arrays.asList(carNames.split(this.delimiter));
	}
}

 

3-5. 메서드를 static으로 호출할지 vs 객체 생성해서 호출할지

각 경우에 따라 메서드에 static 키워드 유무를 판단하고 싶었다. 구글링해보니 static 메서드는 객체의 고유 상태를 필요로 하지 않는 경우에 적합하다. 반면, 객체가 고유 상태를 가지거나 여러 메서드를 통해 상태를 다루는 경우에는 객체를 생성하여 사용하는 것이 좋다. 따라서 Validation, InputView, OutputView 내에 있는 메서드는 static을 사용하고, Controller, Service, Domain은 static을 사용하지 않았다. 그 결과, 클래스 간의 주요 관계가 명확히 보이는 것을 확인할 수 있었다.

 

  • 코드 일부
public class RaceController {
	private RaceService raceService;

	public void start() {
		runRace();
		runResult();
		displayWinners();
	}

	private void runRace() {
		String carNames = getCarNames();
		int attemptNumber = getAttemptNumber();
		this.raceService = new RaceService(Delimiter.COMMA, carNames, attemptNumber);
	}

	private String getCarNames() {
		OutputView.carName();
		String carNames = InputView.getCarNames();

		Validation.validateCarNames(carNames);
		return carNames;
	}

	private int getAttemptNumber() {
		OutputView.attemptNumber();
		int attemptNumber = InputView.getAttemptNumber();

		Validation.validateAttemptNumber(attemptNumber);
		return attemptNumber;
	}

	private void runResult() {
		OutputView.runResultGuide();
		raceService.runResult();
	}

	private void displayWinners() {
		OutputView.winners(raceService.getWinners());
	}
}

 

3-6. 필드에서 객체 생성 및 초기화 vs 생성자 주입

이펙티브 자바에 따르면 자원을 직접 명시하지 말고 의존 객체 주입을 사용하는 것이 좋다고 한다. 따라서 Controller, Service, Domain과 같은 주요 클래스들에 생성자 주입을 사용하니, 의존성을 더 유연하게 관리할 수 있다는 것을 깨달았다.

 

자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
- 이펙티브 자바 : 아이템 5 -

 

3-7. 검증 클래스(입력에 대한 예외처리) 분리할지

2주차 미션에서 자동차의 이름을 입력받는 기능을 구현하며, 입력 검증 로직이 복잡해지는 것을 해결하고 싶었다. 좋은 코드, 나쁜 코드에 따르면 코드를 모듈화하고, 재사용할 수 있도록 일반화하는 것이 좋다고 한다. 따라서 자동차 이름 및 시도 횟수 검증 기능을 Validation 클래스로 분리했다. 입력 예외 상황이 추가로 떠올랐을 때, 해당 클래스를 재사용할 수 있었다.

 

코드를 모듈화하고, 재사용 할 수 있도록 일반화하라
- 좋은 코드, 나쁜 코드 : Chapter 1 -

 

또한 예외 케이스를 추가하는 과정에서 도메인에서 검증을 처리하니 테스트코드에서 문제가 발생했다. 여러 테스트 케이스를 위해 자동차 입력과 시도 횟수 입력 검증이 객체를 생성할 때마다 실행돼 발생한 문제였다. 따라서 해당 코드를 Controller로 옮겨 문제를 해결했다. 하지만 입력 검증이 Controller에서 이루어지는 것이 맞는지 아직 의문이 든다. 이 부분에 대해서는 3주차 미션을 해결하며 확실한 근거를 확립해야겠다.

 

  • 코드 수정 전
public class InputView {
	public static String getCarNames() {
    		Validation.validateCarNames(carNames);
		return Console.readLine();
	}

	public static int getAttemptNumber() {
		String attemptNumber = Console.readLine();
        	int parseAttemptNumber = Parse.parseToInt(attemptNumber);
        	Validation.validateAttemptNumber(parseAttemptNumber);

		return parseAttemptNumber;
	}
}

public class RaceController {
	private RaceService raceService;

	public void start() {
		runRace();
		runResult();
		displayWinners();
	}

	private void runRace() {
		String carNames = getCarNames();
		int attemptNumber = getAttemptNumber();
		this.raceService = new RaceService(Delimiter.COMMA, carNames, attemptNumber);
	}

	private String getCarNames() {
		OutputView.carName();
		String carNames = InputView.getCarNames();

		return carNames;
	}

	private int getAttemptNumber() {
		OutputView.attemptNumber();
		int attemptNumber = InputView.getAttemptNumber();

		return attemptNumber;
	}

	private void runResult() {
		OutputView.runResultGuide();
		raceService.runResult();
	}

	private void displayWinners() {
		OutputView.winners(raceService.getWinners());
	}
}

 

  • 코드 수정 후
public class InputView {
	public static String getCarNames() {
		return Console.readLine();
	}

	public static int getAttemptNumber() {
		String attemptNumber = Console.readLine();

		return Parse.parseToInt(attemptNumber);
	}
}

public class RaceController {
	private RaceService raceService;

	public void start() {
		runRace();
		runResult();
		displayWinners();
	}

	private void runRace() {
		String carNames = getCarNames();
		int attemptNumber = getAttemptNumber();
		this.raceService = new RaceService(Delimiter.COMMA, carNames, attemptNumber);
	}

	private String getCarNames() {
		OutputView.carName();
		String carNames = InputView.getCarNames();

		Validation.validateCarNames(carNames);
		return carNames;
	}

	private int getAttemptNumber() {
		OutputView.attemptNumber();
		int attemptNumber = InputView.getAttemptNumber();

		Validation.validateAttemptNumber(attemptNumber);
		return attemptNumber;
	}

	private void runResult() {
		OutputView.runResultGuide();
		raceService.runResult();
	}

	private void displayWinners() {
		OutputView.winners(raceService.getWinners());
	}
}

 

3-8. 테스트코드

(1) 우테코 테스트 라이브러리 분석

  • assertSimpleTest : 지정된 실행 블록(executable)이 1초 안에 완료되는지 검증
  • assertRandomNumberInListTest : Randoms.pickNumberInList의 반환값을 지정된 값으로 설정하고 테스트가 10초 내에 완료되는지 검증
  • assertRandomNumberInRangeTes t: Randoms.pickNumberInRange의 반환값을 설정하고 10초 내에 완료되는지 검증
  • assertRandomUniqueNumbersInRangeTest : Randoms.pickUniqueNumbersInRange의 반환값을 설정하고 10초 내에 완료되는지 검증
  • assertShuffleTest : Randoms.shuffle의 결과를 지정된 리스트로 설정하고 10초 내에 완료되는지 검증
  • assertRandomTest : 주어진 무작위 메서드의 반환값을 설정하고 테스트가 10초 내에 완료되는지 검증하는 공통 메서드
  • assertNowTest : DateTimes.now의 반환값을 지정된 시간으로 설정하고 10초 내에 완료되는지 검증
더보기
import camp.nextstep.edu.missionutils.DateTimes;
import camp.nextstep.edu.missionutils.Randoms;
import org.junit.jupiter.api.function.Executable;
import org.mockito.MockedStatic;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.MockedStatic.Verification;
import static org.mockito.Mockito.mockStatic;

public class Assertions {
    private static final Duration SIMPLE_TEST_TIMEOUT = Duration.ofSeconds(1L);
    private static final Duration RANDOM_TEST_TIMEOUT = Duration.ofSeconds(10L);

    private Assertions() {
    }

    public static void assertSimpleTest(final Executable executable) {
        assertTimeoutPreemptively(SIMPLE_TEST_TIMEOUT, executable);
    }

    public static void assertRandomNumberInListTest(
        final Executable executable,
        final Integer value,
        final Integer... values
    ) {
        assertRandomTest(
            () -> Randoms.pickNumberInList(anyList()),
            executable,
            value,
            values
        );
    }

    public static void assertRandomNumberInRangeTest(
        final Executable executable,
        final Integer value,
        final Integer... values
    ) {
        assertRandomTest(
            () -> Randoms.pickNumberInRange(anyInt(), anyInt()),
            executable,
            value,
            values
        );
    }

    public static void assertRandomUniqueNumbersInRangeTest(
        final Executable executable,
        final List<Integer> value,
        final List<Integer>... values
    ) {
        assertRandomTest(
            () -> Randoms.pickUniqueNumbersInRange(anyInt(), anyInt(), anyInt()),
            executable,
            value,
            values
        );
    }

    public static <T> void assertShuffleTest(
        final Executable executable,
        final List<T> value,
        final List<T>... values
    ) {
        assertRandomTest(
            () -> Randoms.shuffle(anyList()),
            executable,
            value,
            values
        );
    }

    private static <T> void assertRandomTest(
        final Verification verification,
        final Executable executable,
        final T value,
        final T... values
    ) {
        assertTimeoutPreemptively(RANDOM_TEST_TIMEOUT, () -> {
            try (final MockedStatic<Randoms> mock = mockStatic(Randoms.class)) {
                mock.when(verification).thenReturn(value, Arrays.stream(values).toArray());
                executable.execute();
            }
        });
    }

    public static void assertNowTest(
        final Executable executable,
        final LocalDateTime value,
        final LocalDateTime... values
    ) {
        assertTimeoutPreemptively(RANDOM_TEST_TIMEOUT, () -> {
            try (final MockedStatic<DateTimes> mock = mockStatic(DateTimes.class)) {
                mock.when(DateTimes::now).thenReturn(value, Arrays.stream(values).toArray());
                executable.execute();
            }
        });
    }
}

 

  • init : 테스트 시작 전 콘솔 출력을 캡처하도록 설정
  • printOutput : 테스트 종료 후 캡처된 콘솔 출력을 원래대로 돌리고 출력
  • output : 캡처된 콘솔 출력을 문자열로 반환
  • run : 입력 값을 설정하고 테스트 프로그램의 runMain 메서드 실행
  • runException : run을 실행하되, 예외가 발생하면 무시하고 테스트 마침
  • command : 지정된 입력 값을 콘솔 입력으로 설정
  • runMain : 실제 테스트할 프로그램의 메인 로직을 구현하기 위한 추상 메서드
더보기
import camp.nextstep.edu.missionutils.Console;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.NoSuchElementException;

public abstract class NsTest {
    private PrintStream standardOut;
    private OutputStream captor;

    @BeforeEach
    protected final void init() {
        standardOut = System.out;
        captor = new ByteArrayOutputStream();
        System.setOut(new PrintStream(captor));
    }

    @AfterEach
    protected final void printOutput() {
        System.setOut(standardOut);
        System.out.println(output());
    }

    protected final String output() {
        return captor.toString().trim();
    }

    protected final void run(final String... args) {
        try {
            command(args);
            runMain();
        } finally {
            Console.close();
        }
    }

    protected final void runException(final String... args) {
        try {
            run(args);
        } catch (final NoSuchElementException ignore) {
        }
    }

    private void command(final String... args) {
        final byte[] buf = String.join("\n", args).getBytes();
        System.setIn(new ByteArrayInputStream(buf));
    }

    protected abstract void runMain();
}

 

 

위 내용을 바탕으로 각 기능에 대한 테스트 코드를 작성하였다. 아래는 테스트코드의 일부이다.

  • CarTest
    • @ParameterizedTest : 여러 개의 테스트를 한번에 작성하기 위한 어노테이션
    • @ValueSource : 테스트에 주입할 값을 어노테이션에 배열로 지정
class CarTest {

	@ParameterizedTest
	@ValueSource(strings = {"", " "})
	void 자동차_이름이_비어있을_경우_예외(String carNames) {
		Assertions.assertSimpleTest(() -> {
			assertThatThrownBy(() -> Validation.validateCarNames(carNames))
				.isInstanceOf(IllegalArgumentException.class)
				.hasMessage(ErrorMessage.EMPTY_CAR_NAME.getMessage());
		});
	}

	@ParameterizedTest
	@ValueSource(strings = {"pobi|woni|jun", "pobi-woni-jun"})
	void 쉼표가_아닌_다른_구분자가_입력된_경우_예외(String carNames) {
		Assertions.assertSimpleTest(() -> {
			assertThatThrownBy(() -> Validation.validateCarNames(carNames))
				.isInstanceOf(IllegalArgumentException.class)
				.hasMessage(ErrorMessage.INVALID_DELIMITER.getMessage());
		});
	}

	@ParameterizedTest
	@ValueSource(strings = {"pobi ,woni, jun", "pobi, woni"})
	void 이름과_구분자_사이에_공백이_있을_경우_예외(String carNames) {
		Assertions.assertSimpleTest(() -> {
			assertThatThrownBy(() -> Validation.validateCarNames(carNames))
				.isInstanceOf(IllegalArgumentException.class)
				.hasMessage(ErrorMessage.SPACE_BETWEEN_NAME.getMessage());
		});
	}

	@ParameterizedTest
	@ValueSource(strings = {"pobi,", "woni,", "jun,"})
	void 자동차가_한대일_경우_예외(String carNames) {
		Assertions.assertSimpleTest(() -> {
			assertThatThrownBy(() -> Validation.validateCarNames(carNames))
				.isInstanceOf(IllegalArgumentException.class)
				.hasMessage(ErrorMessage.SINGLE_CAR_NAME.getMessage());
		});
	}

	@ParameterizedTest
	@ValueSource(strings = {"pobiii,woni,jun", "javajigi,jun", "woowacourse,woni"})
	void 자동차_이름이_5자_초과일_경우_예외(String carNames) {
		Assertions.assertSimpleTest(() -> {
			assertThatThrownBy(() -> Validation.validateCarNames(carNames))
				.isInstanceOf(IllegalArgumentException.class)
				.hasMessage(ErrorMessage.OVER_CAR_NAME_LENGTH.getMessage());
		});
	}

	@ParameterizedTest
	@ValueSource(strings = {"pobi,pobi,jun", "woni,woni,pobi", "jun,jun,pobi"})
	void 자동차_이름이_중복되었을_경우_예외(String carNames) {
		Assertions.assertSimpleTest(() -> {
			assertThatThrownBy(() -> Validation.validateCarNames(carNames))
				.isInstanceOf(IllegalArgumentException.class)
				.hasMessage(ErrorMessage.DUPLICATE_CAR_NAME.getMessage());
		});
	}

	@ParameterizedTest
	@ValueSource(ints = {4, 5, 6, 7, 8, 9})
	void 무작위값이_4_이상일_경우_전진_O(int randomNumber) {
		Assertions.assertSimpleTest(() -> {
			Car car = new Car("pobi,woni");
			car.move(randomNumber);
			assertThat(car.getPosition()).isEqualTo(1);
		});
	}

	@ParameterizedTest
	@ValueSource(ints = {0, 1, 2, 3})
	void 무작위값이_4_미만일_경우_전진_X(int randomNumber) {
		Assertions.assertSimpleTest(() -> {
			Car car = new Car("pobi,woni");
			car.move(randomNumber);
			assertThat(car.getPosition()).isEqualTo(0);
		});
	}
}

 

(2) TDD란?

TDD(Test-Driven Development)는 테스트를 먼저 작성하고 그에 맞춰 개발을 진행하는 테스트 주도 개발 방법론이다. 해당 방법론은 코드가 의도한 대로 동작하는지 명확히 확인할 수 있도록 하며, 버그를 초기에 발견하고 코드 품질을 높이는 데 도움을 준다.

 

TDD는 코드의 안정성과 유지보수성을 높이는 방법이다.
- 이펙티브 자바/좋은 코드, 나쁜 코드 -

 

스터디에서 언급되었던 TDD를 구현해보려 했으나, 생각만큼 쉽지 않았다,, 테스트 코드를 위한 코드를 구현하다보니 머릿속이 복잡해, 간단한 요구사항조차 어렵게 느껴졌다. 기능 구현에 앞서 테스트코드를 작성하는 과정은 요구사항에 대한 명확한 이해도와 TDD에 대한 이해도가 필수적이라는 생각이 든다. 남은 프리코스 기간동안 TDD를 꼭 사용해보고 싶다.

 

4. 2주차를 마무리하며 🌱

 2주차를 마무리하며 회고록을 작성하다보니, 더 개선하고 싶은 부분들이 보여 아쉽다. 3주차 미션에서는 각 클래스가 담당하는 기능을 더 세부적으로 나누고 싶다. 또한, 모든 요구사항에 MVC 패턴이 적합하다고 할 수는 없다. 적합한 패턴을 찾는 확장된 시야를 가지기 위해 디자인패턴에 대해 더 공부해야할 것 같다. 미션 종료 직후, 프리코스 커뮤니티를 확인해보니 Validation 클래스를 따로 분리하지 않고 도메인에 구현한 분들도 있는 것 같았다. Validation을 수행하는 계층에 대해 한 번 더 생각해봐야할 것 같다. 프리코스를 참여하며 느끼는 것은 설계/구현/피드백 과정에서 특정 기술을 사용하는 타당성 또는 근거는 꼬리에 꼬리를 물듯 끝이 없다는 것이다. 더 많은 시간을 투자하고, 생각해봐야한다는 것을 느꼈다. 따라서 3주차부터는 미션을 수행하는데 소요된 시간을 매일 기록해보려 한다. 조금 더 의식적으로 임할 수 있지 않을까 싶다.

 

 1주차 피드백을 반영하고 관련 서적을 읽어보며, 코드를 개선해나가는 과정에서 피드백이 나에게 가장 중요하게 작용한다는 것을 깨달았다. 모르는 부분이 생길 때마다 백과사전처럼 책을 펼쳐보는 습관이 생겼다. 또한, 해당 기술을 왜 사용해야 하는지, 가독성/신뢰성/유지보수성/성능 측면에서 타당한 근거를 찾는 과정을 통해 나의 코드를 더 신뢰하게 된다.

 

 코드 리뷰를 하는 과정은 서로 좋은 코드를 구현할 수 있도록, 열띤 토론을 할 수 있어 더 즐겁게 느껴지는 것 같다. 특히 대면 스터디는 더 잦은 의사소통이 있기에 그 효과가 확실한 것 같다. 처음으로 외부에서 스터디를 참여하게 되었는데, 하길 정말 잘한 것 같다. 스터디원분께서 아이스브레이킹 시간도 준비해주셨는데 다른 분들이 겪었던 스토리를 들을 수 있어 흥미로웠다. 스터디원분들께 궁금한게 많은데, 더 친해지게 되면 마음껏 여쭤봐야겠다. 뿐만 아니라 나는 어떤 사람인지, 프리코스를 얼마나 진지하게 임하고 있는지, 나 자신을 되돌아보는 시간이 되었다. 홀로 학습하는 것보다, 프리코스를 진행하며 함께 성장하는 기분이 들어 이 시간들이 행복하다.

 

2주차 미션 PR 링크

https://github.com/woowacourse-precourse/java-racingcar-7/pull/1125

 

[자동차 경주] 이지현 미션 제출합니다. by Jihyun3478 · Pull Request #1125 · woowacourse-precourse/java-racingcar

자동차 경주 🚗💨 목차 기능 구현 목록 1주차 피드백 정리 2주차 목표 1. 기능 구현 목록 [자동차] 자동차 이름을 입력 받는 기능 [예외처리] 입력값이 비어있거나 null인 경우 [예외처리] 자동차

github.com

 

[참고 자료]

모던 자바 인 액션

 

이펙티브 자바

 

좋은 코드, 나쁜 코드

 

 

https://tecoble.techcourse.co.kr/post/2021-04-26-mvc/

 

웹 MVC 각 컴포넌트 역할

개발을 하다보면 여러 디자인 패턴을 마주하게 된다. 그 중 가장 자주 보는 디자인 패턴은 일 것이다. MVC 패턴의 각 컴포넌트(Model, View, Controller…

tecoble.techcourse.co.kr

 

https://tecoble.techcourse.co.kr/post/2021-05-23-stream-api-basic/

 

StreamAPI 나도 한 번 써보자!

Java 의 Stream API 사용 방법을 알아보자. 우아한테크코스 프리코스 과정에서 Stream API 를 사용해서 코드를 맛깔나게 구현하는 분들을 보면 괜스레 해야 할 것 같고, 유용해 보여서 흥미가 생긴다.

tecoble.techcourse.co.kr