Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

테스트 없는 길고 지저분한 Rails 컨트롤러, 이렇게 리팩토링하세요

Rails 개발을 하다 보면 언젠가 프로그래밍을 그만두고 싶어질 만큼 끔찍한 컨트롤러와 마주치게 됩니다. 하나의 기능에 필요한 코드가 전부 한곳에 들어 있고, 인스턴스 변수로 서로 통신하는 before_filter가 15개쯤 달려 있는데, 반드시 특정 순서로 호출해야 하고 순서가 어긋나면 코드가 폭발합니다. 그리고 필연적으로 그 컨트롤러의 테스트는 대략 이런 모습입니다:

test "index" do
  get :index
  assert_response :success
end

멋지네요. 100% 테스트 커버리지라고 할 수 있겠죠?

눈을 감고 존재하지 않는 것처럼 넘어갈 수 있다면 좋겠지만, 언젠가는 이런 컨트롤러의 버그를 고쳐야 하는 날이 옵니다. 그리고 좋은 개발자라면 코드를 발견했을 때보다 더 나은 상태로 남겨두고 싶어 합니다.

하지만 믿을 만한 테스트도 없이 어떻게 리팩토링할 수 있을까요?

일단 테스트를 확보하자 (어떻게든)

안심하고 리팩토링하려면 좋은 테스트가 필요하지만, 정작 이런 코드에는 리팩토링하기 전까지는 좋은 테스트를 작성하기 어렵습니다. 그럼 뭘 해야 할까요?

다행히 컨트롤러가 아무리 엉망으로 작성되어 있어도, 데이터를 보내고 응답을 검증하는 통합 테스트는 작성할 수 있습니다. 당장은 리팩토링 과정에서 컨트롤러의 기존 동작이 변하지 않는지 확인하는 테스트를 작성하세요.

이런 테스트는 단위 테스트만큼 정밀하지 않습니다. 하지만 이런 상위 수준의 테스트가 있으면 앞으로 진행할 리팩토링이 모든 것을 망가뜨리지 않을 것이라는 확신을 얻을 수 있습니다. 게다가 테스트를 작성하는 과정 자체가 코드를 더 깊이 이해하는 데 도움이 되고, 그 덕분에 어떻게 리팩토링할지 판단하기도 쉬워집니다.

숨겨진 의존성을 드러내라

나쁜 컨트롤러 코드를 나쁘게 만드는 것은 무엇일까요? 대부분의 경우 before_filter, 헬퍼 메서드, 200줄짜리 함수의 여러 부분 사이에 존재하는 암묵적인 의존성 때문입니다. 때로는 너무 이른 시기에 리팩토링을 했기 때문일 수도 있습니다. 코드를 개선하려면 이 의존성들을 끊어내야 합니다.

Rails 컨트롤러라면 의존성을 끊는 쉬운 방법이 있습니다. before_filter, 헬퍼 메서드, 상위 클래스 등 컨트롤러 관련 코드가 숨어 있을 만한 곳에서 코드를 복사해 오세요. 그다음 해당 메서드 호출을 복사해 온 코드로 직접 교체합니다.

일부러 DRY 원칙을 잠시 깨는 것입니다. 나중에 더 이해하기 쉬운 방식으로 다시 리팩토링하기 위해서입니다. 지저분해 보이지만, 이제 모든 코드가 눈앞에 드러납니다. 어떤 조각들이 서로 상호작용하는지, 전체 흐름이 어떻게 이어지는지 파악할 수 있습니다.

(이 과정에서는 변경할 때마다 테스트를 실행해서 코드 인라인화가 문제를 일으키지 않는지 반드시 확인해야 합니다.)

테스트 가능한 코드로 리팩토링하라

이제 코드를 다시 리팩토링할 준비가 되었습니다. 기본적인 메서드 추출(extract method), 객체 추출(extract object), 메서드 상향(pull up method) 같은 리팩토링 기법을 주로 사용하게 될 겁니다. 여러 가지 방식으로 리팩토링해 보면서 어떤 구조가 가장 자연스러운지 살펴보세요.

처음 몇 번의 작업에서는 상위 수준의 통합 테스트를 안전망 삼아 진행할 수 있습니다. 하지만 곧 더 탄탄한 장치가 필요해질 겁니다.

리팩토링하면서 코드를 더 테스트하기 쉽게 만들 기회를 적극적으로 찾으세요. 보통 이것은 테스트 대역(test double)을 주입할 수 있는 지점을 만들고, 객체 간 의존성을 줄이며, 컨트롤러가 쉽게 생성하고 단위 테스트할 수 있는 객체에 의존하도록 만드는 것을 의미합니다. 코드를 테스트 가능한 객체로 옮기면 컨트롤러는 점점 작아지고, 이해하기 쉬워지며, 스스로도 테스트하기 쉬워집니다.

번호 목록으로 정리하면?

거대한 컨트롤러를 분해하는 절차를 다시 정리하면 다음과 같습니다:

  1. 컨트롤러를 대상으로 상위 수준의 통합 테스트를 작성하고 실행합니다.
  2. 테스트를 돌려서 모두 통과하는지 확인합니다.
  3. before_filter, 상위 클래스 메서드 등 코드를 숨기고 있는 추상화를 인라인으로 펼칩니다.
  4. 테스트를 돌려서 여전히 통과하는지 확인합니다.
  5. 리팩토링을 수행합니다(메서드 추출, 서비스 객체 추출, 인스턴스 변수를 지역 변수로 교체 등). 코드가 더 나아졌는지 느껴봅니다.
  6. 테스트를 돌려서 여전히 통과하는지 확인합니다.
  7. 방금 코드를 추출했다면, 추출한 객체나 메서드에 대한 단위 테스트를 작성합니다.
  8. 테스트를 돌려서 여전히 통과하는지 확인합니다.
  9. 컨트롤러에 아직 손볼 곳이 있다면 5번으로 돌아갑니다.

다음은 무엇일까?

더 배우고 싶다면 『Working Effectively with Legacy Code』(레거시 코드 활용 전략)를 읽어보세요. 테스트 없는 유지보수 불가능한 코드를 다룰 수 있는 코드로 바꾸는 방법을 다루는 바이블 같은 책입니다. 거대한 모놀리식 컨트롤러를 자주 마주하게 된다면(그리고 저처럼 리팩토링이 즐겁게 느껴진다면) 강력히 추천합니다.

자, 이제 여러분의 공포 스토리를 들려주세요! 지금까지 다룬 최악의 컨트롤러 코드는 어떤 모습이었나요?