인스턴스 변수를 가진 모듈은 유해할 수 있습니다
GitLab v19.1Rails는 어디서든 모듈과 인스턴스 변수를 사용하도록 권장하는 경향이 있습니다. 물론 이 방식은 개발할 때 편리합니다. 동일한 컨텍스트 안에 너무 많은 것이 들어있어, 각 요소들이 서로 강하게 결합되어 있는지 또는 서로 의존하고 있는지 알기 어렵습니다.
배경#
Rails는 어디서든 모듈과 인스턴스 변수를 사용하도록 권장하는 경향이 있습니다.
예를 들어, 컨트롤러, 헬퍼, 뷰에서 인스턴스 변수를 사용합니다.
또한 ActiveSupport::Concern의 사용을 권장하는데, 이는 모든 것을 하나의 거대한 단일 객체에 저장한다는 생각을 더욱 강화하며,
사람들이 그 하나의 거대한 객체 안에서 모든 것에 접근할 수 있게 합니다.
문제점#
물론 이 방식은 개발할 때 편리합니다. 필요한 모든 것이 손 닿는 곳에 있기 때문입니다. 하지만 해당 객체가 커질수록 같은 이유로 나중에는 통제 불능 상태가 되는 등 여러 단점이 있습니다.
동일한 컨텍스트 안에 너무 많은 것이 들어있어, 각 요소들이 서로 강하게 결합되어 있는지 또는 서로 의존하고 있는지 알기 어렵습니다. 복잡도가 어느 수준을 넘으면 파악이 매우 어려워지고, 코드 추적도 극도로 힘들어집니다. 예를 들어, 하나의 클래스가 3개의 서로 다른 인스턴스 변수를 사용하고, 그 모든 변수가 3개의 서로 다른 모듈에서 초기화되고 조작될 수 있습니다. 이 변수들이 문제를 일으키기 시작할 때 추적하기가 어렵습니다. 어떤 모듈이 변수 중 하나를 갑자기 변경할지 알 수 없습니다. 모든 것이 무엇이든 건드릴 수 있습니다.
유사한 우려 사항#
다중 상속이 나쁘다고 말하는 사람들이 있습니다.
여러 인스턴스 변수가 여기저기 흩어진 여러 모듈을 혼합하는 것도 같은 문제를 겪습니다.
ActiveSupport::Concern도 마찬가지입니다. 참고:
concerns를 전용 클래스 & 컴포지션으로 대체하는 것 고려
비슷한 아이디어도 있습니다: 데코레이터와 인터페이스 분리를 사용하여 과도하게 커지는 모델 문제 해결
included가 전체 문제를 해결하지는 않는다는 점을 참고하세요.
의존성을 정의하긴 하지만, 각 모듈이 최종 거대 객체의 인스턴스 변수를 통해 암묵적으로 통신하는 것을 여전히 허용하며,
바로 그것이 문제의 핵심입니다.
해결책#
거대한 객체를 여러 객체로 분리하고, 그 객체들이 API, 즉 공개 메서드를 통해 서로 통신하게 해야 합니다. 간단히 말해, 상속보다는 컴포지션입니다. 이 방식으로 각각의 작은 객체는 자신만의 제한된 상태, 즉 인스턴스 변수를 가지게 됩니다. 하나의 인스턴스 변수가 잘못되면, 그것이 그 작은 객체 하나에서 비롯된 것임을 명확히 알 수 있습니다. 다른 어떤 것도 그것을 건드릴 수 없기 때문입니다.
명확하게 정의된 API를 통해 결합도가 낮아지고 디버그 및 추적이 훨씬 쉬워지며, 다른 객체들이 사용하기 위한 확장성도 훨씬 높아집니다. 암묵적인 의존성이 아닌 명확한 방식으로 통신하기 때문입니다.
허용되는 사용#
하지만 모듈에서 인스턴스 변수를 사용하는 것이 항상 나쁜 것은 아닙니다. 같은 모듈 안에서만 사용되는 경우, 즉 다른 모듈이나 객체가 그 변수를 건드리지 않는다면 허용되는 사용입니다.
특히 ||=와 함께 단일 인스턴스 변수를 사용하여 값을 설정하는 경우는 허용합니다.
이는 다음과 같은 모습입니다:
module M
def f
@f ||= true
end
end
안타깝게도 더 복잡한 규칙을 cop에 코딩하기는 쉽지 않으므로, 사람들의 최선의 판단에 의존합니다. cop에 쉽게 추가할 수 있는 또 다른 좋은 패턴을 찾는다면 추가해야 합니다.
이 cop을 비활성화하지 않고 코드를 재작성하는 방법#
cop을 그냥 비활성화할 수도 있지만, 그렇게 하는 것은 피해야 합니다. 일부 코드는 간단한 형태로 쉽게 재작성할 수 있습니다. 다음의 허용 가능한 메서드를 고려해 보세요:
module Gitlab
module Emoji
def emoji_unicode_version(name)
@emoji_unicode_versions_by_name ||=
JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'digests.json')))
@emoji_unicode_versions_by_name[name]
end
end
end
이 메서드는 이미 자기 완결적이기 때문에 완전히 괜찮습니다.
다른 메서드가 @emoji_unicode_versions_by_name을 사용해서는 안 되며, 이는 문제가 없습니다.
하지만 단순히 ||=가 아니기 때문에 cop 위반에 해당하며,
cop은 이것이 괜찮다는 것을 판단할 만큼 스마트하지 않습니다.
반면에, 이 메서드를 두 개로 분리할 수 있습니다:
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', 'digests.json')))
end
end
end
이제 cop은 불평하지 않습니다.
이 cop을 비활성화하는 방법#
같은 줄의 코드 바로 뒤에 비활성화 주석을 추가하세요:
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)
이 방식으로 해당 값들이 어디서 오는지 더 명확해지고, 인스턴스 변수 사용 대신 오타 검사의 이점을 얻을 수 있습니다. 향후에는 파셜에서도 인스턴스 변수 사용을 금지해야 합니다.