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

엣지 케이스가 '완벽한 해결책'에 구멍을 뚫을 때

완벽해 보이던 해결책, 그리고 시작된 예외의 연쇄

정신 나갔던 테스트 문제에 대한 완벽한 해결책을 찾았습니다. DEFAULT_HOST 상수만 오버라이드하면 모든 게 풀릴 것 같습니다.

다만 지저분한 경고 메시지를 없애려면 warnings를 꺼야 합니다. 하지만 이제 모든 테스트가 통과하고, 고친 건 겨우 몇 줄뿐입니다!

그런데 호스트를 오버라이드하지 않고 싶은 테스트가 딱 하나 있습니다. 괜찮습니다. 상수를 다시 오버라이드하고, warnings도 다시 끄고, 테스트가 끝나면 원래대로 되돌려 놓으면 됩니다. 거의 다 온 것 같아 손에 잡힐 듯합니다!

그런데… 그런데… 그런데…

며칠 뒤, 스물일곱 번째로 막히고 앱이 온통 핵(hack) 덩어리가 된 현실 앞에서 뒤로 물러나 문득 묻게 됩니다. 내가 대체 이러고 있는 거지? 이 해결책이 문제보다 더 나쁜 건 아닐까?

너무 영리한 코드에서 어떻게 벗어날까

처음 아이디어로는 전체 문제를 해결할 수 없다는 게 분명합니다. 그렇다면 처음 아이디어가 못 풀었던 엣지 케이스까지 모두 커버하는 더 나은 아이디어는 어떻게 만들 수 있을까요?

답은 '만들 수 없다'입니다. 과도하게 영리한 코드는 더 영리한 코드로 이길 수 없습니다. 적어도 직접적으로는요. 반대 방향으로 가세요. 단순하게, 정직하게.

구체적으로 무슨 뜻일까요?

추상화하려던 코드를 그냥 인라인으로 풀어 넣으세요. 중복을 감수하세요. 코드를 명시적으로 유지하세요.

DEFAULT_HOST 상수를 다른 기본 호스트로 바꾸려고 했다면, '기본값'이라는 개념 자체를 버리세요. 매번 직접 지정하면 됩니다.

즉, 이렇게 하는 대신:

test/integration/welcome_test.rb
require 'test_helper'

silence_warnings do
  Rack::Test::DEFAULT_HOST = "www.justinweiss.com"
end

class WelcomeTest < ActionDispatch::IntegrationTest
  include Rack::Test::Methods
  
  test "can visit the homepage" do
    get "/"
    # ...
  end

  # ...
end

이렇게 하세요:

test/integration/welcome_test.rb
require 'test_helper'

class WelcomeTest < ActionDispatch::IntegrationTest
  test "can visit the homepage" do
    get "https://www.justinweiss.com/"
    # ...
  end
  # ...
end

겉보기에 완벽했던 해결책이 무너질 때마다, 그 이유는 결국 나중에 처리해야 할 엣지 케이스를 미처 상상하지 못했기 때문이었습니다.

괜찮습니다. 우리는 미래를 예측할 수 없으니까요. 다만 그 징후를 발견하면 파기를 멈추세요. 패치 위에 패치를 얹지 말고, 처음 세운 해결책을 걷어낸 뒤 더 나은 것을 추출하세요.

더 나은 해결책을 추출하는 방법

코드를 명시적이고 직관적인 형태로 모두 펼쳐 놓으면, 재구성할 방법들이 자연스럽게 보이기 시작합니다.

보통은 적절한 위치에서 메서드 추출(Extract Method)이나 클래스 추출(Extract Class)을 적용하는 것만으로 충분합니다. 관건은 그 '적절한 위치'가 어디인지 판단하는 것이죠. 그런데 반복되는 코드가 눈앞에 그대로 드러나 있으면 이 판단이 훨씬 쉬워집니다.

그리고 상속과 위임(delegation)을 적극 활용하세요. 너무 영리해지지 않으면서 코드를 깔끔하게 정리하는 데 도움이 되는 단순한 빌딩 블록입니다.

마지막으로 한 가지 더

문서를 읽는 것도 잊지 마세요:

test/integration/welcome_test.rb
require 'test_helper'

class WelcomeTest < ActionDispatch::IntegrationTest

  setup do
    # This already exists:
    host! "www.justinweiss.com"
  end
  
  test "can visit the homepage" do
    get "/"
    # ...
  end
  # ...
end

항상 이렇게 답이 명확하진 않습니다. 하지만 클래스 세 개와 젬(gem) 하나까지 동원해서 풀려던 문제의 답이, 메서드 하나짜리 내장 기능으로 이미 존재한다는 걸 깨달을 때만큼 겸손해지는 순간도 없습니다.

결국에는 더 나은 해결책이 남는다

두 번째 해결책은 보통 첫 번째보다 모든 면에서 낫습니다.

왜 그럴까요?

  • 개발자로서 경험이 더 쌓였습니다.

    좋은 코드가 무엇인지 더 잘 알게 됩니다.

  • 자신이 만든 시스템을 더 깊이 이해하게 됩니다.

    새로 작성하는 코드가 시스템 안에서 어떤 역할을 해야 할지 더 나은 결정을 내릴 수 있습니다.

  • 어떤 가정이 틀렸는지 알게 됩니다.

    상상 속의 문제가 아니라 실제 존재하는 문제에 맞춰 해결책을 설계할 수 있습니다.

그리고 마지막에는, 여러분의 최고의 영리함이 제자리를 찾을 수도 있습니다. 이번엔 핵 없이 말이죠.

파기는 멈춰야 합니다

영리한 코드를 작성하는 건 즐겁습니다. Ruby에서는 특히 쉽기도 하죠. 그리고 잘못된 곳으로 가고 있다는 걸 알면서도 그 길을 계속 걷기가 특히 쉽습니다.

'뭔가 이상하다'는 불길한 느낌이 들면 잠시 멈추세요. 코드를 다시 펼치고(un-factor), 문서를 읽고, 명시적이고 직관적인 코드로 되돌리세요. 그리고 더 나은 방법을 찾으세요.

끝이 보이지 않는 구덩이를 스스로 계속 팠던 마지막 순간이 언제였는지 기억하시나요? 그때 여러분은 어떻게 빠져나왔나요? 그리고 결과물로 남은 코드는 어떤 모습이었나요?