하고 싶은 일은 분명한데 코드가 말을 안 듣는 경험, 누구나 한 번쯤 겪어봤을 겁니다. 들여쓰기가 몇 단계나 깊어졌거나, 메서드 체이닝이 여섯 개쯤 이어져 있거나, 어딘가 비대칭스럽게 보입니다. 원인이 무엇이든, 뭔가 어색하게 느껴집니다. 그냥 무시할 수도 있습니다. 아직 작성해야 할 기능이 백로그에 산더미처럼 쌓여 있고, 사실 그렇게 심각하지 않다고요. 하지만 그건 실수입니다. 당신의 코드는 지금 무언가를 말하려 하고 있으며, 그 신호를 놓쳐서는 안 됩니다.
코드가 어색하게 느껴지는 순간을 알아차릴 줄 안다면, 소프트웨어 설계 실력이 빠르고 확실하게 향상됩니다. 이런 직관은 경험, 멘토링, 코드 리뷰를 통해 길러지는 것이기 때문에 쉽게 얻기 어렵습니다. 하지만 도움을 줄 수 있는 라이브러리들이 있습니다. 바로 나쁜 코드가 나쁘게 느껴지도록 만드는 '문법적 식초(syntactic vinegar)'를 사용하는 라이브러리입니다.
문법적 식초는 어떤 모습일까?
Ruby에 기본으로 포함된 가벼운 목킹(mocking)·스터빙(stubbing) 라이브러리인 minitest/mock을 활용한 예제를 살펴보겠습니다.
require 'minitest/mock'
class CartTest < MiniTest::Test
def test_error_message_set_on_charge_failure
cart = Cart.new(items)
cart.stub(:charge!, false) do
cart.checkout!
assert_equal "The credit card could not be charged", cart.credit_card_error
end
end
end
테스트를 실행하면 Cart 객체의 charge! 메서드가 스텁(stub) 처리되어, 실제 결제 시스템에는 접근하지 않습니다. 블록 문법 덕분에 원하는 순간에만 정확히 스텁을 적용할 수 있다는 점도 장점입니다. 그런데 여러 개의 메서드를 한꺼번에 스텁해야 한다면 어떻게 될까요?
require 'minitest/mock'
class CartTest < MiniTest::Test
def test_error_message_set_on_charge_failure
payment_processor = PaymentProcessor.new
cart = Cart.new(items, processor: payment_processor)
payment_processor.stub(:charge!, false) do
payment_processor.stub(:login!, true) do
payment_processor.stub(:logout!, true) do
cart.checkout!
assert_equal "The credit card could not be charged", cart.credit_card_error
end
end
end
end
end
우와, 들여쓰기가 상당히 깊어졌습니다. 게다가 문제는 이것이 단 하나의 테스트 코드일 뿐이라는 점입니다. 다른 여러 테스트에서도 같은 코드가 반복될 것임을 충분히 짐작할 수 있습니다.
물론 이런 중첩 구조 전체를 테스트 헬퍼 메서드로 감싸버릴 수도 있습니다. 하지만 코드에 정말로 귀를 기울이고 있다면, 이는 더 나은 방법을 찾으라는 메시지로 받아들여야 합니다. 이제 Test Double(테스트 대역)을 도입할 때가 된 것일지도 모릅니다.
class TestPaymentProcessor < PaymentProcessor
def login!(account_id, key)
true
end
def charge!(amount, credit_card)
credit_card.can_be_charged?
end
def logout!
true
end
end
class CartTest < MiniTest::Test
def test_error_message_set_on_charge_failure
test_payment_processor = TestPaymentProcessor.new
cart = Cart.new(items, processor: test_payment_processor)
cart.credit_card = failing_credit_card
cart.checkout!
assert_equal "The credit card could not be charged", cart.credit_card_error
end
end
이제 테스트 코드가 훨씬 읽기 좋아졌습니다. 게다가 만들어 둔 TestPaymentProcessor는 다른 여러 곳에서 재사용할 수 있습니다. 개발 환경에서 실제 서버에 요청을 보내고 싶지 않다면, 개발 모드에서 활용할 수도 있습니다!
나쁜 코드는 나쁘게 느껴져야 한다
나쁜 코드를 명확하게 드러내는 독단적인(opinionated) 라이브러리를 사용하다 보면, 나쁜 코드를 훨씬 빠르고 일관되게 알아차리게 됩니다. 그 결과 앞으로 작성할 코드는 더 깔끔하고, 읽기 쉬우며, 유지보수하기 덜 괴로운 코드가 됩니다.
여러분이 즐겨 사용하는 독단적인 라이브러리가 무엇인지, 그리고 그것이 나쁜 코드를 발견하고 수정하는 데 어떻게 도움이 되는지 댓글로 알려주세요!