Skip to content

[🚀 사이클2 - 미션 (기물 확장 + DB 적용)] 이프 미션 제출합니다.#347

Open
jihwankim128 wants to merge 32 commits intowoowacourse:jihwankim128from
jihwankim128:step2
Open

[🚀 사이클2 - 미션 (기물 확장 + DB 적용)] 이프 미션 제출합니다.#347
jihwankim128 wants to merge 32 commits intowoowacourse:jihwankim128from
jihwankim128:step2

Conversation

@jihwankim128
Copy link
Copy Markdown

체크 리스트

  • 미션의 필수 요구사항을 모두 구현했나요?
  • Gradle test를 실행했을 때, 모든 테스트가 정상적으로 통과했나요?
  • 애플리케이션이 정상적으로 실행되나요?

어떤 부분에 집중하여 리뷰해야 할까요?

안녕하세요. 베루스 정말 늦었습니다 ㅎㅎ..
cycle1에서 부진했던 탓에 cycle2 를 너무 늦게 진행했네요..

저는 cycle1에서 빠른 PR을 목표로 진행하다가 오히려 다시 보니 이해하기 힘든 코드가 됐습니다...
그래서 이번 cycle2에서는 제가 해볼 수 있는 만큼 분리와 책임에 집중해봤습니다.

이어서 변경 사항과 함께 피드백 바라는 부분 작성하겠습니다.

1. TransactionTemplate과 Proxy 패턴을 통한 관심사 분리

비즈니스 로직에 DB 기술인 트랜잭션 코드가 침범하는 것을 방지하기 위해 JanggiTxService를 도입했습니다.
매번 Connection을 생성하는 방식은 2가지 문제가 있었습니다.

  1. 장기판의 상태가 변경되었을 때, 기물 위치는 변경되지 않는 케이스
  2. 두 변경은 같은 작업 단위인데, 매 번 다른 커넥션을 사용한다.

그래서 한 트랜잭션에서 Connection을 공유하고 공유된 Connection은 트랜잭션이 끝났을 때 반환하는 식으로 개선했습니다.

고민한 지점

트랜잭션은 부가 기능인데 이를 명시적으로 프록시 객체로 만든 것이 과한 설계는 아닐지, 혹은 서비스 계층의 순수성을 지키기 위한 합리적인 선택이었을지에 대한 의견이 궁금합니다.

2. 상태 패턴(State Pattern)을 이용한 장기 게임 흐름 제어

장기 게임의 복잡한 상태(진행 중, 빅장 합의, 종료 등)를 JanggiState 추상 클래스와 하위 상태 객체들로 관리했습니다.

Running, BigJang, Finished, BigJangDone 등의 상태를 정의하고, state.next(board)를 통해 상태 전이가 일어나도록 설계했습니다.
특히 '빅장'이라는 특수한 도메인 규칙을 상태로 녹여내어 JanggiGame 클래스의 많은 책임을 피하게 설계 했습니다.

고민한 지점

    public boolean canPlaying() {
        return state.canPlaying();
    }

    public boolean isBigJang() {
        return state.status() == GameStatus.BIG_JANG;
    }

    public boolean isBigJangDone() {
        return state.status() == GameStatus.BIG_JANG_DONE;
    }
  • state가 delegate 처럼 단순히 호출하는 구조로 바뀌었습니다. 그래서 괜히 depth만 늘어난게 아닌가 하는 고민도 됩니다...
  • 반면에 장기 상태는 장기에 종속된 것이기 때문에, 또 적절한 것 같습니다.

이렇게 양 측의 생각이 오가는데 베루스는 어떻게 생각하시나요?

3. 이전 리뷰

cycle1에서 해결되지 않은 두 리뷰에 대해 적용해봤습니다.

팀 정보를 Formation 객체 생성 시 생성자로 전달하면 메서드 실행 시 파라미터로 전달할 필요가 없을 것 같습니다~

  • TeamFormation으로 래핑했는데, 의도하신 바가 맞을까요?

Path를 추출하는 로직을 MoveStrategy로 구분하지 않고 개별 기물 클래스안에 넣는 것도 좋아보입니다. 지금은 이동을 위한 로직이 흩어져서 구현된 것 같아요. Cannon과 Chariot에 중복된 로직이 있을 수 있긴하지만 각 기물별로 관리한다면 중복이여도 괜찮을 것 같네요.

  • 이동 관련 로직을 어떻게 할지 고민했는데 abstract를 통해 궁성, 직선 장거리 기물을 구분했습니다.

더 나은 방법이 있을까요? (여기가 제일 어렵습니다..)

4. 그 외

그 외는 third party 관련 작업이라, 아직은 비즈니스 및 객체지향에 집중하고 싶습니다.
여유가 되신다면 그 외 사항에 대해서도 자세하게 리뷰해주시면 성장에 큰 도움이 될 것 같습니다!

이번 미션도 리뷰 잘부탁드리겠습니다! 🔥

* 교차점에서 수평/수직 이동 실패하는 문제 해결
Copy link
Copy Markdown

@verus-j verus-j left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 이프~
2단계 진행하시느라 고생했습니다. 개인적으로 장기 미션에 비해 코드의 구조가 복잡한 것 같습니다. 트랜잭션도 고려하면서 기술적으로 다양한 부분을 적용하려고 노력한 점이 보이나 원하는 기술을 적용하면서 더 심플하게 구성할 수 있을 것 같아요. 우선적으로는 불필요한 상속 구조를 제거해보면 좋을 것 같습니다. 관련해서 리뷰 남겨놨으니 확인부탁드립니다~

import model.GameStatus;
import model.JanggiGame;

public class InMemoryJanggiRepository implements JanggiRepository {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용하지 않는 클래스인데 추가한 이유가 있을까요?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아.. DB 접근 실패 시 서킷 브레이커 패턴처럼 InMemory Repository로 제공하기 위함이었습니다!
그리고 cycle 2 - 2단계에서 DB Layer를 추가한다는 것을 인지했기 때문에, InMemory DB로 확장 가능한 구조로 먼저 개선한 후 DB만 갈아끼우기 위해 InMemoryRepository를 사용했엇습니다.


public class ConnectionManager {

private static final ThreadLocal<Connection> THREAD_LOCAL_CONNECTION = new ThreadLocal<>();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ThreadLocal을 사용한 이유가 있을까요?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존에 매번 새로운 Connection으로 각 변경 상태를 저장했습니다.
여기서 큰 문제를 직접 관측했습니다.

Connection Auto Commit으로 인해 각 Insert 작업이 모두 독립적으로 발생합니다. 즉, 하나의 비즈니스 단위 내부에서 독립적인 커밋으로 원자성을 잃는 문제가 발견됐습니다. 예시로 기물 이동에서 장기 상태 변경 -> 기물 위치 변경이 있는 경우, 장기 상태만 변경되고 기물 위치가 변경되지 않는 문제가 있습니다.

그래서 Transaction의 중요성을 깨닫고 Transaction 로직을 도입했습니다.
이 과정에서 Transaction으로 묶인 하나의 비즈니스 로직에서 하나의 Connection으로 처리해야했고, Connection을 관리하기 위해 JdbcRepository를 수정했습니다.

하지만, Transaction은 비즈니스 단위라서 서비스 흐름에 묶다보니 Repository에서 커넥션을 가져올 방법이 없었습니다. 그래서 ThreadLocal에서 스레드 단위로 독립적인 Connection을 사용하도록 처리했습니다.

ThreadLocal의 문제점으로는 ThreadPool에서 관리중인 Thread에서 A처리와 B처리의 Connection이 공유 될 수 있다는 문제가 있습니다. 그래서 이를 해결하기 위해 트랜잭션이 끝나면 ThreadLocal에서 Remove하도록 명시했습니다.

import model.piece.Piece;
import repository.JanggiRepository;

public class JanggiQueryService {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query용과 Command 용을 분리할 필요가 있을까요? 그리고 대부분 QueryService의 로직은 불필요한 것 같습니다. Controller에서 JanggiGame을 사용하면 어떨까요?

import model.JanggiGame;
import model.Team;

public record JanggiResultDto(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dto가 필요할까요?

import model.board.TeamFormation;
import repository.db.TransactionTemplate;

public class JanggiTxService implements JanggiService {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

트랜잭션 적용을 위해 JanggiServiceImpl과 구분해야할까요? 장기 미션에서는 심플하게 트랜잭션을 적용한 JanggiService만 제공하는 것도 좋아보입니다.

import ui.view.InputView;
import ui.view.OutputView;

public abstract class JanggiController {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용자 입력에 따른 기능 제공으로 Controller를 분리한 건 좋았으나 상속을 사용하면서까지 구성이 필요한가 싶습니다. 상속 없이 분리하는 방법은 없을까요?

Comment on lines +38 to +43
private void play(Long janggiId) {
if (janggiQueryService.canPlaying(janggiId)) {
retry(() -> playByTurn(janggiId), OutputView::displayError);
}
OutputView.displayBoard(janggiQueryService.getBoardResponse(janggiId));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

janggiQueryService를 호출하는 로직이 모두 Game 조회를 위해서 DB에 매번 접근할 것 같은데 비효율적인 것 같네요. 어떻게 개선하면 좋을지 고민해보면 좋을 것 같아요~

}

private void checkBigJang(Long janggiId) {
if (!janggiQueryService.isBigJang(janggiId)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BigJang은 뭔가요?

import model.state.Finished;
import model.state.Running;

public abstract class JanggiState {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

너무 과하게 상속을 사용한 것 같습니다... 모든 구조에 상속을 필수적으로 넣은느낌인데 Piece 외의 객체들에서는 상속을 제거해보면 어떨까요? 지금 상속 구조 때문에 컨트롤러에서부터 기물 이동 관련 로직을 보는데까지 너무 복잡합니다. 불필요한 상속을 제거하고 흩어져 있는 로직들을 모아서 한번에 이해할 수 있도록 만들어보면 좋을 것 같아요

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants