[레벨1] 장기 미션 회고록 2 - 1단계 (추상 클래스, 합성)

2025. 4. 20. 13:22·우아한테크코스 7기/회고

 

이미 레벨2가 시작 됐지만 .. 시간이 더 지나면 다 까먹을 것 같아서, 잊어버리기 전에 레벨1 미션 회고를 얼른 마무리하려고 한다!

1단계

1단계 기능 요구사항은 장기판 초기화, 기물 이동이었다. 

장기 룰 참고: https://ko.wikipedia.org/wiki/%EC%9E%A5%EA%B8%B0#%EC%A0%90%EC%88%98%EC%A0%9C

 

Pull Request

https://github.com/woowacourse/java-janggi/pull/38

 

[1단계 - 보드 초기화, 기물 이동] 밍곰(박민선) 미션 제출합니다. by minSsan · Pull Request #38 · woowacou

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

github.com

 

추상 클래스와 합성 적용하기

1단계에서 집중했던 부분은 다음과 같다.

  • 추상 클래스를 적용하여, 각 기물의 타입을 움직임 형태에 따라 분류
    • 상, 마, 졸/병, 사, 왕 - 움직임 횟수가 정해진 기물
    • 차, 포 - 움직임 횟수에 제한이 없는 기물

기물 클래스 다이어그램

 

문제점 - 설계한 타입 계층에서 벗어난 구현 공유

PR에 남긴 관련 질문

 

1단계 코드에서 한 가지 걸리는 부분이 있었다.

 

장기 기물을 분류한 기준은 "어떤 방식으로 움직이느냐"기 때문에, 이와 관련된 구현만 공유하는 것이 자연스럽다. 하지만, 실제 코드에서는 이와 관련된 구현 뿐만 아니라 "장애물을 어떻게 통과할 것이냐"에 대한 구현도 공유하고 있었다.

 

이는 추상 클래스로 의도했던 설계에서 벗어난 것처럼 보였다. 특히, 단순히 중복 코드를 없애기 위해서 상속을 사용하고 있다는 생각이 들어서 개선의 필요성을 느꼈다.

 

해결책 - 장애물 판단 역할을 분리하고 합성하기

기존의 타입 계층 목적성을 유지한 채, 장애물 판단에 대한 중복 코드를 효율적으로 관리하기 위해서는 장애물 판단 역할을 별도의 클래스로 분리하는 것이 좋겠다는 생각이 들었다.

 

따라서 해당 책임을 담당할 HurdlePolicy라는 객체를 새로 정의하고, 합성을 적용하여 각 기물의 중복되는 책임(= 장애물 판단)을 분리했다. (관련 코멘트 - https://github.com/woowacourse/java-janggi/pull/38#discussion_r2009099905) 

  • 차와 포를 제외한 나머지 기물 - 이동 경로 상에 다른 기물이 존재하는 경우 이동할 수 없다. (= 5개의 기물에 중복되는 로직)
  • 차 - 다른 팀의 기물이 존재하는 위치까지 이동할 수 있다.
  • 포 - 다른 기물을 넘어서 이동할 수 있으며, 넘어선 이후에는 다른 팀의 기물이 존재하는 위치까지 이동할 수 있다. (단, 다른 포를 넘거나 잡을 수 없다)

왼쪽 - 차, 포를 제외한 나머지 기물 / 가운데 - 포 / 오른쪽 - 차

 

✚ 상태가 없는 HurdlePolicy?

public class UnpassableHurdlePolicy implements HurdlePolicy {
    // 필요한 정보를 모두 파라미터로 전달 받음
    @Override
    public List<JanggiPosition> pickDestinations(JanggiTeam team, List<Path> coordinates, JanggiPositions positions) {
        return coordinates.stream()
                .filter(path -> !existChessPiece(positions, path))
                .map(Path::getDestination)
                .filter(destination -> isAbleToCatch(team, destination, positions))
                .toList();
    }
    
    ...
}

 

HurdlePolicy를 분리하다보니 위와 같은 형태(필요 정보를 모두 파라미터로 넘겨받는 형태)를 띄게 되었다.

 

이렇게 상태가 없는 객체가 생길 때면, 혹시 더 좋은 방법이 있지 않을까 한번 점검하곤 한다. 그리고 여기서 떠올린 질문은 "파라미터로 넘긴 정보를 갖고있는 객체가 직접 처리할 수는 없을까?" 였다.

 

  1. JanggiTeam, List<Path>(= 후보 경로)를 가지고 있는 기물이 이를 직접 처리하게 한다?
    • 기물마다 구현 코드를 가질 것이다. 즉, 원래 목표했던 "합성을 통한 중복 코드 줄이기"를 달성할 수 없다.
    • 그리고 애초에 기물에서 장애물 판단 로직을 분리하기 위해 HurdlePolicy를 만든 것임

  2. JanggiPositions(= 모든 기물의 위치 정보)를 가지고 있는 보드가 이를 직접 처리하게 할 수 없을까?
    • 이렇게 되면 보드가 기물의 구현체가 무엇인지 검사하고, 그에 맞는 장애물 판단 로직을 수행해야 한다.
    • 즉, 기물을 추상화한 의미(= 보드는 각 기물이 무슨 기물인지(차, 포, 상, 마, ...) 신경쓰지 않아도 됨)가 퇴색된다.

 

두 케이스를 모두 고려해봤을 때, 상태가 없는 형태의 HurdlePolicy를 정의하면서 얻을 수 있는 이점이 더 크다고 판단했다.

 

HurdlePolicy 호출 책임을 기물에서 보드로 변경

실제로는 2단계에서 적용한 내용이지만, 2단계 포스팅 내용이 너무 길어질 것 같아 여기에 기록한다!

기존 코드 - 각 기물이 HurdlePolicy를 직접 호출

HurdlePolicy를 호출하기 위해서는 모든 기물의 위치 정보가 필요하다. 그리고 이 과정에서 보드가 기물에게 모든 기물의 위치 정보를 파라미터로 넘겨줘야 한다.

여기서 들었던 의문점은, "모든 기물의 위치 정보를 알고있는 객체가 직접 처리하게 할 수는 없을까?" 였다.

 

변경 코드 - 추상 메서드를 활용하여 HurdlePolicy를 외부에서 호출하도록

그리고 기물 추상 클래스를 아래와 같이 정의함으로써, HurdlePolicy 호출 책임을 외부로 옮길 수 있었다.

public abstract class JanggiChessPiece {

    private final JanggiTeam team;

    protected JanggiChessPiece(JanggiTeam team) {
        this.team = team;
    }
    
    ...

    // 기물 구현체가 자신에게 해당하는 장애물 정책을 반환하도록 강제
    abstract public HurdlePolicy getHurdlePolicy();

    ...
}

 

그리고 모든 장기 기물의 위치 정보를 갖고있는 외부에서 이를 직접 호출한다.

public List<JanggiPosition> getAvailableDestination(final JanggiPosition position) {
    ...
    // HurdlePolicy를 기물의 외부에서 호출
    HurdlePolicy hurdlePolicy = piece.getHurdlePolicy();
    return hurdlePolicy.pickDestinations(piece.getTeam(), coordinatePaths, janggiPositions);
}

 

1단계 피드백

1. 중복 코드는 상태에 포함될까, 책임에 해당될까?

중복 제거를 위한 작업의 결과가 결국 복잡해진다면 코드를 잘 짰다고 볼 수 있나?

제삼자가 코드를 이해하기 어렵다면 잘 설계한 것이라고 볼 수 있나?

추상 클래스의 경우 그 부분이 더욱 두드러짐

 

2. 중복 제거의 대상이 맞는지 확실하게 검증하고 넘어가자

중복은 모양으로 판단하지 않는다. 어떤 A라는 코드와 B라는 코드가 있을 때, 모양이 같다고 해서 무조건 중복 코드라고 볼 수 없다.
만약, A의 변경이 있을 때 B도 변경해야한다면(반대도 마찬가지) 이는 중복 코드라고 볼 수 있다.

즉, 코드의 중복은 단순히 모양만으로 판단하는 것이 아니라, 역할로 판단하는 것이다.

생각해볼 부분

현재 코드에서 추상 클래스가 중복 코드를 제거했다고 볼 수 있을까?

 

2단계 - To Be Continued ...

2단계에서는 피드백 내용을 중점으로 고민하면서 진행했다. 피드백 수업을 들으면서 장기를 추상 클래스로 구현하는 것이 과연 적합할까? 에 대한 의문이 많이 들었기 때문이다.

 

원래는 2단계도 이번 포스팅에서 모두 정리하려고 했는데, 내용이 너무 길어질 것 같아서 다음 포스팅에 이어서 정리해보겠다!

 

'우아한테크코스 7기 > 회고' 카테고리의 다른 글

[레벨1] 레벨 인터뷰 정리  (4) 2025.04.14
[레벨1] 장기 미션 회고록 1 - 우테코가 바라는 것  (2) 2025.04.12
[레벨1] 블랙잭 미션 회고록  (4) 2025.04.08
[레벨1] 출석 미션 회고록 그리고 TDD  (6) 2025.03.11
[레벨1] 로또 미션 회고록 + 학습 내용 정리  (2) 2025.03.03
'우아한테크코스 7기/회고' 카테고리의 다른 글
  • [레벨1] 레벨 인터뷰 정리
  • [레벨1] 장기 미션 회고록 1 - 우테코가 바라는 것
  • [레벨1] 블랙잭 미션 회고록
  • [레벨1] 출석 미션 회고록 그리고 TDD
민똔
민똔
  • 민똔
    기록은 기억보다 오래 머무른다
    민똔
  • 전체
    오늘
    어제
    • All (24)
      • web (1)
      • frontend (12)
        • javascript (8)
        • CSS (1)
        • react (2)
      • backend (3)
        • CS (1)
        • java (1)
        • spring (1)
      • algorithm (2)
      • 우아한테크코스 7기 (6)
        • 회고 (6)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 깃허브
  • 공지사항

  • 인기 글

  • 태그

    백준
    css
    로그인 기능 구현
    secure cookie
    Session
    일급 컬렉션
    그리디 알고리즘
    리팩토링
    batchsize
    OCP
    httpOnly cookie
    개방 폐쇄 원칙
    LSP
    N+1 문제
    Java
    fetch join
    객체지향
    SUBSELECT
    SOLID
    spring
    access token
    Session Storage
    cookies
    백준 11000
    Refresh Token
    Local Storage
    개발
    JWT
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
민똔
[레벨1] 장기 미션 회고록 2 - 1단계 (추상 클래스, 합성)
상단으로

티스토리툴바