"리팩토링: 루비 에디션" 후기

수정 #refactoring#book

왜 이 책을 읽었나

당시 팀장님은 PHP를 쓰면서도 Rails의 철학을 많이 녹인 코드베이스를 가지고 있었습니다. 스캐폴딩, CoC, 모델과 테이블 이름의 컨벤션 같은 것들이 자연스럽게 들어가 있었고, 팀에서도 그 방식의 영향을 꽤 받았습니다.

그래서 Rails에 관심이 생겼습니다. 단순히 Ruby on Rails라는 프레임워크가 궁금했다기보다, 왜 저런 컨벤션이 생산성을 만든다고 보는지 알고 싶었습니다.

그 무렵 사내 발표를 준비하고 있었습니다. 발표 주제를 찾는 과정에서 『리팩토링』 책이 Ruby판으로도 있다는 걸 봤습니다. 리팩토링 책은 워낙 유명했지만, 머릿속에는 Java 예제로 된 고전이라는 이미지가 더 강했습니다.

Ruby판이라면 Rails 쪽 감각을 익히면서 리팩토링이라는 주제도 같이 볼 수 있겠다고 생각했습니다. 발표 준비 겸 책을 읽고, 정리한 내용을 블로그에도 남기게 됐습니다.

막상 읽어보니 Ruby 문법을 배우는 책이라기보다, 이미 코드 안에서 감으로 하던 정리 작업에 이름을 붙여주는 책에 가까웠습니다. “이런 경우엔 메서드를 빼야지”, “이 조건문은 먼저 return 하는 편이 낫겠다” 같은 판단을 이전에도 하긴 했습니다. 다만 그걸 패턴으로 분류해서 생각해본 적은 많지 않았습니다.

이 책에서 좋았던 점은 거창한 설계 이야기를 하기 전에, 아주 작은 코드 이동부터 다룬다는 점이었습니다. 메서드를 옮기고, 이름을 바꾸고, 조건문을 풀고, 파라미터를 줄이는 일들입니다. 실무에서 매일 하는 일인데 막상 설명하려면 뭉뚱그려 “코드를 정리했다”라고 말하게 되는 것들입니다.

Ruby로 읽으니 다르게 보인 것

Ruby는 Rails를 이해하려고 접한 언어였는데, 생각보다 신선했습니다.

특히 컬렉션을 다루는 방식이 좋았습니다.

arr = (1...4).select { |x| x % 2 == 0 }
# => [2]

JavaScript에서 lodash를 자주 쓰고 있었기 때문에 select, map, inject 같은 메서드는 낯설면서도 금방 익숙해졌습니다. 함수를 넘긴다기보다 코드 블록을 넘기는 느낌도 처음에는 어색했지만, 익숙해지면 표현이 짧고 읽기 좋았습니다.

Ruby판 리팩토링 책을 읽는 장점도 여기 있었습니다. 같은 리팩토링이라도 Java식 클래스 구조로만 볼 때와, Ruby의 블록·모듈·동적 타입 위에서 볼 때 감각이 조금 달랐습니다.

예를 들어 위임을 줄이거나 모듈을 추출하는 이야기는 Ruby의 Forwardable이나 module include와 자연스럽게 연결됩니다. 다른 언어에서도 같은 원칙은 쓸 수 있지만, Ruby에서는 그 문법이 훨씬 가볍게 느껴졌습니다.

감으로 하던 일을 이름 붙이기

책 초반의 메서드 정리 부분은 아주 낯설지는 않았습니다. 긴 메서드를 나누고, 설명이 필요한 조건문을 메서드로 빼고, 매개변수를 줄이는 일은 이미 경험으로 어느 정도 하고 있었습니다.

그런데 익숙하다는 것과 설명할 수 있다는 것은 달랐습니다.

예전에는 코드를 보며 “이건 좀 지저분하다”라고 느끼고 고쳤습니다. 책을 읽고 나니 그 지저분함을 조금 더 구체적으로 말할 수 있었습니다. 메서드가 다른 객체의 데이터를 더 많이 쓰고 있으니 옮겨야 한다든지, 같은 조건 조각이 반복되니 밖으로 빼야 한다든지, 매개변수 묶음이 계속 같이 다니니 객체로 묶어야 한다든지 하는 식입니다.

이 차이가 생각보다 컸습니다. 혼자 코드를 고칠 때는 감으로도 충분할 수 있습니다. 하지만 팀에서 리뷰하거나 설득할 때는 “느낌상 별로다”보다 “이건 Move Method에 가까운 상황이다”라고 말하는 편이 훨씬 낫습니다.

객체 사이의 책임 이동

가장 먼저 눈에 들어온 건 Move Method, Move Field, Extract Class 같은 항목이었습니다.

코드를 짜다 보면 처음에는 편의상 한 클래스에 메서드를 둡니다. 그런데 시간이 지나면 그 메서드가 자기 클래스보다 다른 클래스의 데이터를 더 많이 읽고 있는 경우가 생깁니다. 이때 메서드는 사실 다른 객체의 책임에 가까워진 것입니다.

이런 리팩토링은 단순히 파일을 예쁘게 나누는 일이 아니었습니다. “이 책임은 누구의 것인가”를 다시 묻는 일이었습니다.

비슷하게 Hide Delegate와 Remove Middle Man도 인상적이었습니다. 내부 객체를 감추기 위해 위임 메서드를 두는 것은 좋지만, 위임만 하는 메서드가 너무 많아지면 그 자체가 과잉 중개자가 됩니다. 감추는 것과 드러내는 것 사이에도 균형이 필요했습니다.

데이터 구조를 더 말하게 만들기

데이터 체계화 챕터에서는 Replace Array with Object, Replace Hash with Object, Replace Magic Number with Symbolic Constant 같은 항목이 기억에 남았습니다.

배열이나 해시로 값을 담으면 처음에는 빠릅니다. 하지만 시간이 지나면 row[0], row[1] 같은 접근이 무엇을 뜻하는지 코드만 봐서는 알기 어려워집니다.

row = ["Liverpool", 15]

이런 값은 처음에는 단순해 보이지만, 이름과 승수라는 의미가 숨어 있습니다.

team.name
team.wins

객체로 바꾸면 코드가 조금 길어질 수는 있지만, 데이터가 스스로를 설명하기 시작합니다.

상수도 마찬가지였습니다. 숫자 자체는 틀리지 않아도, 그 숫자가 무엇을 뜻하는지 설명하지 않으면 읽는 사람이 계속 추측해야 합니다. 3, 7, 30 같은 값이 비즈니스 규칙을 뜻하기 시작하면 이름을 붙여야 합니다.

조건문은 의도를 가린다

조건문 간결화 챕터는 지금도 자주 떠올립니다.

가장 실용적인 것은 Replace Nested Conditional with Guard Clauses였습니다. 중첩된 if-else는 모든 분기가 같은 무게를 가진 것처럼 보이게 만듭니다. 하지만 실제 코드에서는 대부분 정상 흐름과 예외적인 흐름이 다릅니다.

예외적인 케이스를 앞에서 return 시키면, 핵심 흐름이 훨씬 잘 보입니다.

return 2 if days_rented > 2
1

이런 코드는 짧아서 좋은 것이 아니라, 드문 경우를 먼저 걷어내고 기본 흐름을 남긴다는 점에서 좋았습니다.

Remove Control Flag도 같은 맥락이었습니다. done = false 같은 플래그를 두고 루프 안에서 상태를 바꾸는 방식은 읽는 사람이 계속 플래그의 생애를 따라가야 합니다.

done = false
until done do
  if condition
    done = true
  end
  value -= 1
end

반면 조건을 만족했을 때 바로 return하면 흐름이 단순해집니다.

loop do
  return value if condition
  value -= 1
end

읽는 사람의 머릿속에 남겨야 하는 상태가 줄어드는 것이 핵심이었습니다.

메서드 호출을 단순하게 만들기

메서드 호출 단순화에서는 이름과 파라미터 이야기가 많았습니다.

Rename Method는 당연한 이야기처럼 보이지만, 막상 제일 자주 실패하는 부분이기도 합니다. 이름이 애매하면 코드를 읽는 사람이 구현을 열어봐야 합니다. 이름이 정확하면 구현을 열어보기 전에 어느 정도 믿고 지나갈 수 있습니다.

Separate Query from Modifier도 좋았습니다. 값을 조회하는 메서드가 내부 상태까지 바꾸면, 호출하는 쪽에서는 부작용을 예상하기 어렵습니다. 조회와 변경을 분리하면 테스트도 쉬워지고, 코드를 읽을 때도 마음이 편합니다.

매개변수가 늘어날 때의 대응도 기억에 남습니다. 계속 같이 다니는 값들이 있으면 Introduce Parameter Object를 생각해볼 수 있습니다. 반대로 객체에서 여러 값을 꺼내 하나씩 넘기고 있다면 Preserve Whole Object가 더 자연스러울 수 있습니다.

이런 항목들은 거창한 패턴이라기보다, 코드를 읽는 사람의 부담을 줄이는 작은 선택에 가까웠습니다.

상속보다 중요한 것

일반화 처리 챕터에서는 상속과 위임을 다시 보게 됐습니다.

처음 객체지향을 배울 때는 공통 기능이 보이면 상속을 떠올리기 쉽습니다. 하지만 책에서는 상속을 만들고, 접고, 위임으로 바꾸고, 다시 계층으로 바꾸는 여러 방향을 같이 보여줍니다.

Pull Up Method와 Push Down Method는 공통 동작을 어디에 둘지 묻습니다. Replace Inheritance with Delegation은 하위 클래스가 상위 클래스의 일부 기능만 쓰고 있다면 굳이 상속할 필요가 없다고 말합니다.

이 부분을 읽으면서 상속은 재사용의 도구라기보다 관계를 강하게 고정하는 선택에 가깝다고 느꼈습니다. 공통 코드가 있다고 바로 부모 클래스를 만들기보다, 그 관계가 앞으로도 맞을지 먼저 생각해야 합니다.

Ruby의 module도 흥미로웠습니다. Java의 interface나 abstract class와 완전히 같지는 않지만, 여러 클래스에 공통 동작을 섞어 넣을 수 있다는 점에서 Ruby다운 일반화 방식으로 보였습니다.

남은 감상

책의 모든 항목이 똑같이 와닿지는 않았습니다. 어떤 항목은 너무 당연해 보였고, 어떤 항목은 실무에서 자주 볼 일이 있을까 싶었습니다. 책 자체도 패턴을 길게 나열하는 구조라 한 번에 쭉 읽으면 조금 건조하게 느껴졌습니다.

그래도 남는 것은 있었습니다.

리팩토링은 코드를 “예쁘게” 만드는 일이 아니라, 다음 변경을 덜 무섭게 만드는 일입니다. 읽는 사람이 기억해야 할 것을 줄이고, 책임을 더 맞는 자리로 옮기고, 데이터와 조건문이 의도를 드러내게 만드는 일입니다.

이 책을 읽고 나서 코드 리뷰에서 쓰는 말이 조금 달라졌습니다. 예전에는 “이건 좀 길다”, “분리하는 게 좋겠다” 정도로 말했는데, 이제는 왜 분리해야 하는지 조금 더 구체적으로 말할 수 있게 됐습니다.

결국 그게 제일 큰 수확이었습니다. 이미 감으로 하던 일을, 다른 사람과 공유할 수 있는 언어로 바꾸는 것.

용어 짧게 정리

  • 리팩토링: 겉으로 보이는 동작은 유지하면서 코드의 내부 구조를 개선하는 작업입니다.
  • Guard Clause: 예외적인 조건을 앞에서 먼저 return해 핵심 흐름을 덜 중첩되게 만드는 방식입니다.
  • Move Method: 메서드가 실제로 더 의존하는 클래스로 메서드를 옮기는 리팩토링입니다.
  • Extract Class: 한 클래스가 여러 책임을 갖게 됐을 때 일부 책임을 새 클래스로 분리하는 리팩토링입니다.
  • Delegation: 직접 처리하지 않고 다른 객체에 일을 맡기는 방식입니다.
  • Parameter Object: 함께 전달되는 파라미터 묶음을 하나의 객체로 만드는 방식입니다.
← 목록으로