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

ActiveSupport의 작은 모듈로 더 나은 글로벌 데이터 관리하기

일반적으로 전역 변수는 좋지 않습니다. 하지만 때로는 적절한 위치에 놓인 전역 변수 하나가 코드를 훨씬 단순하게 만들어 주기도 합니다.

Rails에서는 요청(request) 중에 한 번 설정한 데이터를 애플리케이션의 모든 계층에서 사용해야 하는 경우가 있습니다. 이 요청을 보낸 사용자는 누구인가요? 어떤 권한을 가지고 있나요? 어떤 데이터베이스 연결을 사용해야 할까요?

이런 정보는 코드 전반에서 접근 가능해야 하므로, 매번 인자로 넘기면 코드가 온통 잡음으로 가득 차게 됩니다. 하지만 Ruby 전역 변수를 사용하거나 클래스 변수를 설정하면 그 자체로 새로운 문제가 생깁니다. 여러 스레드가 값을 덮어쓸 수 있고, 결국 큰 혼란과 심하면 복구 불가능한 잘못된 데이터까지 초래할 수 있습니다.

필요한 것은 전역처럼 동작하지만, 오직 해당 요청에만 유효한 무언가입니다.

순수 Ruby 방식

보통은 Thread.current[]로 이 문제를 처리합니다. 다음과 같이 사용합니다:

Thread.current[:current_user] = user

아주 간단하고 나쁘지 않은 해결책입니다. 하지만 두 가지 큰 단점이 있습니다:

  1. 누군가 실수로 데이터를 덮어쓸 수 있습니다.

    두 사람이 같은 키를 선택하면 어떻게 될까요? 서로의 데이터를 덮어쓰게 됩니다. 저장하는 값이 사용자 정보라면 심각한 문제가 됩니다. 직접 만든 애플리케이션에서는 큰 문제가 없을 수 있지만, 젬(gem)을 개발하거나 Rails 앱이 크고 복잡하다면 반드시 고려해야 할 사항입니다.

  2. 구조화되어 있지 않습니다.

    Thread.current[]는 그저 커다란 데이터 자루에 불과합니다. 문서화하기도 어렵고, 무엇을 꺼낼 수 있는지 알아내려면 무엇을 넣었는지 일일이 검색해야 합니다.

    물론 이 정도로 전역 데이터를 많이 다루고 있다면 Thread.current[]의 구조 부족보다 더 시급한 고민이 있겠지만, 그래도 염두에 둘 점입니다.

그렇다면 Thread.current[]보다 더 나은 방법이 있을까요? 짐작하셨겠지만, Rails 자체도 수많은 요청 로컬(request-local) 데이터를 관리합니다. 그리고 ActiveSupport라는 보물창고에는 따라 할 만한 또 다른 패턴이 있습니다.

Rails 방식

ActiveSupport를 파헤쳐 보면 ActiveSupport::PerThreadRegistry를 만나게 됩니다. 이름에서 짐작할 수 있듯이, 바로 우리가 찾던 것입니다.

ActiveSupport::PerThreadRegistry를 사용하면 앞선 예제는 다음과 같이 바뀝니다:

class RequestRegistry
  extend ActiveSupport::PerThreadRegistry

  attr_accessor :current_user, :current_permissions
end

RequestRegistry.current_user = user
RequestRegistry.current_user # => user

약간 더 손이 가긴 합니다. 하지만 ActiveSupport::PerThreadRegistry를 사용하면 다음과 같은 이점이 있습니다:

  • 문서화할 공간이 생깁니다. 클래스를 보는 사람이라면 어떤 전역 데이터를 기대하는지, 어떤 전역 데이터를 사용할 수 있는지 정확히 알 수 있습니다.

  • 깔끔한 API를 얻습니다. 실제로 클래스 변수를 설정하는 것처럼 보입니다. 스레드를 신경 쓰지 않아도 된다면 아마 그렇게 작성했을 코드와 똑같습니다.

  • 클래스에 설정한 데이터는 네임스페이스로 분리됩니다. RequestRegistry.current_userPhoneNumberApiRegistry.current_user와 충돌할 걱정이 없습니다. 완전히 독립적으로 취급됩니다.

Rails 내부에서도 ActiveRecord 커넥션 관리, EXPLAIN 쿼리 저장, ActiveSupport::Notifications 등에 ActiveSupport::PerThreadRegistry를 사용합니다. 따라서 PerThreadRegistry의 강력함에 대한 좋은 예시를 더 찾고 있다면 Rails 소스 코드 자체가 훌륭한 출발점입니다.

스레드와 요청이 일치하지 않을 때

일부 Rails 서버에서는 하나의 스레드가 여러 요청을 처리합니다. 이 경우 PerThreadRegistry에 넣은 데이터가 다음 요청까지 남아 있게 됩니다. 아마 원하는 동작은 아닐 겁니다.

Rails가 그러하듯, PerThreadRegistry에 넣은 데이터를 다 사용한 후 직접 정리하면 됩니다. 무엇을 저장했느냐에 따라 어차피 정리 작업이 필요할 수도 있습니다.

댓글에서 MattB가 더 쉬운 해결책인 request_store를 소개했습니다. Thread.current[]처럼 동작하지만, 요청이 끝나면 스스로 비워집니다.

이 덕분에 request_store의 안전성과 PerThreadRegistry의 API를 결합한 PerRequestRegistry를 만들 여지가 열려 있습니다. 이미 만들어 둔 분이 있다면 꼭 알려주세요!

직접 사용해 보세요

전역 데이터는 나쁠 수 있습니다. 전역 변수가 너무 많으면 금방 유지보수하기 어려운 코드가 됩니다.

하지만 요청 전체에 걸쳐 데이터를 한곳에 모아 두는 것이 합리적이라면, Thread.current[]보다 한 단계 나아가 보세요. ActiveSupport::PerThreadRegistry나 request_store를 사용해 보세요. 최소한 전역 데이터를 다루는 일이 조금은 덜 위험해질 겁니다.