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

배경

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 헬퍼와 메일러의 작동 방식으로 인해 우리는 그들의 인스턴스 변수를 사용하지 않을 수도 있을 것입니다. 이런 경우에 우리는 그것들을 무시할 수 있을 것입니다. 이러한 모듈은 다른 임의의 객체들과 공유되지 않기 때문에 어느 정도 격리되어 있습니다.

뷰에서의 인스턴스 변수

뷰에서의 인스턴스 변수는 나쁘다. (컨트롤러의 관점에서) 인스턴스 변수를 사용하는 것이 누구인지, 그리고 (partial의 관점에서) 어디에서 그 값을 설정했는지 쉽게 알 수가 없기 때문에, 데이터 의존성을 추적하기가 매우 어려워집니다.

우리는 이렇게 사용하려고 노력하고 있습니다:

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

그리고 partial 안에서:

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

이렇게 하면, 그 값이 어디서 오는지 더욱 명확해지며, 인스턴스 변수를 사용하는 것에 비해 오타 확인 기능을 얻을 수 있습니다. 미래에는 partials에서의 인스턴스 변수 사용을 금지해야 합니다.