리팩토링의 분류

리팩토링은 기존의 코드를 재구성하여 코드의 품질을 향상시키는 작업이다.
일반적으로 리팩토링이라하면 변수나 함수를 정리하는 등의 방식으로 코드 품질과 생산성을 높이는 방식을 말한다.

그런데 이렇게 간단하게 진행되는 리팩토링 외에 ‘리디자인’ 정도로 리팩토링이 필요한 경우가 있다.
마틴파울러는 이걸 big refactoring이라고 불렀다. big refactoring을 어떻게 하면 좋을지를 고민해보며 업데이트 하는 나의 리팩토링 방향.

사용하지 않는 코드 삭제하기

사용하지 않는 코드는 남아서 refactoring이 경직되게 만들고 코드를 이해하기 어렵게 만든다.
따라서 사용하지 않는 코드는 리팩토링의 모든 스텝마다 가장 먼저 체크되어야 할 부분이다.

특히 legacy module의 경우 어떤 코드(모듈)을 사용하는지 사용하지 않는지 조차 알지 못하는 경우도 많다.
api call 지표를 보면서 사용하지 않는 api를 확인하는 것도 좋다.
이런 삭제는 얽혀있는 스파게티가 풀릴 수 있는 하나의 실마리들이 된다.

리팩토링으로 제거할 수 있는 코드들은 이런게 있다.
내가 실제로 10년 된 모듈을 맡게되면서 리팩토링한 것들.

사용하지 않는 api

내가 맡은 모듈은 전세계에서 하루에 수십 억건의 요청이 들어오는 서비스이다.
10년 된 모듈 답게 legacy version부터 수 십개의 api가 있었는데, 리팩토링을 하며 사용하지 않는 api들을 찾았다.
먼저 지표에서 api 호출이 없는 것을 확인하고, 클라이언트 담당자에게 사용하는 api인지를 확인했다.

이렇게 지운 api만 해도 수 건이지만, 중요한건 이게 지워짐으로 인해 꼼꼼하게 얽혀있는 스파게티 코드에서 스파게티 몇 줄을 뺌으로써 조금 더 느슨한 스파게티를 만들 수 있다는 것이다.

사용하지 않는 policy

api와 마찬가지로 사용하지 않는 policy도 많다.
이 서비스에서 policy는 property의 일종으로 사용되었고 서비스 전반에 영향을 미치는 짬뽕 같은 코드였다. 한 눈에 보이지도 않고 리팩토링을 하려고 하면 계속 냄새를 내뿜는 policy를 손보았다.

오버 프로그래밍으로 만들어뒀지만 사용하지 않는 policy가 정리되기도 하지만,
Content 타입 별로 서로 다른 동작을 하도록 넣어뒀던 policy가 사실 모두 같은 값을 갖고 있기도 했다.
이런 policy를 지우면 해당 로직도 지울 수 있고 마찬가지로 느슨한 스파게티를 만드는데 도움이 된다.

사용하지 않는 code

api나 policy는 관심을 갖고 보아야 확인할 수 있지만 사용하지 않는 code를 찾는건 참 쉽다.
상위의 class, method, parameter, property 레벨에서 사용하지 않는 것들을 먼저 정리하면 하위의 필요 없던 로직들이 덩달아 지워지기도 한다.
‘리팩토링을 다 하고 봤더니 실제론 필요 없는 코드’라는 상황이 되지 않도록 사용하지 않는 코드들을 먼저 정리하면 스파게티도 느슨하고 리팩토링도 더 쉽다.

리팩토링 아키텍쳐 설계하기

이미 운영중인 서비스, 그것도 legacy 서비스를 한 순간 리팩토링하긴 쉽지 않다.
마틴 파울러도 저서 ‘리팩토링’에서 이건 불가능한 일임을 몇 번이고 얘기했다.
작은 리팩토링이라면 서비스 개발 중에 리팩토링을 할 수 있겠지만 big refactoring의 경우 전략적인 접근이 필요하다.

중간에 치고들어오는 다른 작업들로 리팩토링이 생각한 일정대로 진행되지 않을 수도 있다.
리팩토링을 한번에 하지 못하고 끊기더라도 나아갈 방향을 제시하기 위해 아키텍쳐 설계는 꼭 필요하다.

아키텍쳐 설계가 중요한 이유는 리팩토링된 모듈의 구조에 대한 생각이 팀원들 모두 다르기 때문이다.
나 혼자 한다면 설계는 내 머리 속에 있지만 팀원들은 그렇지 않다.
생각보다 설계를 맞추는 과정은 더 디테일해야할 수 있는데, 실제로 아키텍쳐를 공유했는데도 다르게 이해해서 리팩토링이 잘못 진행되기도 했다.

명확한 리팩토링 아키텍쳐는 리팩토링 방향을 명확하게 한다.
리팩토링은 천천히 진행되겠지만 설계는 명확한 방향으로 나와야 한다.
설계가 명확하지 않으면 애써 진행한 리팩토링한 것을 다시 리팩토링 할수도 있다.

그렇지만 전체 아키텍쳐 설계를 다시할 순 없다.
설계가 명확한 방향이어야할 뿐, 전체적인 설계가 완벽하게 나와야한다는 것은 아니다. 리팩토링의 대상이 되는 서비스들의 설계가 명확하면 된다.

  • 특정 UseCase나 domain에 대해서만 명확하게 설계를 잡을수도 있다.

Acceptance test 확보하기

리팩토링이지만 대규모 작업을 하기 위해선 작업의 안정성을 확보하기 위한 test는 필수다.
마틴 파울러도 리팩토링을 커버할 수 있는 충분한 test가 필요하다고 말한다.
리팩토링이 안전하게 됐는지 검증해줄 수 있는 test가 분명 필요하다.

이 test는 SQE 팀에서 만든 test일수도 있고, 배포 전에 팀 내에서 수행하던 acceptance test일 수도 있다. 혹은 둘 다 일수도 있고.
test가 없다면 만들어야한다. 수정하지 않고 리팩토링만 하고 내보내겠다며 test를 수행하지 않는 것은 문제다.

완전히 커버할 수 있는 test를 만들기는 어렵다.
그렇다고 리팩토링하며 모든 모듈의 test를 작성할 여유가 없을 수 있다.
그렇지만 기본적인 시나리오를 커버하는 acceptance test는 있어야 한다.
개인적으로 big refactoring에선 unit test가 큰 효용성을 갖기는 어렵고 acceptance test 같은 상위 수준의 test가 필요하다고 생각한다.

점진적으로 작업하기

이미 말했듯, 리팩토링을 한번에 하는 것은 쉽지 않다.
그리고 대규모 리팩토링으로 인한 장애가 있을수도 있다.

아키텍쳐 리팩토링에서 가장 중요한 것 중 하나는 점진적으로 작업하는 것이다.
legacy 코드들을 보다보면 아키텍쳐 변경 외에도 손대고 싶은 로직들이 많다.
‘신기하게도 지저분한 코드를 만들었네’ 하는 생각과 ‘이걸 어떻게 오류 없이 돌렸을까’ 하는 생각도 들고, 때로는 오류를 발견하기도 한다.

이런 로직에 대한 수정들이 아키텍쳐와 같이 반영되면, 장애가 발생했을 때 아키텍쳐로 인한 것인지, 로직 수정에 의한 것인지 알 수 없다.
로직은 잠시 내려놓고 아키텍쳐에 대한 리팩토링만 반영하고 배포까지 되도록 한다.
점진적인 작업에서 중요한건 잦은 배포다.
잦은 배포로 안정적인 rollback point를 만들어두고 나면 혹시 잘못된 수정이 있더라도 리팩토링된 안전한 코드들을 확보할 수 있다.

인터페이스 나누기

어느정도 사용하지 않는 코드도 걷어내고, 리팩토링 아키텍쳐도 설계했고, 신뢰할 수 있는 테스트도 준비됐다면 실제 리팩토링은 어떤 작업부터 하면 좋을까?
모든 개발이 그렇듯 인터페이스를 나누는 것이 가장 먼저가 되야한다.
그렇기 때문에 아키텍쳐를 설계를 먼저하는 것이기도 하고.

리팩토링에서 가장 중요한건 인터페이스를 끊어내고 리디자인된 인터페이스로 옮겨가는 것이다.
한번에 갈 수 없기 때문에 인터페이스를 먼저 작업하고 부분 부분 옮겨가야한다. 도메인 인터페이스를 나누고, UseCase를 나누고, Port 인터페이스를 분리하는 작업들을 한번에 하기보다는 하나의 도메인, 하나의 UseCase, 하나의 Port 씩 인터페이스를 생성하고 옮겨가는 방향이 명확하다.

인터페이스 방향을 잡아놓으면 새로 추가되는 클래스들은 리디자인된 인터페이스 방향에 맞춰 개발할 수 있다.
인터페이스를 나눌 때 패키지의 위치도 중요하다.
여러 인터페이스 레벨에서 이게 adapter의 인터페이스인지, application의 인터페이스인지

도메인 분리하기

Hexagonal + DDD의 리팩토링을 초점에 둔다면, 리팩토링을 통해 얻을 수 있는 것은 도메인을 명확하게 하고 로직을 분리할 수 있다는 것이다.
인터페이스를 나누면 어느정도 정리가 되지만 도메인을 분리하면 장점은 너무 많다. DDD 자체의 장점을 다 가지게 되는 것이나 마찬가지니까.

코드 이해도를 확연하게 높일 수 있고 중복 제거도 된다.
도메인 로직 보호와 도메인 언어로 서비스 코드를 관리할 수도 있다.

그런데, 생각보다 10년 묵은 legacy 코드를 도메인으로 분리하는건 쉽지 않았다.
인터페이스만 나눈다고 될 일도 아니고 도메인 로직이 프로젝트 전반에 걸쳐 사용되고 있었기 때문이다.

도메인 로직을 분리하는 팁이라고 하자면,

  1. 우선 대상 aggregate을 만든다.
    • aggregate의 꼴이 새로 DDD를 했을 때와는 다르겠지만 현재 legacy 코드와 호환할 수 있는 aggregate를 만든다.
    • aggregate와 legacy에서 사용하는 class들 사이의 converter를 만든다.
      • 이 converter는 리디자인된 aggregate을 사용하는 로직들에서 legacy 코드들과 호환할 때 convert 한 후 사용할 수 있는 도구.
  2. aggregate에 도메인 로직을 넣는다.
    • 도메인 개념이 없는 코드에선 도메인 로직이 여기저기 흩어져있다.
    • 이런 경우엔 중복코드가 여러곳에 있고 코드가 파편화되어 있기도 하다.
    • 도메인 개념의 로직들을 하나씩 도메인 모델로 밀어넣고 코드를 정리한다.
  3. 대상 aggregate의 repository를 만든다.
    • 서비스에 따라 필요한 CRUD가 다를 수 있으나 보통은 몇 가지 인터페이스의 CRUD가 있기 마련이다.
    • aggregate의 CRUD 중 가능한 기능부터 repository에 추가하고 분리한다.
    • repository의 개념은 db의 repository와 다를 수 있고 이를 명확하게 잡는 것이 중요하다. (이건 DDD 개념이 있어야 함)
  4. repository를 서비스에 적용한다.
    • repository의 모든 기능이 만들어질 필요는 없다. read 중 하나의 함수만 완성되었다면 그걸 서비스에 적용한다.
    • 서비스 로직에서 도메인 개념없이 read를 하고 있었다면 이것만으로도 가독성이 향상되고 지저분한 로직을 제거하고 곳곳에서 호출되던 중복된 repository 로직들을 제거할 수 있다.

이 정도만 해도 도메인이 어느정도 정리된다.
이 과정이 부분부분 반복되다보면 도메인 서비스로 분리할 것들이 보이기도 한다.
그치만 legacy 코드를 전부 이렇게 만드는 것만 하더라도 오랜 시간이 걸린다.

reference

  • 도메인 주도 설계 첫걸음 13장
  • 스트렝글러 패턴
  • 리팩토링, 마틴 파울러