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

관점만 바꾸면 코드가 달라진다: 얽힌 스파게티 코드 풀기

그 거대한 if 문 덩어리가 눈앞에서 계속 쳐다보고 있습니다. 분명 단순화할 수 있을 것 같은데, 막상 손대면 비즈니스 로직이 발목을 잡죠.

예를 들어 견적서(Quote)를 만들어 판매하는 플랫폼이 있다고 해보죠. 견적서에는 여러 개의 품목(LineItem)이 담깁니다. 그런데 규칙이 좀 꼬여 있습니다. 광고(Ad) 품목은 중복으로 담을 수 있는데, 웹사이트(Website) 품목이 여러 개라면 가격을 합산해 한 줄로 표시해야 합니다. 게다가 웹사이트를 구매하려는데 견적서에 이미 광고가 5개 이상 들어 있다면, 웹사이트 가격에 20% 할인을 적용해야 하고요.

노트북을 창밖으로 던지는 소리가 여기까지 들리는 것 같군요.

이런 규칙들을 if 문으로 처리할 수도 있습니다:

class Quote
  attr_accessor :line_items
 
  ... 
 
  def add_line_item(line_item)
    if line_item.kind_of?(Ad)
      self.line_items << line_item
    elsif line_item.kind_of?(Website)
      if @line_items.select {|item| item.kind_of?(Ad) }.length >= 5
        # TODO: 소수점 아래 몇 푼은 제 은행 계좌에 넣어두자
        line_item.price *= 0.8
      end
      existing_website = self.line_items.detect { |item| item.kind_of?(Website) }
      if existing_website
        existing_website.price += line_item.price
      else
        self.line_items << line_item
      end
    end
  end
end

하지만 인정하자면, 이건 정말 끔찍한 코드입니다. 이렇게 얽힌 코드를 어떻게 풀 수 있을까요?

메서드를 잘게 쪼개는 방법도 있지만, 그건 장난감을 옷장에 쑤셔 넣고 방을 치웠다고 엄마에게 보여주는 것과 다름없습니다. 그리고 저 kind_of? 호출들은 여전히 마음에 걸리고요.

그런데 만약 견적서(Quote)의 관점이 아니라 품목(LineItem)의 관점에서 상황을 바라본다면 어떨까요? 어떤 종류의 품목인지 물어보고 견적서에 추가하는 대신, 그냥 품목에게 스스로 견적서에 자신을 추가하라고 명령하는 겁니다.

메서드를 뒤집어 보세요!

제가 즐겨 쓰는 리팩토링 기법 중 하나는 호출자(caller)와 피호출자(callee)의 역할을 뒤집어 보는 것입니다. 위 코드를 예시로 바꿔보겠습니다:

app/models/quote.rb
class Quote
  ...
  def add_line_item(line_item)
    line_item.add_to_quote(self)
  end
end
app/models/line_item.rb
class Ad < LineItem
  ...
  def add_to_quote(quote)
    quote.line_items << self
  end 
end
app/models/website.rb
class Website < LineItem
  def add_to_quote(quote)
    if quote.line_items.select {|item| item.kind_of?(Ad) }.length >= 5
      # TODO: 소수점 아래 몇 푼은 제 은행 계좌에 넣어두자
      self.price *= 0.8
    end
    existing_website = quote.line_items.detect { |item| item.kind_of?(Website) }
    if existing_website
      existing_website.price += self.price
    else
	  quote.line_items << self
    end
  end
end

아직 완벽하지 않습니다. website.rb는 여전히 많은 리팩토링이 필요하고, 메서드를 뒤집으면서 line_items의 캡슐화가 깨진 점도 마음에 걸립니다.

그럼에도 첫 번째 층위의 복잡성은 제거했습니다. 이제 코드를 LineItem에 둘지 Quote에 둘지, 어디가 가장 자연스러운지에 따라 선택할 수 있습니다. LineItem 객체들은 상속과 믹스인(mixin)을 활용해 서브클래스 간의 공통점과 차이점을 처리할 수 있죠. 게다가 새로운 LineItem 서브클래스를 추가할 때 add_line_item 메서드를 부풀리지 않아도 됩니다.

코드는 조금 더 깔끔해지고, 훨씬 유연해졌습니다. 전반적으로 충분히 이득이라고 말할 수 있죠.

이 패턴을 쓰지 않는 편이 나은 경우

'메서드 뒤집기'가 유용한 만큼, 이 패턴을 피해야 할 이유도 있습니다:

  • 캡슐화가 깨질 수 있습니다. 원래 외부에 노출하고 싶지 않았던 Quote 객체의 속성을 공개해야 할 수도 있습니다.

  • 결합도가 높아질 수 있습니다. QuoteAd가 이제 서로를 알아야 합니다. 서로 알아야 하는 정도가 클수록 코드는 오히려 복잡해질 수 있습니다.

  • Ad의 단일 책임 원칙(SRP)을 위반할 수 있습니다. 이제 Ad는 자신을 Quote에 추가하는 방법까지 알아야 하는 책임을 지게 됩니다.

이런 문제들은 대부분 우회할 수 있습니다. 하지만 반드시 인지하고 있어야 합니다. 리팩토링이 코드를 오히려 더 나쁘게 만들면 안 되니까요!

제가 이 기법을 좋아하는 이유

이런 단점에도 불구하고, 이것은 제가 가장 좋아하는 리팩토링 중 하나입니다. 이 패턴을 적용한 뒤 작성하는 코드는 대체로 더 명확하고 확신에 차 있습니다.

심지어 결과물이 만족스럽지 않을 때조차, 이 패턴을 적용하는 과정 자체가 객체 간 관계를 다른 시각으로 바라보게 만듭니다. "이 기능은 정말 최악이다, 이 지저분한 코드를 짜야 한다니 믿을 수 없다"는 늪에 빠졌을 때, 머릿속에 새로운 해결책을 떠올리게 해 주는 계기가 되죠. 코드를 다르게 구조화할 방법을 강제로 고민하게 만든다는 점이 놀랍도록 유용합니다.

직접 코드에 적용해 보세요

제가 좋아하는 많은 패턴처럼, '메서드 뒤집기'도 『Smalltalk Best Practice Patterns』에서 처음 접했고, 그때부터 값진 도구로 자리 잡았습니다.

다음에 비슷하지만 동작이 조금씩 다른 객체들을 다루느라 고민될 때, 이 기법을 한번 시도해 보세요! 새 코드가 더 마음에 든다면 그대로 가져가면 됩니다. 설령 그렇지 않더라도, 더 나은 코드로 이어지는 사고의 길을 열어줄 것입니다.