인스턴스 변수를 사용하는 모듈들은 해로울 수 있다
배경
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)
이 방법을 통해 값들이 어디서 왔는지 명확해지며, 인스턴스 변수 사용 대신 오탈자 검사의 혜택을 얻을 수 있습니다. 미래에는 부분 뷰에서의 인스턴스 변수 사용을 금지해야 합니다.