Coderoad

우아한테크코스 레벨1 - 체스

2024-04-18 at 우아한테크코스 category

체스 - OOP & DB

대망의 레벨 1 마지막 미션, 체스 미션을 마쳤습니다. 살면서 체스를 한 번도 플레이하지 않아서 미션 시작 전에는 걱정이 많았습니다.(블랙잭 미션에서도 그랬지만...) 그래도 체스는 엄청 유명한 보드 게임이다보니 도메인 지식을 쌓는 것은 크게 어렵지 않았습니다. 기물의 움직임, 초기 보드의 상태, 체스의 승리 조건 등을 공부했고, 페어 종이와 함께 구현을 시작했습니다.

1, 2단계 - 체스판 초기화와 말 이동

이번 미션은 앞선 미션들과 다르게 총 4단계로 구성되어 있었습니다. 1, 2단계는 페어와 함께 진행하고 3, 4단계는 각자 진행하면 됐는데, 3, 4단계에 비해 1, 2단계가 훨씬 어려웠습니다. 체스의 대부분이라고 할 수 있는 체스판 세팅과 기물 이동 기능 구현을 1, 2단계에서 모두 끝내야했기 때문입니다. 사실상 페어와 함께 개발하는 3일 동안 거의 모든 기능을 구현해야 했던 것입니다.

그래도 1차 리뷰 요청 마감까지 매일매일 퇴근도 늦춰가면서 열심히 구현했습니다. 중간에 한번 코드를 완전 갈아엎기도 하고 종이와 디자인 패턴 도입을 가지고 열띤 토론을 하기도 했습니다. 장장 1시간에 가까운 토론 끝에 이번 미션에서는 커맨드 패턴, 전략 패턴상태 패턴을 도입해봤습니다. 특히, 상태 패턴은 여러 도메인에서 활용했습니다.

package domain.game.state;

public interface State {
    State start();

    State end();

    boolean isInit();

    boolean isStarted();

    boolean isEnded();

    boolean isNotEnded();
}
package domain.piece.state;

import domain.piece.info.Color;
import domain.piece.info.Direction;
import java.util.List;

public interface State {
    List<Direction> movableDirection(final Color color);
}
상태 패턴을 이용해 체스 게임과 기물의 상태를 추상화

이번 미션의 핵심 키워드 중 하나가 OOP였던 만큼, 객체의 역할과 책임을 제대로 부여하려고 꽤 많은 시간을 쏟았습니다. '체스 기물의 움직임은 누구의 책임인가?', '체스 보드가 해야하는가?', '체스 기물이 스스로 해야하는가?'와 같은 질문들 뿐만 아니라 '현재 이기고 있는 플레이어는 누구인가?', '게임의 시작과 종료는 누가 결정하는가?'와 같은 질문들을 끊임없이 던졌습니다.

그러다보니 설계가 너무 복잡해졌고 객체의 역할과 책임은 모호해지기 시작했습니다. 이 문제를 어떻게 해결해야 할지 고민하던 중, 다른 크루들이 디자인 패턴 도입을 고민하고 있는 것을 알게 됐습니다. 저와 페어도 스스로 구조를 고민하기보다 디자인 패턴들을 활용하는 것이 더 효율적으로 코드를 작성할 수 있는 방법이 될 수 있음을 깨달았습니다. 제가 맞닥뜨린 객체 설계에서의 문제들은 이미 디자인 패턴으로 해결된 것들이었습니다.

사실 이전 미션들에서는 디자인 패턴을 적용하는 것이 꺼려졌습니다. 뭔가 디자인 패턴을 적용하면 스스로 생각해낸 해결책이 아니라는 생각에 올바르지 않은 학습법 같았습니다.

그러나, 블랙잭 미션의 리뷰어 현구막의 피드백을 읽고 생각을 바꿨습니다.

design-pattern
디자인 패턴의 필요성에 대한 현구막의 생각

결국 디자인 패턴은 수많은 개발자들의 연구 끝에 탄생한 Best Practice라고 볼 수 있습니다. 훌륭한 OOP를 위해 획기적인 객체 설계 아이디어를 떠올리기 보다 이미 널리 알려진 문제의 해결책을 활용하는게 더 낫다는 생각을 하게 됐습니다. 제가 정말 기가 막히게 멋진 코드를 작성했다고 해서 모든 개발자가 그 코드를 한번에 이해하리라는 보장은 없습니다. 차라리 그렇다면 더 큰 문제를 해결하기 위해 이미 해결된 작은 문제의 해결책들을 활용하는 것이 다른 개발자에게도 좋고 저도 편할 것입니다.

그래서 커맨드 패턴, 상태 패턴, 전략 패턴을 도입했습니다. 각각 사용자의 명령어 입력에 따른 동작 수행의 책임, 체스 게임과 보드의 상태에 대한 책임, 체스 기물의 이동 방식에 대한 책임을 가집니다. 이렇게 한번 디자인 패턴을 적용해보니 미션 구현이 훨씬 편안해졌습니다.

물론 디자인 패턴을 깊게 학습해보진 못했기 때문에 객체 간의 의존성을 제대로 관리하지 못해서 이번 미션의 리뷰어인 아서에게 여러번 피드백을 받았습니다...

3, 4단계 - 승패 및 점수와 DB 적용

3, 4단계는 1, 2단계에 비하면 정말 쉽게 해결할 수 있었던 것 같습니다. JDBCConnection pool에 대해서 학습해야 해서 소요된 시간을 제외하면 훨씬 여유로웠습니다. 실제로, 게임 승패 처리와 점수 계산 로직을 구현하는데는 크게 어려움이 없었습니다. 오히려, JPAMyBatis를 사용하지 않고 처음으로 Java와 DB를 연결하다보니 DB쪽 코드에서 이런저런 시행착오가 많았습니다.

특히, DB에 대한 의존성이 생긴 코드들을 테스트하는 것은 처음이라 도무지 방법이 떠오르지 않았습니다. 거기다 어떻게든 작성한 테스트들은 일부는 성공하고 일부는 실패했습니다. 문제의 원인을 찾던 중, 이전 테스트 메소드가 다음 메소드에 영향을 준다는 것을 알게 됐습니다. 즉, 테스트 분리가 제대로 이루어지지 않아서 DB의 상태가 매 테스트에 영향을 준 것입니다.

이것을 해결하기 위해 여러 방법을 모색하다 Fake 객체를 통한 테스트 더블을 활용하기로 했습니다. @Transactional을 사용하거나 DB에 쿼리를 직접 날리는 방법도 있었겠지만 전자는 Spring Framework 없이는 사용할 수 없었고, DB에 쿼리를 날리는 방식은 결국 DB의 종류에 의존해야 한다는 문제가 있었습니다. 저는 DB가 테스트에 필요해진 것 자체가 통합 테스트지 단위 테스트의 범위가 아니라는 생각했습니다. 레벨 1에서 학습한 테스트는 단위 테스트가 전부인데, 통합 테스트를 하려해서 어려워진 것입니다.

dao-test
테스트 더블, Fake 객체를 활용하기로 한 이유

이렇게 생각한 이유를 PR 메시지에 적어서 3, 4단계를 제출했습니다.

test-double

아서도 공감해주셨다...

다행히 아서도 제 생각에 공감해주셔서 미션을 잘 마무리 할 수 있었습니다. 커넥션 풀을 직접 구현해보기도 하고 직접 구현한 커넥션 풀 때문에 테스트에서 문제가 발생하기도 했지만 잘 해결했습니다. 이번 미션은 디자인 패턴에 대한 생각과 Java가 DB를 다루는 방법에 대해 많이 배운 미션이었습니다.

공부할 개념들

  • JDBC
  • 테스트 더블
  • 디자인 패턴
  • 슬라이스 테스트
  • 컴파일 타임 의존성과 런타임 의존성
hangillee

Personal blog by hangillee.

Road to good developer.