우아한테크코스/회고

[우아한테크코스] LV1 - 체스 미션 회고

_Hiiro 2023. 4. 10. 20:42

 

우아한테크코스 LV1 마지막 미션은 체스 게임을 구현하는 것이었다. step1,2는 콘솔에서 체스판을 초기화 하고 말을 움직여서 게임을 진행할 수 있도록 하는 것이 목표였다. step 3,4는 상대 편의 King을 잡았을 때 게임을 종료하고 점수를 계산해 승패 여부를 출력하도록 하고 DB와 연동해서 중간에 진행되던 게임을 다시 불러와서 진행할 수 있도록 하는 것이 목표였다.

 

이번 step1,2 페어는 엔초였다. 당시 테코톡 발표와 각종 야크 쉐이빙으로 밀린 task에 허덕이던 상황이었어서 체력적으로 많이 힘든 상황이었다. 그런데 엔초는 도메인을 분석해서 객체에게 책임을 분배하는 능력이 정말 뛰어난 크루였다. 함께 미션을 진행하면서 객체를 설계하는 관점에 대해 많은 것을 배울 수 있었다. 그 덕분에 step1,2의 진행 뿐만 아니라 이후 리팩토링을 할 때에도 구조적으로 크게 바꿀만한 사항이 없었어서 스무스하게 진행할 수 있었다. 

 

나중에 엔초는 레벨1 필독서인 객체지향에 대한 사실과 오해를 읽으면서 객체 설계에 대한 인사이트를 많이 얻을 수 있었던 것 같다고 얘기해줬다. 개인적으로 레벨1을 지나오면서 독서를 통한 이론적 지식의 습득을 잘 챙기지 못했다고 생각했는데 한 번 더 스스로를 반성하게 되는 계기가 되었다. 😭

 

 

 

[객체에 대한 책임 분배]

 

이번 미션을 진행하면서 배운 것들 중 가장 중요하다고 생각되는 부분이다. 여태까지 요구사항을 분석하고 Domain 객체를 설계하는 과정에서 현실 세계에서 스스로 행동하지 못하는 것들을 객체화해야하는 경우가 빈번했다. 그럴 때마다 항상 현실세계의 객체가 그러한 것처럼 소프트웨어 상 구현된 객체에서도 외부에서 이러한 책임을 수행해줘야 한다고 생각해왔다. 

 

그런데 체스게임의 Domain 객체를 설계하는 과정에서 엔초는 현실세계에서는 스스로 어떤 것도 할 수 없는 사물이나 추상적인 개념일지라도 프로그램 상에서 객체화가 되면 스스로 수행할 수 있는 책임을 부여할 수 있어야 한다고 얘기해줬다. 이 말을 들었을 때 뒤통수를 한 대 맞은 것처럼 얼얼했던 기억이 난다. 

 

public class Path {
    
    private final List<Position> positions = new ArrayList<>();

    public Path(Position start, Position end) {
        makePathTo(start, end);
    }

    public void makePathTo(Position start, Position end) {
        if (Direction.of(start, end) == Direction.OTHER) {
            positions.add(end);
            return;
        }
        calculatePath(start, end);
    }

    private void calculatePath(Position start, Position end) {
        /*
        start Position과 end Position 객체를 넘겨받아서 두 Position 사이의 경로를 계산하는 기능
         */
    }

    public int size() {
        return positions.size();
    }
}

 

원래 Domain 객체를 설계하던 대로 생각했으면 시작 지점과 도착 지점을 나타내는 Position 객체들을 이용해 경로를 계산하는 책임을 어떤 객체에게 부여했을까?  체스말이나 체스게임 혹은 경로 계산기 등과 같이 스스로 생각했을 때 구체적인 객체에게 이를 부여했을 것이다. 하지만 그럴 필요 없이 Path라는 객체를 만들고 해당 객체에게 이 책임을 수행할 수 있도록 설계하면 되는 것이다.

 

결과적으로 체스말, 체스게임 등의 객체가 수행해야 하는 책임의 범위가 너무 커지지 않게 방지할 수 있었다. 또한 Path 일급 컬렉션을 만들어 두고 경로를 계산하는 책임을 지는 경로 계산기 등과 같은 객체를 따로 더 만들지 않아도 되었다. 이를 조금 더 일반화된 표현으로 바꾸면 Context에서 Position 객체를 이용해 경로를 계산하는게 아니라 Path 객체가 스스로 자신의 경로를 계산할 수 있도록 책임을 부여했다고 얘기할 수 있을 것 같다.

 

 

이후 방학 기간에 드디어 객체지향에 대한 사실과 오해를 읽게 되면서 몸으로 먼저 겪었던 부분을 접하게 되었다.

 

현실 세계의 전등은 사람의 손길 없이는 스스로 불을 밝힐 수 없지만 소프트웨어 세계의 전등은 외부의 도움 없이도 스스로 전원을 켜거나 끌 수 있다. 현실 세계에서는 사람이 직접 주문 금액을 계산하지만 소프트웨어 세계에서는 주문 객체가 자신의 금액을 계산한다.

 

현실세계에서 스스로 행동할 수 없는 것들을 객체로 설계할 때 항상 어떤 상태 값을 가져야 하는지만 중점적으로 생각하고 외부에서 이를 어떻게 사용해야 할지만 고민했었다. 결과적으로 이렇게 설계된 객체는 자신의 상태값을 외부로 넘겨서 책임을 위임하도록 하는 객체답지 못한 객체가 되었다. 객체의 책임 분배에 대한 시야가 많이 좁았다는 것을 다시 한 번 느꼈다. 앞으로는 소프트웨어 세계에서 설계되는 객체는 현실 세계와는 다름을 인지하고 객체에 적절한 책임을 부여할 수 있도록 해야겠다. 

 

 

 

[TDD와 Commit 단위]

 

레벨1에서 지속적으로 우리가 지키고 있는 것 중 하나는 TDD이다. TDD는 테스트 코드를 먼저 구현하고 이를 통과시킬 수 있는 코드를 구현하는 방식으로 개발을 진행하는 방식으로 진행된다. 그런데 여태까지는 별다른 생각 없이 Commit 컨벤션에 test라는 키워드가 있었어서 테스트 코드를 구현하고 나서 실패하는 test 코드를 commit 하고 이후 기능 구현 코드를 완성해 commit하는 방식으로 진행했다. 그런데 이런 방식에는 Commit을 원자적인 단위로 보지 못했다는 문제점이 있었다.

 

좋은 Commit은 어떤 상황에서라도 빌드를 깨뜨려서는 안 된다.
누구든지 해당 Commit으로 프로그램을 시작하거나 브랜치를 생성할 수 있어야 이상적이다.

 

굉장히 일리가 있었다. 만약 모종의 이유로 인해 특정 시점 Commit으로 프로젝트를 되돌려야 한다고 했을 때 해당 Commit이 돌아가지 못하는 프로그램으로 Commit되어 있었다면...? 굉장히 골 아픈 상황을 마주할 수 밖에 없어지는 것이다. 그래서 이번 미션 commit의 단위를 TDD 과정에서 구현한 테스트 코드와 테스트를 통과시키는 구현 코드를 한 단위로 묶어서 Commit을 하게 되었다. TDD로 프로젝트를 진행할 때 Commit을 어떻게 가져가야 하는지에 대한 인사이트를 정립할 수 있는 기회가 되었다.

 

 

 

[Layered Architecture - Service Layer]

 

3,4 단계 미션을 구현하면서 Layered Architecture의 도입을 고려하게 되었다. 맨 처음 DB를 연동하는 과정에서 Controller가 직접 Dao를 사용해서 데이터를 불러오고 저장하도록 구현했었다. 아래는 해당 부분에 대한 리뷰어 영이의 Comment이다.

controller는 domain과 view를 연결하는 역할을 하지만 db와의 직접적인 연결은 좋은 방법이 아닌것 같습니다
service layer를 도입하는게 좋을것 같습니다
presentation, business, persistance layer로 관리할 수 있도록 변경이 필요할것 같습니다.

 

MVC 패턴에서 Controller의 역할은 Domain과 View를 연결하는 것이기 때문에 Dao를 다루는 책임을 질 다른 객체의 필요성이 대두되었음에는 동의했다. 그렇다면 Service Layer는 정확하게 어떤 역할을 수행해야하는 것일까? 🤔

 

여러 레퍼런스를 찾아보고 크루들과 얘기해보며 정리한 Service Layer의 책임은 다음과 같았다.

 

  • Service Layer는 컨트롤러와 Dao, Domain 사이에서 중개 역할을 수행하는 책임을 진다.
  • Service Layer에서는 트랜잭션과 도메인 간의 순서만을 보장해야 한다.
    • Dao는 데이터베이스 쿼리를 날리고 원시 값 데이터를 Dto에 담아서 Service Layer로 반환하는 역할
    • Service Layer는 Dto에 담긴 데이터를 가지고 도메인에 전달하여 비즈니스 로직을 수행할 수 있도록 한다.
    • Service Layer는 출력에 필요한 데이터를 Dto에 담아서 controller(Presentation Layer)에게 넘겨준다.

 

Service Layer의 책임을 수행하는 클래스를 구현하면서 자연스럽게 Dto의 사용 관점도 정리가 되는 것을 느꼈다.

  • Dao로부터 조회한 원시 값 데이터들을 Service Layer로 전달한다.
  • Domain 객체들이 스스로의 책임을 수행한 결과를 Presentation Layer에 전달한다.

이렇게 Layer간 데이터를 교환하는 책임을 지는 객체가 바로 Dto의 본질이었던 것이다. 예전 Dto에 대해 개인적인 고찰을 했을 때에도 어느 정도 인지는 하고 있었지만 직접 이렇게 사용해보니 더 Dto의 본질이 와닿을 수 있었던 것 같다.

 

마지막으로 코코닥과 Service Layer에 대해 얘기하면서 알게된 부분을 정리하며 회고를 마쳐볼까 한다. 우리가 레벨 내내 View와 Domain을 분리하려고 기를 쓰며 고생했던 이유는 이렇게 프로젝트의 규모가 커질 때 드러난다. Domain은 우리가 제공하는 서비스의 핵심이다. 즉 속된 말로 진짜 우리의 돈줄이자 생명줄인 것이다. 그런데 프로젝트 규모가 커지면 Domain을 제외하더라도 신경써야 할 부분들이 무지막지하게 많아진다. 이런 상황에서 개발자는 수많은 요구사항 변경을 접하게 될 것이며 그럴 때마다 어디를 수정해야 하는지 고민할 것이다. View를 손봐야 될 수도 있을 것이고 DB와 연동하는 방식을 수정해야 될 수도 있을 것이다. 아니면 새로운 서비스 도입을 위해 Domain을 새로 설계해서 기존 프로젝트에 추가해야 될 상황도 발생할 수 있다.

 

만약 기존에 설계해서 이미 서비스되고 있는 Domain이 이런 수많은 요구사항 변경이 발생할 때마다 영향을 받아야만 한다면 어떻게 될까? 개발자인 우리는 생명줄이 혹여나 끊어지지는 않을지 항상 노심초사해야만 할 것이다. 그렇기 때문에 우리는 반드시 Domain의 왕국을 만들어서 최대한 외부로부터 견고하게 지켜야 한다. 이로부터 많은 디자인 패턴과 아키텍쳐가 등장한 게 아닐까 한다. 

 

DB를 연동하면서 Domain 설계 원칙들이 많이 망가진다고 피드백 강의에서 들었던 기억이 난다. 그러면 어차피 무너질 원칙들을 우리는 왜 시간과 노력을 들여가며 이렇게 배운거지? 라는 생각이 들었었는데 잘못된 생각이었음을 깨달았다. 앞으로도 어떻게든 우리의 생명줄(돈줄)인 Domain의 왕국을 만들어 지키는데 계속 써먹어야 할 지식들이니 소중하게 다룰 수 있도록 하자. 🤣