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

[레벨2] 지하철 미션

d02 2023. 6. 18. 20:55
 

GitHub - yoondgu/jwp-subway-path

Contribute to yoondgu/jwp-subway-path development by creating an account on GitHub.

github.com

기능 요구사항

지하철 노선도라는 복잡한 요구사항을 가진 웹 애플리케이션을 구현하는 미션

1단계 : 지하철 정보 관리 기능
제공되는 뼈대코드(간단한 지하철 역과 노선 CRUD)를 바탕으로 노선에 역 등록 및 제거 기능을 객체지향적으로 설계
가급적 TDD로 구현
- 노선에 역 등록 API 신규 구현
- 노선에 역 제거 API 신규 구현
- 노선 조회 API 수정 : 노선에 포함된 역을 순서대로 보여주도록 응답 개선
- 노선 목록 조회 API 수정 : 노선에 포함된 역을 순서대로 보여주도록 응답 개선

2단계 : 경로 조회 기능
프로덕션과 테스트 데이터베이스 설정 분리
- 경로 조회 API 구현 : 최단 거리 경로를 구하는 API 구현 (거리 계산, 여러 노선 환승 고려)
- 최단 거리 경로 조회 시에는 외부 라이브러리인 jgrapth 라이브러리 사용
- 요금 조회 기능 추가

(선택) 3단계 : 요금 정책 추가
변경에 유연한 설계 고민하기
- 노선별 추가 요금 정책 반영
- 연령별 요금 할인 정책 반영
- 경로 조회 API 변경

작성한 요구사항 목록, API 명세, 코드리뷰 피드백 및 리팩터링 목록은 저장소의 README.md에서 확인할 수 있다.

전체 회고

복잡한 도메인 요구사항 : 다음엔 다이어그램으로 명확하게 하자

Spring 기술 익히기 위주였던 레벨2의 지난 미션과 달리, 도메인 이해에 대한 난이도가 확 올라간 미션이었다.

그래서 그 어느 때보다 페어 프로그래밍에서 토론하며 진행하는 시간이 많았다.

도메인이 복잡하다보니 토론이 길어지면서 "그래서 무엇때문에 이 얘기 하고 있었지?" 싶거나,

복잡한 도메인을 이해하며 대화하려고 하니 어려움을 느낄 때도 많았다. 

그래도 적극적이면서 의견을 잘 들어주는 페어를 만나 즐겁게, 또 시간 내에 구현할 수 있었고 리뷰어께서도 복잡한 도메인 치고는 코드가 깔끔한 편이라고 해주셨다.

 

하지만 돌이켜보니, 복잡한 도메인인 만큼 기록으로 확실하게 남길 수 있는 다이어그램을 만들며 했으면 더 좋았겠다는 생각이 든다!

미션 구현까지 주어진 시간이 얼마 없어 다이어그램을 만들 마음의 여유를 내지 못했지만 말이다.

의사소통하며 결정한 사항에 대한 오해도 줄이고, 뭔갈 이해하는 데 드는 에너지를 줄일 수 있을 것 같아서다.

실제로 레벨1에서 가장 어려운 미션이었던 체스 미션에서도 시퀀스 다이어그램을 그린 게 도움이 되었던 게 기억난다.

 

API 명세 변경으로 도메인을 단순화할 순 없다. 클라이언트 의존적인 개발을 주의하자

기존 지하철 노선도에 새 역을 추가하는 기능에는 요구사항 특성 상 분기가 무척 많아졌다.

기존 API 설계 시, 왼쪽 - 오른쪽 에 위치하는 두 역을 전달받도록 했는데

그러다보니 두 역 중 어느 것이 기준 역인지 판단한 뒤
상행 방향으로 추가하는지, 하행 방향으로 추가하는지 판단하고 각 경우에 따라 검증 및 거리 조정을 해주어야 했다.

그러다보니 코드에는 분기문도 많아지고, 여러 경우의 수를 빠르게 이해하기 어려워졌다.

 

그래서 나는 API 명세에서 어떤 약속을 정해두면 분기를 줄일 수 있지 않을까? 생각했다.

“역 등록”은 무조건 기준 역에 대해 하행 방향 다음 역에 등록하는 것이라고 정의하는 것이다.

애초에 API에서 기준 역, 추가 역을 전달받은 뒤
무조건 한 방향으로만 추가할 수 있게 하고, 가장 끝 역을 넣을 때에만 기준 역에 null을 받도록 하면
이런 분기가 필요 없어져 비즈니스 로직을 구현하기 위한 코드가 간단해질 것이라고 생각한 것이다.

 

1단계 미션 제출일 날 페어와 이 부분에 대해 긴 시간을 토론했다.

결국 페어가 나의 제안을 받아들여줘서 시간이 얼마 남지 않았음에도 방식을 바꾸어 진행했는데,

다른 분기는 줄어들어도 결국 기준 역의 null 여부를 체크해야 하므로 비즈니스 로직의 복잡도를 낮추는 장점은 취하지 못했다..

(시간이 부족한 상황에서 확신할 수 없는 새로운 방향을 내가 너무 피력한 것 같아 미안했다 ㅠㅠ)

그래도 이후 리팩터링에서 "등록 방향"을 전달받게 하면서 최대한 분기를 줄이고 구조적으로 해결해나갈 수 있게 되었다.

 

그럼에도 이를 계기로 큰 교훈을 하나 얻었다.

일단 API 명세 변경으로 "비즈니스 로직"을 바꿀 수는 없다. 비즈니스 로직 구현을 위한 코드는 바꿀 수 있어도.

그렇다면 만약 위와 같이 API 명세 변경으로 비즈니스 로직 코드의 복잡도를 낮추려는 시도를 성공했다고 치자.

그러면 과연 적절한 시도였다고 할 수 있을까?

 

우리가 MVC 패턴을 사용하는 이유가 무엇인가? 모델과 뷰가 서로 독립적이기 위함이다.

이처럼 API 명세는 클라이언트와 소통하는 계층에 해당하고 언제든지 변경될 수 있는 요소이다.

당장은 이에 의존하여 비즈니스 로직의 코드가 단순해졌더라도, 이후 프로젝트의 변경이나 유지보수에 대해서는 방어가 되지 않는다.

이에 대해 리뷰어님이 조언해주신 내용 중 "클라이언트에게 어떤 기대를 하는 것은 결국 이슈가 된다"는 말도 잊지 말자.

 

그래서 다른 계층에 영향 없이 독립적으로 존재하는 이상적인 도메인 모델을 구현하도록 노력하는 방식으로 리팩터링을 진행했다.

 

 API는 클라이언트 측에서 명세를 확인하고, 약속된 방법으로 사용하는 것을 전제로 하므로 문제 없다고 생각하기도 했었는데,

이 "약속"이 얼마나 영원할 것인가? 항상 의심하자..! 클라이언트에 의존적인 개발을 하지 않도록 주의하자.

 

테스트에서 가짜 객체 사용의 위험성

물론 테스트 목적, 대상에 따라서 다르겠지만 나는 굳이 꼽자면 Classist보다는 Mockist에 가깝다고 생각한다.

가짜 객체를 쓰면 테스트의 신뢰도가 낮아진다고 생각할 수 있지만, 해당 협력 객체에 대한 별도의 단위테스트에서 이 협력객체를 실제 객체로서 테스트하므로 괜찮다고 생각했다. (단위 테스트가 전체적으로 잘 작성되어 있다는 전제 하에)

 

그런데 이번에 서비스 계층 테스트에서 Mockito를 적극적으로 쓰면서, 가짜 객체 사용의 위험성을 직접 경험해보았다.

"가짜 객체를 쓰기 위해 세부 구현에 의존하게 된다"는 말을 이제 정확히 이해했다.

 

서비스 계층의 각 메서드가 dao의 메서드를 잘 호출하고, 예외를 잘 던지는지만 확인하려고 한 건데

dao에서 어떤 값을 반환해야 하는지 미리 다 지정해주어야 했다.

그러다보니 세부 구현이 테스트 코드에 모두 드러났고, 테스트 코드를 수정하는 것도 쉽지 않았다.

 

하지만 서비스 계층에서 dao의 메서드까지 호출해주어야 했을까?

"어떤 테스트를 하려고 했냐"에 따라서도 달라지는 문제인 것 같기도 하다..!

위험성이 있으니 쓰지 말아야지~가 아니라 Mockist 방식의 테스트 방법에 대해 더 공부해보고 싶어졌다.

 

유연한 설계를 위한 디자인 패턴 적용해보기

- 노선에 따른 추가 요금 적용

- 연령에 따른 할인 적용

 

위와 같은 요금 정책의 추가를 어떤 식으로 반영하는 게 유연한 설계인가 오래 고민했다.

생각해보면 기본 요금 정책과 위 두 가지 추가 정책은 "같은 층위의, 서로 다른 정책"이라고 보긴 어려웠다.

추가 요금을 적용하고, 할인을 적용하더라도 기본 요금 계산을 먼저 해야 하기 때문이다.

 

아래와 같이, 서로 다른 여러 가지 세부 정책이 요금 정책에 다르게 적용될 수 있다고 생각했다.

이를 하나의 인터페이스로 풀거나, 각 정책을 같은 층위로 생각해 여러 개의 인터페이스를 인자로 받으면

매번 인자를 변경해주어야 한다.

그래서 기본 요금 계산은 늘 베이스로 수행하되, 전-후에 다른 로직을 추가한 구현체를 자유롭게 새로 생성할 수 있는 데코레이터 패턴을 적용해보았다.

요즘 좋은 설계를 어떻게 해야 잘 할 수 있을지, 전보다 막막해진 기분이었는데 맹목적인 학습 대신 필요에 따라 디자인 패턴 중 내가 원하는 게 있는지 찾아서 적용해보니 좋았다.

 

이 내용은 별개의 포스팅으로 정리하겠다!

 

미션 로그

  • 로깅을 하는이유, Logback
  • @PostConstruct를 사용한 초기화의 장단점
  • 지하철 노선에 역을 추가/삭제하는 복잡한 변경에 대해, DB에서 모든 정보를 삭제하는 것은 비효율적인가? -> 상황에 따라 트레이드오프로 판단할 것.
  • 동일한 DTO의 사용처가 늘어나는 상황(여러 클라이언트, 다른 스펙의 API 추가..)에 대한 대응은 어떻게 하면 좋을까?
  • DTO는 controller vs service 어느 계층에 속해야 할까? (나만의 관점 가지기)
  • 컨트롤러 테스트에서 세부적인 비즈니스 규칙을 모두 검증할 필요가 있을까? (검증 대상, 비용을 고려해 비중을 정하기)

 

주요 기술 부채

  • 인수 테스트란?
  • 외부 라이브러리에 대한 종속성은 어떻게 푸는 게 깔끔할까?
  • 디자인 패턴 간의 차이, 언제 무엇을 쓰면 좋을지 기준 세우기
  • Spring Profile 기능이란?
  • Spring Configuration 방식 중 Annotation vs XML의 장단점 비교
  • Mockist 방식의 테스트 기법
  • GET 메서드에서 query parameter vs body 사용하는 기준 세우기
반응형