리팩토링(refactoring)은 코드의 동작은 그대로 유지하면서 내부 구조와 품질을 개선하는 작업을 의미합니다. 리팩토링을 꾸준히 적용하면 코드를 읽고 수정하기가 훨씬 쉬워지고, 버그 발생 가능성도 크게 줄어듭니다.
이 글에서는 루비(Ruby) 개발자라면 반드시 알아두어야 할 대표적인 리팩토링 기법들을 실제 예제와 함께 소개합니다.
1. 메서드 추출(Extract Method)
가장 널리 쓰이는 리팩토링 기법 중 하나가 바로 '메서드 추출'입니다. 기존 메서드 안의 특정 코드 블록을 새로운 메서드로 분리하는 방식으로, 그 결과 이름만 보고도 역할을 파악할 수 있는 작고 명확한 메서드들이 만들어집니다.
먼저 리팩토링 전 코드를 살펴보겠습니다.
@sold_items = %w( onions garlic potatoes )
def print_report
puts "*** Sales Report for #{Time.new.strftime("%d/%m/%Y")} ***"
@sold_items.each { |i| puts i }
puts "*** End of Sales Report ***"
end
이 메서드에서 가장 지저분한 부분은 현재 날짜를 생성하는 코드입니다. 이 부분부터 별도의 메서드로 추출해 보겠습니다.
def print_report
puts "*** Sales Report for #{current_date} ***"
@sold_items.each { |i| puts i }
puts "*** End of Sales Report ***"
end
def current_date
Time.new.strftime("%d/%m/%Y")
end
코드가 훨씬 읽기 좋아졌습니다. 여기서 한 걸음 더 나아가, 나머지 로직도 역할별로 분리하면 다음과 같은 구조가 됩니다.
def print_report
print_header
print_items
print_footer
end
def print_header
puts "*** Sales Report for #{current_date} ***"
end
def current_date
Time.new.strftime("%d/%m/%Y")
end
def print_items
@sold_items.each { |i| puts i }
end
def print_footer
puts "*** End of Sales Report ***"
end
코드의 전체 길이는 늘었지만, 각 메서드가 하나의 역할만 담당하기 때문에 가독성이 크게 향상되었습니다. 작은 메서드를 만드는 것을 두려워하지 마세요. 잘게 나눠진 메서드야말로 좋은 코드의 기본입니다.
2. 조건문 리팩토링
복잡하고 길게 늘어진 조건문도 의미 있는 이름의 메서드로 추출하면 훨씬 읽기 쉬워집니다.
예제:
def check_temperature
if temperature > 30 && (Time.now.hour >= 9 && Time.now.hour <= 17)
air_conditioner.enable!
end
end
if 문의 두 번째 조건은 직관적이지 않습니다. 이 조건을 별도의 메서드로 추출해 보겠습니다.
def check_temperature
if temperature > 30 && working_hours
air_conditioner.enable!
end
end
def working_hours
Time.now.hour >= 9 && Time.now.hour <= 17
end
여기서 한 핵심은 조건에 설명력 있는 이름을 부여했다는 점입니다. 덕분에 이 코드를 나중에 읽는 사람(미래의 자신 포함!)이 조건의 의미를 한눈에 파악할 수 있습니다.
3. 메서드 객체로 대체(Replace Method with Method Object)
때로는 통제를 벗어나 거대하게 자란 메서드를 마주하게 됩니다. 이런 메서드는 지역 변수가 많아 리팩토링하기가 까다롭습니다. 이럴 때 유용한 해결책이 '메서드 객체(Method Object)' 기법입니다.
"거대한 메서드는 클래스가 숨어버리는 장소다." — Robert C. Martin (Uncle Bob)
예제를 살펴보겠습니다.
require 'socket'
class MailSender
def initialize
@sent_messages = []
end
def send_message(msg, recipient = "rubyguides.com")
raise ArgumentError, "message too small" if msg.size < 5
formatted_msg = "[New Message] #{msg}"
TCPSocket.open(recipient, 80) do |socket|
socket.write(formatted_msg)
end
@sent_messages << [msg, recipient]
puts "Message sent."
end
end
sender = MailSender.new
sender.send_message("testing")
리팩토링을 위해 새로운 클래스를 만들고, 기존의 지역 변수들을 인스턴스 변수로 승격시킵니다. 이렇게 하면 데이터를 일일이 주고받는 번거로움 없이 추가 리팩토링을 자유롭게 진행할 수 있습니다.
리팩토링 후의 MailSender 클래스는 다음과 같습니다.
class MailSender
def initialize
@sent_messages = []
end
def deliver_message(message)
send(message)
@sent_messages << message
puts "Message sent."
end
def send(msg)
TCPSocket.open(msg.recipient, 80) { |socket| socket.write(msg.formatted_msg) }
end
end
그리고 새롭게 도입된 Message 클래스입니다.
class Message
attr_reader :msg, :recipient
def initialize(msg, recipient = "rubyguides.com")
raise ArgumentError, "message too small" if msg.size < 5
@msg = msg
@recipient = recipient
end
def formatted_msg
"[New Message] #{msg}"
end
end
sender = MailSender.new
msg = Message.new("testing")
sender.deliver_message(msg)
이제 메시지 생성 로직과 전송 로직이 각각의 클래스로 깔끔하게 분리되어, 단일 책임 원칙에 더욱 충실한 설계가 되었습니다.
마무리
오늘 소개한 리팩토링 기법들을 활용하면 단일 책임 원칙(Single Responsibility Principle)을 자연스럽게 지키면서 클래스와 메서드의 크기를 관리 가능한 수준으로 유지할 수 있습니다.
이 글이 도움이 되셨다면 주변 동료 개발자들과 공유해 보세요!