우아한테크코스/미션 기록

[레벨1] 체스 3, 4단계 - 승패 및 점수, DB 적용

d02 2023. 4. 14. 13:50

기능 요구사항

콘솔 UI에서 체스 게임을 할 수 있는 기능을 구현한다. 승패 및 점수 계산 기능을 구현하고, DB를 적용한다.

- 체스 게임은 상대편 King이 잡히는 경우 게임에서 진다.
- King이 잡혔을 때 게임을 종료해야 한다.
- 체스 게임은 현재 남아 있는 말에 대한 점수를 구할 수 있어야 한다.
- "status" 명령을 입력하면 각 진영의 점수를 출력하고 어느 진영이 이겼는지 결과를 볼 수 있어야 한다.

점수 계산 규칙
- 체스 프로그램에서 현재까지 남아 있는 말에 따라 점수를 계산할 수 있어야 한다.
- 각 말의 점수는 queen은 9점, rook은 5점, bishop은 3점, knight는 2.5점이다.
- pawn의 기본 점수는 1점이다. 하지만 같은 세로줄에 같은 색의 폰이 있는 경우 1점이 아닌 0.5점을 준다.
- king은 잡히는 경우 경기가 끝나기 때문에 점수가 없다.
- 한 번에 한 쪽의 점수만을 계산해야 한다.

필수 요구사항
- 애플리케이션을 재시작하더라도 이전에 하던 체스 게임을 다시 시작할 수 있어야 한다.
- DB를 적용할 때 도메인 객체의 변경을 최소화해야한다.
선택 요구사항
- 체스 게임방을 만들고 체스 게임방에 입장할 수 있는 기능을 추가한다.
- 사용자별로 체스 게임 기록을 관리할 수 있다.

 

구현하며 고민한 내용

DB 실행환경

이번 미션에서 처음으로 Docker를 사용해보았다.

(지금까지 이해한 바로는, Docker는 어플리케이션의 실행 환경을 격리하여,
실행 환경까지 쉽게 설정해두고 실행할 수 있도록 도와주는 플랫폼인 것 같다.)

내 컴퓨터가 아니라 리뷰어가 어플리케이션을 실행했을 때도 정상 작동해야 하므로,
DB 또한 Docker를 사용해 실행 환경을 로드할 때 초기화할 수 있도록 설정해보았다.

 

Dao 클래스 간 중복코드 개선

순수 JDBC를 사용하다보니 Dao 클래스, 메서드 간 중복 코드가 매우 많았다.

이에 대해 Spring에서는 JdbcTemplate을 사용한다는 것을 참고하여
비슷하게 사용할 수 있도록 별도 템플릿 클래스와 로우매퍼 인터페이스를 정의해 사용했다.

 

테스트 코드

이번 미션을 진행할 때 네오 강의에서 테스트 더블에 대해 배웠고, 마침 책 <좋은 코드, 나쁜 코드> 중에서도 테스트 더블 부분을 읽었다.

DB가 테스트에 영향을 받지 않게 하기 위해서,
체스보드 서비스 클래스를 테스트할 때 DB 대신 인스턴스 변수인 맵을 사용하는 Fake Dao를 주입하여 진행했다.

 

코드리뷰에서 배운 것들

체크 예외를 언체크 예외로 전환하기

JDBC 관련 코드에서 SQLException을 처리하는 부분에 대해 깊이 생각해보지 않았는데,

리뷰어님이 참고 링크와 함께 이를 언체크예외로 변환해서 다시 던지는 것을 제안해주셨다.
참고 링크 : 토비의 스프링 - 예외처리위 링크에서 이야기하는 것처럼

"어차피 복구가 불가능한 예외라면 가능한 한 빨리 런타임 예외로 포장해 던지게 해서

다른 계층의 메소드를 작성할 때 불필요한 throws 선언이 들어가지 않도록 해줘야 한다" 는 말에 동의했다.

이를 통해 정리한 나의 예외 처리 기준
예외가 전파되는 다음 클래스에게 예외 처리를 강제해야 할 때는 Checked Exception을 사용하는 게 좋다.
예외가 전파되는 다음 클래스가 해당 예외에 대해 알 필요가 없을 때는 Unchecked Exception 을 사용한다.

 

static import 사용의 장단점

의미가 명확한 enum에 static import 사용은 매우 유용하다.
이렇게 코드 가독성을 높일 수 있지만, 잘못 사용하면 오히려 가독성이 아주 떨어질 수도 있다.
자주 사용하는 클래스의, 이름만 보아도 어디에 속하는지 알 수 있는 정적 멤버를 사용하는 데에만 사용하자.
참고 링크 : Static import에 대한 관찰

 

템플릿 콜백 패턴

Spring jdbcTemplate을 참고하여 Dao의 중복로직에 대한 템플릿 클래스를 만들었는데,
리뷰어님이 이에 대해 "템플릿 콜백 패턴"을 잘 썼다고 해주셨다.
그래서 오히려 나도 모르게 패턴을 사용하고 있었다는 것을, jdbcTemplate이 템플릿 콜백 패턴이라는 것을 알게 되어 재밌었다.
간단하게 말하면 전략 패턴의 전략이 콜백으로 변경된 것이 템플릿 콜백 패턴인 듯하다. (아직 깊게 공부는 안해보았다)
(전략 패턴은 전략을 필드로 삽입, 템플릿 콜백 패턴은 전략을 매개변수로 전달받는다)
콜백 코드: 호출한 곳에서 실행되지 않고, 호출을 요청한 곳에서 실행되는 코드, 다른 코드의 인수로서 넘겨주는 실행 가능한 코드

 

부정 연산자 지양하기

부정 연산자를 사용하면, 코드를 읽을 때 boolean 결과를 한번 더 뒤집어야하므로 가독성이 떨어진다.
부정 연산자를 써야 한다면 차라리 반대 메서드를 하나 더 만들자.

팩토리 클래스로 Dao 의존성 주입 책임 위임

Service 클래스가 Dao를 외부로부터 주입받도록 구현하였더니, Controller에서부터 이를 알고 있어야 했다.

이에 대해 리뷰어님이 별도 팩토리 클래스를 사용하여 의존성을 조립하는 방식으로 책임을 분리하는 것을 제안해주셨다.
이렇게 하니 Controller가 Dao에 대한 불필요한 정보(어떤 구현체를 사용할지)를 알 필요가 없고,
의존성을 설정하는 로직을 분리할 수 있어 좋았다.
또, 이와 함께 추가적으로 구현체가 변경될 경우(다른 DB를 사용하는 등)
Factory 클래스의 내용을 쉽게 변경할 수 있도록 정적 팩터리 메서드를 추가했다.

public class ChessGameControllerFactory {

    private ChessGameControllerFactory() {
    }

    public static ChessGameController create() {
        return new ChessGameController(ChessGameServiceFactory.create());
    }

}

public class ChessGameServiceFactory {

    private ChessGameServiceFactory() {
    }

    public static ChessGameService create() {
        return new ChessGameService(ChessBoardService.ofJDBCDao());
    }

}

// 서비스 객체에 대한 정적 팩토리 메서드
public static ChessBoardService ofJDBCDao() {
    return new ChessBoardService(new JdbcBoardPiecesDao(), new JdbcBoardStatusesDao());
}

public static ChessBoardService ofInMemoryDao() {
    return new ChessBoardService(new InMemoryBoardPiecesDao(), new InMemoryBoardStatusesDao());
}

public static ChessBoardService of(BoardPiecesDao boardPiecesDao, BoardStatusesDao boardStatusesDao) {
    return new ChessBoardService(boardPiecesDao, boardStatusesDao);
}
코드리뷰 피드백:
크으.. 정적 팩터리 메서드까지 사용해주시다니 정말 좋네요 👍
도이도 이미 알고 구현하셨겠지만
Factory 클래스를 사용했을 때는 의존성 조립이 바깥에서 조립되는데만약 구현체가 변경되면 Factory 클래스를 변경해야하는 과정이 필요한데요(다른 개발자가 Factory 클래스를 인지하지 못할 수 도 있겠죠?..)
도이가 구현하신것 처럼 정적 팩터리 메서드를 사용한다면 해당 클래스 내부에서 의존성 조립까지 한 눈에 파악할 수 있어서 좋다고 생각합니다!!
반응형