우테코 5기

[레벨 4 회고] 레벨 4 레거시 코드 리팩토링 미션 회고

teo_99 2023. 11. 4. 01:12

레벨 4는 프로젝트로 바빠 미션에 대한 회고는 여태 작성하지 못했었는데, 마지막 미션에서는 얻은게 상당한 것 같아 회고를 작성하게 되었습니다. 레거시 코드 리팩토링 미션은 기존의 절차지향적으로 작성된 코드를 단계적으로 리팩토링 및 보완하는 미션입니다.

 

비즈니스 요구사항 분석하기

step1 의 요구사항은 비즈니스 요구사항을 분석하고 테스트 코드를 작성하는 것이었습니다. 처음 주어진 코드는 빈약한 도메인 모델을 사용하고 있었을 뿐만 아니라 서비스에 모든 로직이 쏠려 있는 트랜잭션 스크립트 패턴으로 구성되어 있었습니다.

코드 예시

또한 문서라고 할만한 게 전혀 없었기 때문에 오로지 코드로부터 요구사항을 분석해야 했습니다. 얼추 기능목록은 뽑아내긴 했으나 코드만으로 비즈니스를 유추하기에는 모호한 부분도 존재했습니다.

TableGroupService.ungroup 메소드

도메인 이해도가 낮아서 그런 것인지, 테이블 그룹을 해제할 때 왜 테이블을 '비지 않은 테이블'로 변경하는지 알기 어려웠습니다. 또한 이게 레거시 코드가 잘못 작성되었다는 가능성을 염두에 두지 않을 수 없기 때문에 더 고민이 되었습니다. 확실히 비즈니스를 코드로만 파악한다는게 상당히 어렵다고 느껴졌습니다. 그래서 요구사항을 분석하는 데 상당한 시간이 걸렸던 것 같습니다.

작성한 비즈니스 요구사항 중 일부

테스트 코드 작성하기

요구사항을 기반으로 테스트를 작성했습니다. 레거시 코드는 모든 로직이 서비스에 집중되어 있었기 때문에 통합 테스트 형식으로 진행했습니다. 추후 비즈니스 로직을 개선할 것을 고려해 Mocking을 사용하지 않고 Classic하게 테스트 코드를 작성했습니다. Service의 의존성이 바뀔 때마다 테스트가 깨져버린다면 테스트 슈트의 존재 의의가 부족하다고 판단했습니다.

테스트 코드 일부

위와 같은 형식으로 모든 요구사항에 대한 통합 테스트를 작성했고 컨텍스트 캐싱까지 진행했습니다. 다만 이 때 컨텍스트 캐싱을 한 것이 나중에 모듈을 분리하면서 걸림돌이 되었는데.. 뒤에서 작성하겠습니다.

또한 기존 레거시 시스템의 경우 flyway 마이그레이션 스크립트로 더미 데이터들을 삽입하고 있었는데, 테스트에 영향을 줄 수 있다고 판단해 DatabaseCleaner를 통해 데이터베이스의 모든 테이블을 비워줬습니다. 처음 사용하는 방법이었는데 크루 허브의 블로그를 참고해서 진행했습니다.

 

서비스 리팩토링

step2는 단위 테스트하기 어려운 코드와 단위 테스트하기 쉬운 코드를 분리해서 단위 테스트를 작성하는 것이 목적이었습니다. 해당 단계를 통해 느낀 점은 점진적으로 리팩토링해야 한다는 것이었습니다. 레거시 코드의 변경지점이 매우 많다보니 한 번에 모든 것을 바꾸기에는 쉽지 않았고, 시도하다가 롤백도 여러 번 했습니다. 따라서 리팩토링은 다음과 같은 순서대로 진행했습니다.

 

  1. 컨트롤러에서 요청 및 응답에 대해 DTO를 사용하도록 변경
  2. JPA를 사용하도록 변경 및 연관관계 설정
  3. Setter 삭제, 생성자를 사용하도록 변경
  4. 서비스로부터 비즈니스 로직 분리, 도메인 객체 내부로 캡슐화
  5. 도메인 객체들에 대한 단위 테스트 생성
  6. 인수 테스트 생성

 

기능목록과 테스트 슈트의 중요성

이 과정에서 기능목록 및 테스트 슈트의 중요성을 많이 느꼈습니다. 위 작업들을 모두 수행하다보니 아래와 같이 변경 지점이 상당히 많았는데, 변경 이전의 코드와 100% 동일한 기능을 한다고는 보장하기 어려웠습니다.

 

물론 테스트 케이스가 전부 통과하고 기능 목록에 있는 내용에 대해서는 동일한 동작을 수행했지만, 빠뜨린 비즈니스 요구사항이 충분히 있을 수 있다고 생각했습니다. 그리고 정말 놓친 부분이 있었어서 나중에 기능을 보완하기도 하였습니다. step1에서 단순히 통합 테스트만 생성했는데, 인수테스트 등 더 꼼꼼하게 테스트를 작성하지 못했던 것이 아쉬웠습니다.

 

의존성 리팩토링

step3는 의존성 리팩토링을 진행했습니다. step2에서 JPA와 함께 객체 참조를 사용하다보니 양방향 의존성이나 패키지 순환 의존성이 발생하는 경우가 있었습니다. 

 

이 시점에 제이슨의 도메인 주도 개발 강의와 조영호님의 우아한 객체지향을 들으며 미션을 병행하게 되었고 관련 개념을 미션에 접목시켜보았습니다. 유사한 라이프사이클이나 제약조건을 가지는 도메인 객체들을 하나의 그룹으로 묶고, 서로 다른 그룹의 도메인 객체들끼리는 객체 참조가 아닌 ID 참조를 수행하게 하여 약한 결합을 유지하였습니다.

 

그리고 그룹 경계(컨텍스트)를 넘어서는 협력이 발생하는 경우에는 Spring Event를 사용했습니다. 다만 고민이 되었던 부분은 Spring Event에서 로직까지 담당할지, 아니면 서비스에게 위임할지였는데 저는 Spring Event는 단순히 메소드 호출 역할만 하는게 더 응집도 있는 설계라고 판단이 되어서 아래와 같이 코드를 작성했습니다.

다만 서비스 레이어에서 이벤트를 발생시키는 방법이 아니라 도메인 내부에서 협력이 필요한 순간에 AbastactAggregateRoot을 활용해서 이벤트를 발행시키는 방법이 더 깔끔하다는 생각이 드는 것 같습니다. 

 

서비스에서 이벤트를 발행하는 방법과 도메인 객체에서 이벤트를 발행하는 방법을 비교해보면 다음과 같은 장단점이 있을 것이라 예측되는데 아직 실무 경험이 부족하므로 명확한 판단을 내리기는 어려운 것 같습니다.

  • 서비스 이벤트를 발행하는 경우
    • 애그리거트 간의 협력이 서비스에 드러나서 가시적임
    • 도메인 객체에서는 해당 애그리거트와 관련된 비즈니스 로직만 수행한다고 단언할 수 있음
  • 도메인 이벤트를 발행하는 경우
    • 서비스로 핵심 비즈니스 로직을 노출시키지 않을 수 있음
    • 여러 서비스에서 재사용하기가 쉬움
    • 애그리거트 간의 협력이 가시적이지 않음

 

이벤트 테스트하기

Spring Event에 대한 테스트는 `@RecordApplicationEvents` 와 `ApplicationEvents` 를 활용했습니다. 이벤트를 기록하고 발생했는지 여부를 확인할 수 있는 기능으로, 어떤 메소드를 수행했을 때 특정 이벤트가 몇 번 발생했는지 등을 확인할 수 있습니다.

다만 비동기나 트랜잭션 처리에 관련해서는 관련 지식이 부족한 상태인데, `@TransactionalEventListner` 등에 대한 학습을 보완해야겠다는 생각이 듭니다.

 

어떤 도메인 객체들을 애그리거트로 묶어야 하는가

어떤 도메인 객체들이 묶여야 하는지, 어떤 도메인 객체들은 묶이지 않아야 하는지 고민이 많이 들었습니다. 크루들마다 도메인 객체들을 묶는 방법이 다르기도 했습니다. 저는 '같이 변경되는지' 위주로 애그리거트를 판단했습니다. 같이 변경된다는 것은 같은 제약사항을 공유한다는 의미입니다.

 

객체지향을 학습할 때 해결 영역에 존재하는 '도메인 모델'을 참고해서 객체를 모델링해야 한다고 학습했었는데, 애그리거트도 비슷한 맥락으로 구성되지 않나 생각합니다. 다만 아직까지는 실무 경험이 없고, 장기적인 리팩토링 경험이 부족해서 명확한 판단 기준은 없는 상황입니다. 이 부분은 실무 경험이 생기면서 자연스레 해결될 고민이므로 추가적인 학습은 당장 필요하진 않을 것 같습니다.

 

최종적으로 분리한 애그리거트와 의존성은 아래와 같습니다. 애그리거트 간의 협력은 대부분 이벤트로 분리했습니다.

 

 

멀티 모듈 적용

step4의 요구사항은 멀티모듈을 적용하는 것이었습니다. 레이어 별로 모듈을 분리할 수도 있겠으나 이번 미션의 핵심은 계층 간 격리가 아닌 애그리거트 간의 격리라고 생각해 애그리거트별로 모듈을 나눴습니다. 그리고 gradle에서 필요한 의존성을 각각의 모듈에서 설정해줬습니다.

모듈 분리

여러 애그리거트에서 사용하는 VO는 common 모듈에 두고, 필요한 모듈에서 이를 의존하도록 설정해줬습니다. 다만 우아한기술블로그의 멀티모듈 설계 이야기에서는 common 모듈의 위험성과 추상화 계층과 역할에 따라 보다 철처히 모듈을 나눌 것을 권장하는데 나중에 멀티모듈을 실제 프로젝트에 적용할 일이 있을 때 다시 읽으면서 정리해 보아야겠습니다.

 

통합테스트는 어느 모듈에?

애그리거트 별로 모듈은 분리했으나 통합테스트가 문제였습니다. 또한 앞서 컨텍스트 캐싱을 위해 만들어두었던 ServiceTestContext도 의존성 덩어리였기 때문에 어느 모듈에도 속하지 못했고, Spring Boot Application이 동작하는 app 모듈에 둘 수 밖에 없었습니다.

 

다만 반대로 생각하면 여러 도메인이 협력하는 통합테스트는 별도의 모듈로 존재하는게 당연한 것 같기도 한데, 어떤 방식으로 풀어내야 할지 확신은 없는 것 같습니다. Fixture 처리라든가, 통합 테스트 처리를 어떻게 해야 할지 고민하느라 많은 시간을 보냈던 것 같습니다.

 

레거시 리팩토링 미션 후기

레거시 코드에 대한 생각을 많이 해볼 수 있었고 어떻게 리팩토링을 해야 하는지, 어떤 관점으로 접근해야 하는지에 대한 틀을 잡는 계기가 되었습니다. 사실 이정도 규모도 상당한 시간이 걸렸는데 실무 도메인에서는 얼마나 많은 시간이 걸릴지, 얼마나 어려울지 상상하기가 어려운 것 같습니다. 역시 달리는 기차의 바퀴를 갈아끼우는 일은 어렵다는 생각이 듭니다. 앞으로 더 많이 공부해야겠습니다..