티스토리 뷰

지난 회고 보기

[우아한 테크코스 5기] 프리코스 3주차 회고 (만만치 않은 객체지향)

이번 과정을 통해 제약사항들을 준수하며 코드를 작성하는 것은 굉장히 어렵다는 걸 느꼈습니다.

한 것

테스트 코드 리팩토링 학습

이번 주에는 부족했던 테스트 코드 리팩토링에 관해 공부하였습니다.

중복된 메소드 테스트 코드에 @Nested를 사용하여 계층구조로 작성하는 방법을 배우고 미션에 적용해보았습니다.

public class PlayerTest {

	@Test
	@DisplayName("플레이어의 죽음을 나타내는 상태 테스트")
	void 플레이어_상태_테스트() {
		//given
		Player player = new Player();
		assertThat(player.isAlive()).isTrue();
		//when
		player.die();
		//then
		assertThat(player.isDead()).isTrue();
	}

	@Test
	@DisplayName("플레이어 이동 후 위치 변화 테스트")
	void 플레이어_이동_테스트() {
	    //given
		Player player = new Player();
		int currentLocation = player.getCurrentLocation();
	    //when
		player.move();
		int moveLocation = player.getCurrentLocation();
	    //then
		assertThat(currentLocation).isNotEqualTo(moveLocation);
	}

	@Test
	@DisplayName("플레이어가 죽었을 때 이동하는 예외처리 테스트")
	void 플레이어_사망_이동_테스트() {
		//given
		Player player = new Player();
		//when
		player.die();
		//then
		Assertions.assertThatThrownBy(player::move)
			.isInstanceOf(IllegalStateException.class);
	}
}

리팩토링 적용 전) 테스트 코드

기존 리팩토링 전 코드에 다음과 문제점이 보여집니다.

  1. 플레이어 상태를 나타내는 테스트 : 하나의 메서드에서 두가지 의미를 가짐.
    1. 새로 태어났을 때 플레이어의 상태는 ALIVE이다.
    2. die() 메소드를 실행하면 플레이어의 상태는 DEAD이다.
  2. 플레이어 move 메서드 테스트 : move라는 메서드를 두가지 테스트로 나눔
    1. 플레이어가 살아있는 경우 (정상적인 경우)
    2. 플레이어가 죽은 경우 (비정상적인 경우)
@SuppressWarnings("NonAsciiCharacters")
@DisplayName("Player 클래스")
public class PlayerTest {

	@Nested
	@DisplayName("플레이어의 상태는")
	class Describe_status {

		//given
		Player player = new Player();

		@Test
		@DisplayName("처음 생성되었을 때는 ALIVE 이다.")
		void player_alive() {
			assertThat(player.isAlive()).isTrue();
		}

		@Test
		@DisplayName("죽은 후에는 DEAD 이다.")
		void player_die() {
			//when
			player.die();
			//then
			assertThat(player.isDead()).isTrue();
		}
	}

	@Nested
	@DisplayName("move 메소드는")
	class Describe_move {

		Player player = new Player();

		@Test
		@DisplayName("이동 후 위치가 변화한다.")
		void 플레이어_이동_테스트() {
			int currentLocation = player.getCurrentLocation();
			//when
			player.move();
			int moveLocation = player.getCurrentLocation();
			//then
			assertThat(currentLocation).isNotEqualTo(moveLocation);
		}

		@Test
		@DisplayName("죽었을 때 이동하면 예외가 발생한다.")
		void 플레이어_사망_이동_테스트() {
			//when
			player.die();
			//then
			Assertions.assertThatThrownBy(player::move)
				.isInstanceOf(IllegalStateException.class);
		}
	}
}

리팩토링 적용 후) 테스트 코드

위와 같이 리팩토링을 적용한 테스트는 다음과  같은 이점을 갖습니다.

테스트할 하나의 주제(메서드)에 대해 여러 경우의 수를 나눠서 테스트할 수 있다.

 

테스트 결과

위와 같이 결과 또한 계층형으로 알아보기 쉬운 테스트 결과를 가집니다.

참고자료:

JUnit5로 계층 구조의 테스트 코드 작성하기

 

 

미션 과정

4주 차 미션은 "다리 건너기" 구현이였습니다. 

다리건너기 게임

이번 미션에서 추가된 프로그래밍 요구사항인 함수 길이 제한 10라인, 제공된 클래스의 제한사항을 준수하며 코드를 작성하는 게 어려웠습니다.

이번 주 차에서는 클래스(객체)를 분리하는 연습, 리팩터링 연습을 목표로 하고 있습니다.

 

클래스 분리 

3주 차와 마찬가지로 MVC 패턴을 사용하여 domain, service, view, controller 4가지 패키지 구조를 사용하였습니다.

 

domain 패키지는 Bridge, Player, GameRecord 3개의 클래스로 구성하였습니다.

  • Bridge 클래스는 위, 아래 건널 수 있는 판을 가집니다. 
  • Player 클래스는 상태(생존, 사망)를 가지며 이동하면 위치가 바뀌는 기능을 가집니다.
  • GameRecord 클래스는 게임을 기록하고 게임의 상황을 알아볼 수 있는 기능을 제공합니다.

 

service 패키지는 BridgeGame 하나로 구성하였습니다.

  • BridgeGame 클래스는 플레이어를 이동시키고 게임을 다시 시작할 수 있으며 현재까지의 게임의 진행 상황을 알아보는 역할을 합니다.

 

view 패키지는 InputView, OutputView 2개의 클래스로 구성하였습니다.

  • InputView 클래스는 사용자에게 필요한 입력을 받고 입력된 값을 검증하는 역할을 합니다.
  • OutputView 클래스는 게임의 진행상황과 결과를 출력하는 역할을 합니다.

 

controller 패키지는 MainController 하나로 구성하였습니다.

  • MainController 클래스는 입력을 바탕으로 게임을 진행하고 진행상황을 출력하는 역할을 합니다.

 

리팩터링


“BridgeMaker” 클래스에는 메소드를 추가해도 된다는 조건이 없어 ‘makeBridge’ 메소드를 10라인으로 맞추기 위해 많이 고민하였습니다.

 

public List<String> makeBridge(int size) {
    List<String> result = new ArrayList<>();
    for (int i = 0; i < size; i++) {
        result.add(getUpOrDown());
    }
    return result;
}

private String getUpOrDown() {
    if (bridgeNumberGenerator.generate() == 0) {
        return Constants.DOWN;
    }
    return Constants.UP;
}

리팩토링 전) 다리만들기 메소드

이처럼 처음에는 메소드를 분리해서 사용했지만, 요구 사항에 메소드를 추가해도 된다는 조건이 없어 아래와 같이 'getUpOrDown' 메소드를 제거하고 

Map 자료구조를 사용하여 코드를 합쳤습니다.

public List<String> makeBridge(int size) {
   List<String> result = new ArrayList<>();
   Map<Integer, String> mapToMoving = Map.of(0, Constants.DOWN, 1, Constants.UP);
   for (int i = 0; i < size; i++) {
      result.add(mapToMoving.get(bridgeNumberGenerator.generate()));
   }
   return result;
}

리팩토링 후) 다리만들기 메소드

 

 

또한, 다리 건너기 결과를 출력하는 메소드를 작성하는 과정에서 2가지 방법을 고민했습니다.
1.  “GameRecord”라는 클래스에서 toString 메소드를 오버 라이드해서 다리 건너기 결과 양식에 해당하는 값을 반환하고 “OutputView”에서 출력만 담당한다.
2. “GameRecord”의 필드를 Getter를 사용해 “OutputView”에서 다리 건너기 결과에 해당하는 양식을 만들어서 출력한다.

1번을 고민한 이유는 Getter를 사용해서 필드를 노출하고 싶지 않았고 다리 건너기 결과 양식이 “GameRecord”라는 클래스를 표현할 수 있다고 생각했습니다.

그러나, 최종적으로 내린 판단은 출력 양식을 도메인 클래스에서 갖는 게 단일 책임의 원칙에 위반된다고 생각하여 2번 방법을 사용하였습니다.

public void printMap(GameRecord gameRecord) {
    System.out.println(logToString(gameRecord.getUpLog()) + "\n" + logToString(gameRecord.getDownLog()));
}

private String logToString(List<String> log) {
    String PREFIX = "[ ";
    String DELIMITER = " | ";
    String SUFFIX = " ]";
    return PREFIX + String.join(DELIMITER, log) + SUFFIX;
}

다리 건너기 결과를 출력하는 메소드


아쉬운 점

이번 주차 미션을 제출하기전 제약사항을 다시 한번 되돌아 보았습니다.

그 중 프로그래밍 요구 사항의 Java 코드 컨벤션에 Tab은 들여쓰기에 사용되지 않는다.

이런 문구가 있어 코드를 찾아보니, 미션 제공 코드에서는 들여쓰기를 스페이스바를 사용하였지만,

저는 이 부분을 인텔리제이를 통해 Tab 캐릭터를 사용하고 있었습니다...
(기존에는 Tab 크기를 스페이스바 4개 사이즈로만 하면 되는줄 알았습니다..)

 

컨밴션을 조금만 더 자세히 읽어봤으면 알아차렸을 부분을 뒤늦게 깨닫는게 아쉬운 점이 있습니다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함