# # 위험 없는 리팩토링: 동작을 그대로 유지하며 모듈 추출하기
모놀리스에서 모듈을 별도 서비스로 옮길 때 주요 목표는 알려진 버그와 문서화되지 않은 특이점까지 동작을 정확히 그대로 보존하는 것이다. 이 단계에서 코드를 "정리"하려는 시도는 종종 회귀 버그를 초래한다. 최소 변경 전략을 사용해 함정을 피하는 방법을 보여주겠다.
'지루한' 접근법이 회귀 버그로부터 당신을 구하는 이유
프로덕션 시스템은 수년간의 운영으로 형성된 암묵적 계약을 가지고 있다: 클라이언트 통합, 오류 처리, 타이밍 지연, 외부 API 적응 등이다. 이 계약은 문서화되지 않았다—로그, 커밋, 팀 경험에 묻혀 있다. 리팩토링은 버그를 포함한 동작을 변경해서는 안 된다. 예를 들어, 기존 코드가 null 대신 빈 문자열을 반환한다면 이를 바꾸면 이미 이 로직에 적응한 클라이언트가 깨진다. 핵심 원칙: 추출은 형태를 바꾸지만 의미론을 보존한다. 원래 동작에서 벗어나면 리팩토링이 예측 불가능한 릴리스가 된다.
한 번에 한 유형의 변경만: 내부 구조 전에 경계 설정
작업을 단계로 나눈다:
- 단계 A: 계약(HTTP, 큐, 버스)을 보존하며 모듈 추출하고 기능 플래그 도입.
- 단계 B: 스택, 라이브러리, 코드 스타일 업데이트.
- 단계 C: 버그 수정.
- 단계 D: 성능 최적화.
단일 PR에서 단계를 섞으면 읽기 힘든 diff, 롤백 골치, "이게 버그야 새로운 기능이야?" 논쟁이 불가피하다. 첫 단계에서는 기존 코드를 감싼다: 새 서비스 경계, 내부에 기존 로직. 예를 들어 라우팅 모듈 추출 시 원래 route() 함수를 유지하되 HTTP 엔드포인트로 감싼다. 이렇게 하면 코드가 "추함" 세 보이더라도 동작이 동일하다.
기능 플래그: 비상 킬 스위치
프로덕션에서 기존 코드와 새 코드를 병행 실행하려면 config나 env 변수로 제어하는 스위치가 필요하다. 이는 다음을 제공한다:
- 인시던트 중 재배포 없이 즉시 롤백.
- 트래픽 일부로 점진적 롤아웃(카나리 릴리스).
- 동작 비교를 위한 기존 구현 장기 보존.
중요하게, 스위치는 서비스 재시작 없이 작동해야 한다. 그렇지 않으면 야간 인시던트가 간단한 config 조정 대신 긴급 호출이 된다. 플래그는 인프라가 아닌 비즈니스 로직 수준에서 구현—오케스트레이터에 복잡한 의존성 피하기.
동작을 복사하라, 다시 쓰지 마라
안전한 추출 지침: 빌드에 필요한 부분만 변경하며 파일을 "그대로" 복사. 실제 예—액터에서 순수 함수로 라우팅 이동:
object UpdateRouting {
def route(update: Update): Either[RoutingError, RouteTarget] =
if (update.message.exists(_.chat.isGroupOrSupergroup)) Right(GroupFlow)
else if (update.callbackQuery.isDefined) Right(CallbackFlow)
else Right(PrivateFlow)
}
모델 매핑 시 미묘한 세부 사항까지 보존. 기존 코드가 누락된 channelId를 빈 문자열로 변환했다면 똑같이:
def toBusMessage(in: PlatformInMessage): BusIncoming =
BusIncoming(
correlationId = newCorrelationId(),
channelId = in.channelId.getOrElse("")
)
어떤 의미론 변경(빈 문자열을 null로 바꾸기 등)도 다른 서비스와의 계약을 깨뜨린다. 목표: 비트 단위로 동일한 동작, 코드가 구식처럼 보여도.
추출 중 절대 피할 것
기능 플래그가 활성인 동안 다음은 피하라:
- 새 라이브러리. 각각 transitive 의존성과 공격 표면 추가. 스택 업데이트는 별도 작업.
- 코드 스타일 변경. 기존 도메인 코드와 새 래퍼 섞는 건 안전을 위한 일시적 비용.
- 발견된 버그 수정. 티켓 로그하고 안정화 후 처리. 그렇지 않으면 모든 인시던트가 리팩토링 탓.
- '파일 열린 김에' 최적화. 프로파일링과 속도 향상은 별도 사이클.
이 제한이 과도해 보일 수 있지만, 6개월 후 git blame에서 명확한 히스토리: "모듈 추출, 버스 연결"이 아니라 "모든 것 다시 씀"이 아니다.
테스트: KPI가 아닌 계약 고정
순수 함수(라우팅, 매핑)에 대한 단위 테스트는 추출 이전 동작 캡처. 추출 후 녹색 테스트는 로직 변경 없음 보장. 통합 시나리오(네트워크, 큐, DB)에는 E2E 테스트 사용, 하지만 한계 유의:
- 외부 시스템 목은 코드의 잘못된 가정 영속화.
- 실제 API는 테스트 미커버 미묘한 특이점(예:
/api/prefix) 가짐.
항상 프로덕션과 유사한 스테이징 환경에서 수동 실행 포함. 자동화는 거의 실제 조건 체크 대체 못 함.
코드 증가가 버그가 아니다, 프로세스 단계다
첫 단계 후 코드 양이 불어나는 건 정상: 어댑터, 기능 플래그, 모놀리스 중복. 안전한 롤백과 단계적 롤아웃 대가다. 정리와 축소는 경계 안정, 기존 코드 제거, 테스트로 정확성 확인 후 두 번째 단계. 선행 최적화 말고 먼저 작동 확인.
핵심 요약
- 추출 목표—전면 개선이 아닌 동작 "그대로" 이전.
- 기능 플래그 필수—배포 없이 안전 롤백.
- 테스트는 추출 전 계약 고정, 후 아님.
- 스택 변경, 버그 수정, 최적화 병행 금지.
- 코드 증가 가능—전략 일부, 실수 아님.
레거시 코드 리팩토링은 실험 장소 아님. 프로세스 지루할수록 새벽 3시 프로덕션 다운 wakeup 적다. 이 규칙 따르면 위험한 추출이 예측 가능 작업 된다.
— Editorial Team
아직 댓글이 없습니다.