티스토리 뷰

 

레벨 1의 두 번째 미션은 사다리 게임입니다. 이번 미션부터는 TDD로 진행해야 하는 요구사항이 추가되었습니다. TDD는 Test Driven Development의 줄임말로 테스트 주도 개발이라는 뜻을 가지고 있습니다. 이번 미션에서는 사다리 구조를 잡고 생성하는 것부터 사다리 게임을 실행하는 로직까지 전부 TDD로 진행해야 했습니다.

 

TDD를 하는 이유

- 디버깅 시간을 줄여준다.

- 동작하는 문서 역할을 한다.

- 변화에 대한 두려움을 줄여준다.

 

TDD 원칙

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

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

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

 

첫 TDD 소감

이번 미션을 진행하기 전까지 TDD는 한 번도 해본 적이 없어서 처음 설계를 하는 단계부터 시간이 많이 소요되었습니다. 기존에 구현하던 방식은 프로덕션 코드를 먼저 작성한 후 애플리케이션이 정상적으로 작동이 되면 도메인 로직에 대한 단위 테스트를 작성했는데, TDD를 진행하기 위해서는 튼튼한 설계가 바탕이 되어야 했고 테스트를 고려한 설계를 하는 것부터 어려운 점이 많았습니다.

 

TDD를 직접 경험해보니 TDD가 왜 호불호가 갈리는지 느낄 수 있었습니다. TDD를 진행하기 위해 안정된 설계를 하고, 그에 맞는 테스트 코드를 작성하며 도메인 로직을 추가합니다. 그러므로 놓치는 기능을 최소화하며 애플리케이션을 완성할 수 있었습니다. 또한 테스트 코드가 미리 작성되어 있어 도메인 로직에 문제가 발생한 경우 디버깅하는 비용을 줄일 수 있었습니다.

 

하지만 앞서 말한 것 처럼 안정된 설계를 하는 시간적 비용이 많이 들었고 애플리케이션의 전체 구조를 변경해야 하는 경우 전체 테스트까지 수정해야 하는 상황이 발생했습니다. 또한 충분한 도메인 지식이 없는 경우 상당한 시간이 소요될 것 같다는 생각이 들었습니다.


설계 피드백

 

어떤 방식으로 접근할 것인가? (Out -> In vs In -> Out)

 

Out -> In

- Ladder를 생성하는 부분과 생성한 Ladder를 실행하는 부분을 분리

- TDD 구현 시 특정 상태를 분리해 시작하는 것이 중요

- Ladder를 생성하는 부분은 무시하고, Ladder를 실행하는 부분에서 시작

- 테스트하기 어려움

- 기존 코드를 삭제하고 Line의 move()에서 다시 시작

- 도메인 지식을 쌓은 후 더 작은 단위로 분리할 객체는 없는지?

 

In -> Out

- 가장 작은 단위의 객체(In)부터 기능을 추가하면서 Out 방식으로 완성해 나간다

- 시작 단계에서 가장 작은 단위의 객체를 찾는 것이 쉽지 않다

 

 

애플리케이션 개발 단계에서 두 방식 중 하나의 방식만 사용되지 않습니다. 두 방식이 핑퐁처럼 주고 받으며 개발이 진행됩니다.

예를 들어 Out -> In으로 시작하는 경우 도메인 지식으로 설계할 수 있는 부분까지 설계합니다.

도메인 설계에서 Object Graph 중 가장 마지막 노드(의존성을 가지는 객체가 하나도 없는 객체)부터 구현을 시작하는 것이 TDD로 접근하기 수월합니다. 사다리 타기 미션에서는 Ladder나 Line이 될 수 있습니다.

In -> Out으로 시작하는 경우에는 도메인 지식을 잘 파악하고 있는 상태이기 때문에 사다리 게임에서 발판, 좌표, 방향등 작은 단위부터 테스트를 진행해나가며 설계할 수 있습니다.

 

 

미션 피드백

- 현재 애플리케이션에서 컨트롤러가 필요할까요?

 

첫 번째로 달린 리뷰가 컨트롤러를 없애면 어떨까요? 였습니다. 처음 해당 리뷰를 봤을 때는 굉장히 당황스러웠습니다. MVC 패턴을 이용해서 애플리케이션을 구현하는데 컨트롤러가 없으면 어떻게 하라는 거지..?!  스스로 이해가 되지 않아서 컨트롤러의 역할에 대해서 다시 한번 정의해 보고 리뷰어분께 정리된 생각을 말씀드렸습니다.

MVC 패턴을 적용한다는 생각에 사로잡혀 각각의 Layer에 대한 책임을 명확히 하지 않고, 도입하는 명확한 기준도 없이 사용하고 있다는 점을 깨달았습니다. 결론적으로 레벨 1의 목적인 도메인에 집중하지 못하고 있었습니다. 컨트롤러는 도메인이 많아지고 요청에 대해서 처리해야하는 것들이 구분되기 시작된다면 도입하는 것이 적절하다는 기준을 세웠고, 현재 사다리 게임에서는 컨트롤러의 로직을 메인 애플리케이션으로 이동시켰습니다.

 

 

- 단순히 값을 꺼내서 쓰는 객체가 도메인 객체일까요?

 

사다리 게임의 결과를 조회할 때 Player 별 Prize를 담기 위해서 Map<String, String>라는 타입의 변수를 설정했습니다. 또한 해당 변수를 원시 값으로 두지 않기 위해 GameResult라는 객체를 생성한 후 멤버 변수로 Map<String, String>를 설정했습니다. 하지만 리뷰어분께서 단순히 값을 꺼내서 쓰는 객체가 도메인 객체인지 생각해 보라는 피드백을 주셨습니다.

public class GameResult {

    private final Map<String, String> gameResult;

    public GameResult(Map<String, String> gameResult) {
        this.gameResult = gameResult;
    }

    public String findByName(String name) {
        if (!gameResult.containsKey(name)) {
            throw new IllegalArgumentException("이름과 일치하는 참가자가 존재하지 않습니다.");
        }
        return gameResult.get(name);
    }

    public boolean contains(String name) {
        return gameResult.containsKey(name);
    }

    public Map<String, String> getGameResult() {
        return gameResult;
    }
}

해당 도메인에는 값을 꺼내서 전달하는 로직만이 존재했습니다. 우리가 도메인에 View 로직을 담지 말자라고 강조 하고 있는데 정확히 View 로직만을 담고 있다는 생각이 들었습니다. 왜냐하면 참가자에 따른 결과를 보여주는 것은 View(Client)의 책임이라고 생각합니다. 서버는 전체 게임 결과만을 클라이언트로 전송하면 된다고 생각합니다. 그러므로 굳이 객체로 감싸지 않더라도 Map<String, String> 타입만 View 계층에 넘겨줘 처리할 수 있기 때문입니다.

 

 

- Getter를 어떤 객체까지 열어줘야 할지 고민이 됩니다.

 

예를들어 Players 객체에 Player가 있고 Player 객체에 name 필드가 있다고 하면, Players 중 한 명의 이름을 가져오고 싶은 경우

1. Players.getPlayer(index).getName() 으로 가져올 수 있는 방법이 있고,

2. Players.getName(index)를 이용해 가져올 수 있는 방법이 있습니다.

 

현재 사다리 게임에서는 Player에 name이라는 필드가 1개만 존재하기 때문에 크게 문제는 없다고 생각하지만 2번 방법을 이용해 Player의 값을 바로 가져오는 경우, 해당 도메인에 여러 필드가 추가된다면 Players 객체에도 늘어난 필드만큼 getter가 늘어난다고 생각합니다.

1번 방법을 쓰는 경우 참조하는 메서드가 길어질 수 있다는 생각이 들었습니다.

class Players {

	private List<Player> players;
	
	// 1번 방법
	public List<Player> getPlayers() {
		return players;
	}
	
	// 2번 방법
	public getPlayerName(int index) {
		return player.get(index).getName();
	}

	public getPlayerPhoneNumber(int index) {
		return player.get(index).getPhoneNumber();
	}

	public getPlayerAddress(int index) {
		return player.get(index).getAddress();
	}
}
class Player {

	private String name;
	private String phoneNumber;
	private String address;

	public String getName() {
		return name;
	}
	
	public String getPhoneNumber() {
		return phoneNumber;
	}

	public String getAddress() {
		return address;
	}
}

 

질문이 모호할 수도 있을 것 같아 이해하기 쉽도록 players.get(index)와 같은 예시를 작성했는데 실제로 이렇게 사용하지는 않습니다 😅

도메인 로직을 실행하며 원하는 값을 가져오기 위해 . 을 두 번 이상 사용해 접근하는 것이 가독성을 해친다는 느낌이 들어 질문을 남겼습니다. 이번 미션에서는 참가자 수(players.getPlayer().size())를 예로 들 수 있습니다. 리뷰어분과 이야기를 나누며 내린 결론은 자주 사용되는 부분이라면 한 번에 가져와도 되고, 자주 사용하지 않는 값이라면 get 해서 가져오는 것이 나을 것 같다입니다.

 

또한 해당 getter들이 view에서 쓰이는 것이라면 view에 players처럼 일급객체를 전달하는 것이 아닌 List<Player> 자체를 넘겨서 원하는 값만을 get 해서 사용하는 것이 나을 것 같습니다. 왜냐하면 도메인 로직이 아닌 출력용 로직이기 때문에 굳이 도메인 객체에 getter를 추가해 줄 필요가 없다고 생각합니다.

댓글