영호

우아한테크코스 5기 프리코스 4주차 후기 본문

우아한테크코스5기

우아한테크코스 5기 프리코스 4주차 후기

0h0 2022. 11. 25. 15:22

개요

4주 간의 프리코스가 끝나면서 소감을 정리해보려고 합니다.

4주 간의 소감

프리코스를 진행하기 전 저는 객체지향에 대한 감이 없었습니다. 4주 동안 의식적인 연습을 통해 미션을 수행하면서 메서드를 분리하고, 클래스를 분리하고, 코드 컨벤션을 지키려고 노력했습니다. 그러다 보니 1주 차에 어색했던 부분이 2주 차에서 보다 능숙해지고 2주 차에 어색한 부분이 3주 차에는 보다 능숙해지는 경험을 할 수 있었습니다. 

 프리코스를 마치고 1, 2주 차의 코드를 보면 정말 리팩터링 할 요소가 너무 많이 보입니다. 그만큼 프리코스 기간 동안 성장한 거 같아 기쁘고, 앞으로도 다른 기수의 프리코스 내용을 연습하면서 계속해서 역량을 키워나갈 예정입니다.

 4주 동안 힘들기도 했지만, 많은 성장을 할 수 있어서 보람 있는 4주로 기억될 거 같습니다.

 

배운 점

프리코스를 통해 배운 점이 너무 많습니다.

2가지 경로를 통해 역량을 키울 수 있었는데, 하나는 미션 내용으로 주어지는 코드 컨벤션과 다양한 클린 코드 원칙이 있습니다. 다른 하나는 프리코스 커뮤니티에서 진행되는 피어 리뷰입니다.

가독성 있는 코드

우선 프리코스를 진행하면서 지켜야 하는 다양한 클린 코드 원칙, 자바 컨벤션 등을 통해 가독성 있는 코드 작성에 대한 능력을 키울 수 있었습니다.

 

테스트 코드

일단 기본적으로 단위 테스트를 작성하면서 어떻게 해야 테스트하기 좋은 코드를 작성하는지에 대한 감을 잡을 수 있었습니다.

  • 테스트하기 어려운 코드
    • 테스트하기 어려운 random, 사용자 입력 값 등 개발자가 관리할 수 없이 항상 다른 값에 대해서 해당 부분을 메서드의 parameter로 받게 함으로써 테스트하기 유용한 코드를 작성할 수 있었습니다.
  • 테스트 코드도 코드다
    • 3주 차 공통 피드백에 제시된 내용입니다. 테스트 코드도 코드기 때문에 지속적인 리팩터링을 통해 중복 제거가 필요합니다. 
    • 그동안 저는 3주 차 미션에서 제 테스트 코드는 정말 많은 중복이 있었습니다.
    • 아래 내용을 보면 동일한 코드에 parameter만 "1000 ", "1100", "900"으로 달라집니다.

  
@Nested
@DisplayName("로또 구입 금액 유효성 예외테스트")
class PaymentLottoMoneyExceptionTest {
@Test
@DisplayName("로또 구입 금액에 숫자가 아닌 것이 있으면 예외 발생")
void hasNotNumberExceptionTest() {
IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class,
() -> new PaymentLottoMoney("1000 "));
assertThat(exception.getMessage()).isEqualTo(ExceptionMessages.PAYMENT_ONLY_NUMBER);
}
@Test
@DisplayName("로또구입금액이 천원 단위가 아니면 예외발생")
void notThousandUnitExceptionTest() {
IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class,
() -> new PaymentLottoMoney("1100"));
assertThat(exception.getMessage()).isEqualTo(ExceptionMessages.PAYMENT_ONLY_ONE_THOUSAND_UNIT);
}
@Test
@DisplayName("로또구입금액이 천원 미만이면 예외발생")
void lessThanThousandExceptionTest() {
IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class,
() -> new PaymentLottoMoney("900"));
assertThat(exception.getMessage()).isEqualTo(ExceptionMessages.PAYMENT_ONLY_ONE_THOUSAND_UNIT);
}

이 같은 로직은 중복 제거가 필요합니다 그래서 저는 4주 차에 아래와 같이 중복을 제거한 테스트 코드를 작성할 수 있었습니다.


  
@ParameterizedTest
@CsvSource(value = {"0:U", "1:D", "2:D", "3:U"}, delimiter = ':')
@DisplayName("입력과 다리의 해당 칸 값이 일치한다면 true")
void isPassStepTest(int index, String moving) {
assertThat(bridge.isPassStep(index, moving)).isEqualTo(true);
}

디자인 패턴

4주차 미션을 진행하면서 InputView, OutputView의 경우 멀티 쓰레드 환경이 아니기 때문에 하나의 인스턴스만 있어도 되지 않을까?라는 생각을 했습니다. 그래서 저는 그동안 이론으로만 알고 있던 싱글톤 패턴을 적용해보려고 시도했습니다.


  
public class InputView {
private static InputView inputview = new InputView();
private InputView() {
}
public static InputView getInputView() {
if (inputview == null) {
inputview = new InputView();
}
return inputview;
}
}

  
public class InputService {
private static InputService inputService = new InputService();
private InputService() {
}
public static InputService getInputService() {
if (inputService == null) {
inputService = new InputService();
}
return inputService;
}
public int inputBridgeSize() {
while (true) {
try {
return InputView.getInputView().readBridgeSize();
} catch (IllegalArgumentException exception) {
OutputView.getOutputView().printErrorMessage(ExceptionMessage.BRIDGE_RANGE.getMessage());
}
}
}
public String inputPlayerMoving() {
while (true) {
try {
return InputView.getInputView().readMoving();
} catch (IllegalArgumentException exception) {
OutputView.getOutputView().printErrorMessage(ExceptionMessage.MOVE.getMessage());
}
}
}
public String inputRetryCommand() {
while (true) {
try {
return InputView.getInputView().readGameCommand();
} catch (IllegalArgumentException exception) {
OutputView.getOutputView().printErrorMessage(ExceptionMessage.RETRY_INPUT.getMessage());
}
}
}
}

디자인 패턴의 필요성 공감

view분리 중 미션을 진행하며 input, output객체들은 하나만 있으면 된다고 판단했습니다. 그래서 평소에 대략적인 이론만 알고있던 싱글톤 패턴을 더 찾아보면서 적용해봤습니다. 사실 디자인 패턴들에 대해 배울 때 “저런 이유 때문에 필요할 수 있겠구나”정도로 생각 했습니다. 그러나 미션을 진행하면서 “이런 부분을 해결하기 위해서 해당 패턴이 생겨났구나“라는 공감 했습니다.

객체의 생성과 분리

4주차 미션을 진행하면서, controller는 객체를 생성하지 않고 사용만 하고 싶었습니다. “오브젝트”라는 책을 읽으면서 객체의 생성과 분리를 읽고 보니, 저는 하나의 controller에서 객체를 생성하고, 메시지를 보내고 있었습니다. 그래서 저는 inputView, OutputView를 싱글톤으로 변경 후 인스턴스를 가져오는 형식으로 변경했습니다. 비록 게임 결과 인스턴스를 생성하는 부분도 분리하고 싶었지만, 성공하지 못한 점이 아쉽게 남았습니다.

작은 클래스 유지

클래스를 작게 유지하려고 노력했습니다. controller에서 사용자의 입력을 받고 유효한 입력이 들어올 때 까지 반복하는 로직이 들어가있었습니다. 이렇게 하다 보니 controller의 크기가 너무 커졌다는 느낌을 받았습니다. 그래서 저는 InputService를 통해 유효한 입력이 들어올 때 까지 입력받는 책임을 분담 시키는 과정을 통해 작은 클래스를 유지하려고 노력했습니다.

리팩토링의 즐거움

 계속해서 제 코드를 살펴보면서 고칠 부분을 찾고, 이를 고치기 위해 구글링을 하다 보니 새로운 내용을 알게되고, 또 이를 바탕으로 고칠 점을 찾는 과정이 너무 재밌었습니다. 코드에 정답은 없기에 아직도 제 코드에는 리팩토링 할 요소가 있을 것이라 생각됩니다. 이번 프리코스를 통해 리팩토링이 재밌게 다가왔습니다.

문서화

기본적으로 프리코스를 진행하면서 기능 요구사항 목록을 정리해야 된다는 요구사항이 있습니다. 처음에는 이 부분이 어색하게 다가왔습니다. 그러나 미션을 반복하면서 요구사항을 정리하고 이를 기반으로 구현하고 기능별로 커밋하다 보니 구현 중 길을 잃더라도 다시 지금 제가 무엇을 해야 하는지 리마인드 하면서 길을 찾을 수 있었습니다.

 또한 살아있는 문서를 위해 한 번 작성된 기능 목록에서 추가적으로 구현해야 하는 기능이 생기면 계속해서 문서를 수정하면서 최종적으로 제 코드와 일치하는 문서를 작성하기 위해 노력하면서 프리코스를 진행하면서 문서화의 장점을 체감했습니다..

 

캡슐화

 프리코스를 진행하면서 객체 지향과 관련된 책을 사서 읽고 많은 내용을 찾아봤습니다. 그 결과, 제 생각에는 캡슐화를 지키는 것이 좋은 객체지향의 근간이 된다는 생각이 들었습니다. 저는 그동안 요구사항을 만족하기 위한 데이터들은 어떤 것이 있을지에 대해 먼저 생각을 했습니다.

 하지만, 이번 기간 동안 공부를 하다 보니 데이터 중심이 아닌 책임을 중심으로 생각해야 된다는 사실을 알게 됐습니다. 그래서 요구사항을 충족하기 위한 기능 목록을 정리하고 해당 기능을 수행하는 객체가 무엇인지 생각하고, 해당 기능을 수행하기 위한 데이터를 생각하며 구현했습니다.

 그리고, 메서드는 최대한 추상화하여 public으로 공개하고 세부적인 행동은 private으로 감싸려고 고민했습니다. 이렇게 코드를 작성하니 내부의 세부 구현 방식이 변경되더라고 변경으로 인한 영향이 해당 객체 밖으로 번지지 않아서 리팩터링이 보다 수월해짐을 느낄 수 있었습니다.


  
public void startGame() {
BridgeGameResult bridgeGameResult = new BridgeGameResult(bridgeSize);
while (gameProgress && !isAllAnswer) {
bridgeGameResult.clearResult();
gameTryCount++;
progressOneLife(bridgeGameResult);
}
showFinalResult(bridgeGameResult.getFinalResult(isAllAnswer, gameTryCount));
}
private void progressOneLife(BridgeGameResult bridgeGameResult) {
int moveIndex = 0;
boolean isPlay = true;
while (isPlay && !isAllAnswer) {
isPlay = progressPlayerMove(moveIndex++, bridgeGameResult);
isAllAnswer = bridgeGameResult.isGameSuccess();
}
}
private boolean progressPlayerMove(int moveIndex, BridgeGameResult bridgeGameResult) {
boolean isPossibleMove = bridgeGame.move(moveIndex, InputService.getInputService().inputPlayerMoving(), bridgeGameResult);
OutputView.getOutputView().printMap(bridgeGameResult.getCurrentResult());
if (!isPossibleMove) {
askReplay();
}
return isPossibleMove;
}
private void askReplay() {
String gameCommand = InputService.getInputService().inputRetryCommand();
if (bridgeGame.retry(gameCommand)) {
return;
}
gameProgress = false;
}
private void showFinalResult(StringBuffer result) {
OutputView.getOutputView().printResult(result);
}

4주 차 구현 내용 중 일부입니다. 게임을 시작하고 싶으면 외부에서는 해당 객체 인스턴스의 startGame만 호출하면 "나머지 세부 동작은 해당 인스턴스에서 알아서 처리해줄 거야"라고 생각합니다. 그래서 startGame의 parameter가 추가되는 등의 변경이 아닌 내부 세부 동작 방식이 바뀌더라도 외부에 영향이 갈 일이 없습니다.

클래스 분리

클래스를 식별할 때 책임을 수행할 클래스를 식별하기 위해 노력했습니다. 그리고 이렇게 식별된 객체들의 협력을 통해 요구사항을 만족시킬 수 있도록 노력했습니다. 이런 방식을 통해 객체를 식별하다 보니 제 생각엔 자율적인 객체들이 완성됐습니다. 

 아직 부족한 실력이지만 리팩터링을 통해 계속해서 클래스를 분리하는 과정을 통해 앞으로도 역량을 키울 수 있는 원동력을 얻어갈 수 있었습니다.

일급 컬렉션

이 부분도 미션을 진행하면서 새롭게 알게 된 지식입니다. 일급 컬렉션이란 하나의 컬렉션을 인스턴스 변수로 가지는 객체를 말합니다. 자세한 설명은 다른 블로그의 글을 첨부하겠습니다.

 저는 4주 차 미션에서 게임 결과를 UP, DOWN각각을 별도의 List로 관리하는 방식을 택했습니다. 생각해보면 UP, DOWN의 List는 다리 건너기 게임 도메인에 종속적인 자료구조라는 생각을 했습니다. 그래서 저는 각각을 별도의 BridgeStair라는 객체로 분리해서 사용했습니다. 

 사실 BridgeStair가 좋은 네이밍은 아니라고 생각하지만, 단순히 List <String>보다는 BridgeStair타입으로 생성되는 것이 더 명확하다고 생각합니다.

메시지를 보내라

이 부분은 프리코스 기간 동안 지키려고 노력한 부분입니다.

 해당 내용은 공통 피드백에도 있었고, 객체지향 관련 서적과 다양한 글에서도 나오는 내용입니다. 예를 들어, 게임 성공 여부를 판단할 때 게임 결과를 알고 있는 객체에게 getter를 사용하는 것이 아닌 isGameSuccess와 같은 public메서드를 통해 성공 했는지 여부를 알려달라는 메시지를 보내는 것입니다.

 제가 생각한 메시지를 보내서 얻는 이점은 캡슐화에 가까워 진다는 것입니다. 데이터와 관련 책임을 한 곳에 모으기 위해서는 getter가 아닌 해당 객체에서 데이터를 가지고 처리하고, 다른 객체에게는 단순히 추상화된 메서드를 통해 내부 동작을 숨길 수 있기 때문입니다.


  
public class BridgeStair{
public boolean isGameSuccess() {
return currentResult.size() == bridgeSize
&& currentResult.get(currentResult.size() - 1).equals(BridgeGameResultStatus.CORRECT);
}
}

한가지만 하자

기능 구현을 위해 코드를 작성하다 보면 분명히 다른 부분에서 리팩터링 할 부분이 보이곤 합니다. 저는 이 경우 바로 리팩터링을 하곤 했습니다. 그러나 이렇게 하다 보면 기능 별 커밋이 어려워질 뿐만 아니라, 코드가 꼬이는 경험을 했습니다.

 그래서 4주차 미션을 진행할 때는 리팩터링 할 요소가 보이면 문서에 기록을 한 뒤, 작성하던 기능 구현을 마치고 커밋한 뒤 구현을 하려고 노력했습니다.

 이렇게 하니 마지막 미션을 그동안의 미션과 다르게 짧은 시간을 투자할 수 밖에 없었지만, 중간에 길을 잃어 허비하는 시간을 줄일 수 있었습니다.

 

피어 리뷰

이번 우 테코 5기부터 프리코스 커뮤니티가 생기면서 다양한 활동이 있었지만, 저는 그중에서 피어 리뷰에 참여하면서 얻어가는 것이 많았습니다.

 다른 분들이 제 코드에 대해 더 나은 방법을 제시해주시고, 또 제 코드에 대해 물어보시면서 다양한 생각을 할 수 있었습니다. 그리고 저 역시 다른 분들의 코드를 리뷰하면서 제가 생각한 방법보다 더 좋은 해결방법을 알 수 있었습니다. 또한, EnumMap, getOrDefault, 문자열 처리, StringJoiner 등 다양한 지식도 얻을 수 있는 경험이었습니다.

enum에서의 문자열 처리

예외상황에 대한 문자열 처리를 할 때 저는 문자열 전체를 상수로 관리했습니다. 그러나 다른 분들의 코드를 보니 문자열 포매팅을 이용해 처리하는 방식이 더 좋아 보여 4주 차 미션에서 아래와 같이 적용해봤습니다.


  
public enum ExceptionMessage {
BRIDGE_RANGE(" %d ~ %d 사이의 숫자를 입력해야 합니다.",
BridgeSizeRange.MIN_BRIDGE_SIZE.getBridgeSize(),
BridgeSizeRange.MAX_BRIDGE_SIZE.getBridgeSize()),
MOVE(" 이동 입력은 대문자 %s 혹은 대문자 %s 만 가능합니다.",
BridgeStep.UP.getStep(),
BridgeStep.DOWN.getStep()
),
RETRY_INPUT(" 재시작 입력은 대문자 %s 혹은 대문자 %s 만 가능합니다.",
BridgeGameCommand.RETRY.getCommand(),
BridgeGameCommand.QUIT.getCommand());
private final String message;
ExceptionMessage(String message, Object... replacers) {
this.message = String.format(message, replacers);
}
public String getMessage() {
String baseErrorMessage = "[ERROR]";
return baseErrorMessage + message;
}
}

StringJoiner를 통한 반복 제거

출력 형식을 맞추기 위해 저는 기존에 StringBuffer.append를 계속해서 사용했습니다. 그러나 다른 분이 제 코드를 리뷰해주시면서 StringJoiner에 대해 언급해주셨습니다. 4주 차 미션을 진행하면서 결과를 출력할 때, 해당 내용을 적용할 수 있는 부분이 있어서 아래와 같이 적용해봤습니다.


  
public StringJoiner getCurrentResult() {
StringJoiner bridgeResultMessage = new StringJoiner(GameConstants.resultDelimiter, GameConstants.resultPrefix, GameConstants.resultPostfix);
for (BridgeGameResultStatus result : currentResult) {
bridgeResultMessage.add(result.getResultStatus());
}
return bridgeResultMessage;
}

 


  
public class GameConstants {
public static final String startGameMessage = "다리 건너기 게임을 시작합니다.\n";
public static final String finalResultMessage = "최종 게임 결과\n";
public static final String resultDelimiter = " | ";
public static final String resultPrefix = "[ ";
public static final String resultPostfix = " ]";
private GameConstants() {
}
}

다양한 의견 공유

미션을 진행하면서 최대한 메서드 분리, 클래스 분리하기 위해 노력했습니다. 그러나 다른 분들이 제 코드에 리뷰해주시는 부분을 보면 분리를 할 수 있는 부분들이 있었습니다.

 그리고 제 코드에 대해 의견을 물어보시는 분들도 있었고, 저도 다른 분들의 코드를 리뷰하면서 궁금한 부분에 대해 물어보면서 서로 다양한 의견을 주고받을 수 있었습니다. 다른 분들이 제 코드에 대해 피드백해주시는 것도 많은 도움이 됐지만, 제가 다른 분들의 코드를 보면서 배운 점도 매우 많았습니다.

 이 과정을 통해 같이 시너지를 내며 성장하는 경험을 할 수 있었습니다.

 

Comments