인스턴스 변수를 사용하는 모듈들은 해로울 수 있다

배경

Rails는 어떤 이유로든 사람들이 컨트롤러, 헬퍼, 뷰에서 인스턴스 변수를 사용하는 것을 장려하고 있습니다. 또한 ActiveSupport::Concern의 사용을 장려하고 있으며, 이는 모든 것을 거대하고 단일한 객체에 저장하여 그 객체에 모든 것을 액세스할 수 있도록 하는 아이디어를 더욱 강화합니다.

문제점들

물론, 모든 것을 손쉽게 이용할 수 있어 개발하는 데 편리할 수 있습니다. 그러나 선택한 객체가 커지면, 동일한 이유로 제어하기 어려워질 수 있습니다.

같은 문맥에 너무 많은 것들이 있으며, 서로 얽힌 것인지 아닌지, 서로에게 의존하는 것인지 아닌지를 알 수 없습니다. 복잡성이 어느 정도로 커지면 알기가 매우 어려워지고 코드를 추적하기도 매우 어려워집니다. 예를 들어, 한 클래스가 3가지 다른 인스턴스 변수를 사용하고, 그 모두가 3가지 다른 모듈에서 초기화되고 조작될 수 있습니다. 이러한 변수들이 문제를 일으킬 때 추적하기가 어렵습니다. 어떤 모듈이 갑자기 변수 중 하나를 변경할지 모릅니다. 모든 것이 어떤 것이라도 터치할 수 있습니다.

유사한 우려들

다중 상속을 사용하는 것이 좋지 않다는 주장이 있습니다. 여러 모듈과 여기저기 흩어져 있는 다중 인스턴스 변수를 섞는 것도 똑같은 문제를 겪을 수 있습니다. 같은 이슈가 ActiveSupport::Concern에도 해당됩니다. 관련이 있다면, 특정 클래스 및 구성으로 관심사(concerns) 대체 고려를 참조하세요.

유사한 아이디어가 있습니다: 과도한 모델 크기 문제 해결을 위한 데코레이터와 인터페이스 분리 사용

included가 전체 문제를 해결하지는 않는다는 점을 주목하세요. 이들은 의존성을 정의하지만, 여전히 최종 거대한 객체 내의 인스턴스 변수를 통해 모듈 간에 암시적으로 대화할 수 있게 합니다. 여기서 문제가 발생합니다.

해결책

우리는 거대한 객체를 여러 객체로 나눠야 하며, 그들은 API, 즉 공개 메서드를 통해 서로 통신해야 합니다. 요약하자면, 상속 대신 구성. 이렇게 하면 각 작은 객체가 자체적으로 제한된 상태, 즉 인스턴스 변수를 갖게 됩니다. 하나의 인스턴스 변수가 문제를 일으키면, 그것이 해당 단일 작은 객체에서 나온 것임을 명확히 알 수 있습니다. 다른 누구라도 그것을 접근할 수 없기 때문입니다.

명확히 정의된 API로 인해, 사물들은 덜 얽히고 디버깅 및 추적이 훨씬 쉬워지며, 암시적인 의존성이 아닌 명확한 방법으로 통신하기 때문에 다른 객체들이 확장하기도 더 쉬워집니다.

허용되는 사용

그러나 모듈 내에서 인스턴스 변수를 사용하는 것이 항상 나쁜 것은 아닙니다. 해당 변수가 다른 모듈이나 객체에 접촉되지 않는다면 허용될 수 있습니다.

우리는 특히 일부 것들이 ||=로 값을 설정하는 한 인스턴스 변수를 사용하는 것이 허용됩니다. 다음과 같이 보일 것입니다:

module M
  def f
    @f ||= true
  end
end

불행히도, 보다 복잡한 규칙을 코피에 코딩하는 것은 쉽지 않으므로, 우리는 사람들의 최선의 판단에 의존합니다. 좋은 패턴을 찾을 경우, 이것을 코프에 쉽게 추가할 수 있습니다.

이 코프를 비활성화하고 다시 작성하는 방법

코프를 비활성화할 수는 있지만, 그렇게 하는 것은 피해야 합니다. 어떤 코드는 간단한 형태로 다시 작성할 수 있을 수도 있습니다. 가능한 경우 다음과 같은 방법을 고려하세요:

module Gitlab
  module Emoji
    def emoji_unicode_version(name)
      @emoji_unicode_versions_by_name ||=
        JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json')))
      @emoji_unicode_versions_by_name[name]
    end
  end
end

이 메서드는 이미 스스로 포함되어 있기 때문에 완전히 괜찮습니다. 다른 메서드들이 @emoji_unicode_versions_by_name을 사용하면 안 되고, 우리는 좋습니다. 그러나 여전히 코프에서 문제를 일으키지만, 이것이 단순히 ||= 때문이며, 코프가 이게 괜찮다고 판단하지 못할 뿐입니다.

반면, 이 메서드를 두 가지로 나눌 수도 있습니다:

module Gitlab
  module Emoji
    def emoji_unicode_version(name)
      emoji_unicode_versions_by_name[name]
    end
    
    private
    
    def emoji_unicode_versions_by_name
      @emoji_unicode_versions_by_name ||=
        JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json')))
    end
  end
end

이제 코프는 불평하지 않습니다.

이 코프를 비활성화하는 방법

코드 바로 뒤에 비활성화 주석을 넣으세요:

module M
  def violating_method
    @f + @g # rubocop:disable Gitlab/ModuleWithInstanceVariables
  end
end

여러 줄인 경우, 섹션을 위해 활성화 및 비활성화할 수도 있습니다:

module M
  # rubocop:disable Gitlab/ModuleWithInstanceVariables
  def violating_method
    @f = 0
    @g = 1
    @h = 2
  end
  # rubocop:enable Gitlab/ModuleWithInstanceVariables
end

해당 지점 이하는 아무것도 확인되지 않기 때문에 언젠가는 활성화해야 합니다.

현재 무시해야 할 것들

Rails 헬퍼와 메일러의 작동 방식 때문에 인스턴스 변수의 사용을 피할 수 없을 수도 있습니다. 이러한 경우에는 현재 그것들을 무시할 수 있습니다. 해당 모듈들은 다른 무작위 객체들과 공유되지 않으므로 어느 정도 격리되어 있습니다.

뷰 내의 인스턴스 변수

인스턴스 변수를 사용하는 것은 나쁩니다. (컨트롤러의 관점에서) 누가 인스턴스 변수를 사용하고 있는지, (부분 뷰의 관점에서) 어디에서 설정했는지 쉽게 알 수 없기 때문에 데이터 의존성을 추적하기가 매우 어렵습니다.

우리는 다른 방식을 사용하려고 노력하고 있습니다:

= render 'projects/commits/commit', commit: commit, ref: ref, project: project

부분 뷰에서는:

- ref = local_assigns.fetch(:ref)
- commit = local_assigns.fetch(:commit)
- project = local_assigns.fetch(:project)

이 방법을 통해 값들이 어디서 왔는지 명확해지며, 인스턴스 변수 사용 대신 오탈자 검사의 혜택을 얻을 수 있습니다. 미래에는 부분 뷰에서의 인스턴스 변수 사용을 금지해야 합니다.