Ruby 다중 스레드 환경에서의 공유 상태 관리: ActiveSupport::CurrentAttributes의 이해와 활용

How do `CurrentAttributes` work? - Abhijeet Anand

작성자
Ruby Australia
발행일
2025년 01월 25일

핵심 요약

  • 1 Ruby 다중 스레드 환경에서 클래스 변수와 같은 공유 상태는 데이터 오염 및 예기치 않은 동작을 유발하는 주요 원인입니다.
  • 2 Rack::Lock, Mutex, Thread Local Variables 등 기존의 공유 상태 관리 방법들은 성능 저하, 복잡성, 그리고 파이버 기반 환경에서의 한계점을 가집니다.
  • 3 ActiveSupport::CurrentAttributes는 Rails 요청 생명주기에 통합되어 스레드 또는 파이버 단위로 격리된 상태를 제공하지만, 여전히 전역 상태의 특성과 서브 파이버에서의 한계점을 인지하고 신중하게 사용해야 합니다.

도입

Ruby는 다중 스레드 환경을 기반으로 동작하며, 웹 애플리케이션 서버인 Puma를 비롯한 다양한 구성 요소들이 스레드를 활용합니다. 이러한 환경에서 클래스 변수와 같은 공유 상태를 부주의하게 사용하면 여러 스레드가 동시에 접근하여 데이터를 덮어쓰거나 예상치 못한 오류를 발생시킬 수 있습니다. 본 발표는 Ruby 다중 스레드 시스템에서 발생하는 공유 상태 관리의 문제점을 지적하고, 이를 해결하기 위한 다양한 접근 방식들을 소개하며, 특히 Rails의 ActiveSupport::CurrentAttributes가 제공하는 기능과 그 한계점을 심층적으로 탐구합니다.

다중 스레드 환경에서 공유 상태 관리의 문제점은 여러 예시를 통해 명확히 드러납니다.

공유 상태 문제점의 예시

  • 파일 분할 서비스: 클래스 레벨 메서드에서 파일을 분할하고 결과를 클래스 변수에 저장할 경우, 여러 요청이 동시에 처리되면 다른 요청의 데이터가 덮어씌어지는 문제가 발생합니다.
  • 페이지네이션 인덱싱: 유사하게, 다중 스레드 시스템에서 컨텍스트 스위치가 발생하면 클래스 변수에 저장된 인덱스 값이 예상과 다르게 변경되어 오류를 일으킬 수 있습니다.
  • 과거의 실패 경험: 발표자는 과거에 주문 처리 시스템에서 자체적인 동기화 로직(별도 프로세스 및 스레드 활용)을 구현했으나, 이는 클래스 변수를 사용한 “매우 나쁜 코드”의 전형적인 예시로, 유지보수와 디버깅에 큰 어려움을 겪었음을 고백합니다.

공유 상태 관리의 대안과 한계

  • Mutex: 상호 배제를 통해 동시 접근을 막는 기본적인 방법이지만, 구현의 복잡성과 잠재적인 버그를 유발할 수 있습니다. 발표자는 Mutex 사용 경험이 “끔찍했다”고 언급합니다.
  • Rack::Lock 미들웨어: 전체 요청을 단일 Mutex로 감싸 동시성을 제한합니다. 이는 전체 애플리케이션의 성능을 심각하게 저하시키며, 다중 스레드 기반의 Puma와 같은 서버의 이점을 상쇄합니다.
  • Thread Local Variables (Thread.current): 스레드별로 독립적인 데이터를 저장할 수 있어 공유 상태 문제를 해결하는 것처럼 보입니다. 그러나 Puma와 같은 서버는 스레드를 재사용하므로, 요청 처리 후 스레드 로컬 변수를 명시적으로 초기화(nil로 설정)하지 않으면 이전 요청의 데이터가 다음 요청으로 유출될 수 있습니다. 또한, ApplicationController에서 상태를 관리하는 것은 비즈니스 로직과 상태 관리의 결합을 야기하여 코드의 응집도를 떨어뜨립니다.

ActiveSupport::CurrentAttributes의 활용

Rails 5.2부터 도입된 ActiveSupport::CurrentAttributes는 이러한 문제에 대한 개선된 접근 방식을 제공합니다. * 기능: Thread.current와 유사하게 스레드 레벨 저장소에 상태를 저장하지만, Rails 요청 생명주기(Request Cycle)에 자동으로 연결되어 요청 시작 시 설정되고 요청 종료 시 초기화됩니다. * 활용: CurrentAttributes를 상속받는 클래스를 정의하고, 필요한 속성(예: username, locale)을 지정하여 사용합니다. set 메서드를 통해 요청 사이클 외부(예: 백그라운드 작업)에서도 사용하거나, with 블록을 통해 임시로 값을 오버라이드할 수 있습니다. * 격리 수준: 기본적으로 스레드 기반 격리를 사용하며, 필요에 따라 파이버 기반 격리(scope: :fiber)도 지원합니다. 이는 Thread.current 또는 Fiber.current를 사용하여 데이터를 저장하는 방식입니다.

CurrentAttributes의 한계점

  • 전역 상태의 본질: CurrentAttributes는 내부적으로 여전히 전역 상태(Singleton 인스턴스)를 관리하며, 이는 “마법”처럼 작동하지만, 여러 곳에서 속성을 설정할 경우 “블랙 매직”이 되어 디버깅을 어렵게 만들 수 있습니다.
  • 서브 파이버 문제: 파이버 기반 서버(예: Falcon)나 async gem을 사용하는 환경에서 fiber 격리 수준을 설정하더라도, 서브 파이버는 부모 파이버의 데이터를 자동으로 상속받지 못하는 문제가 있습니다. Ruby 3.x에서 도입된 Fiber.storage가 이 문제의 해결책으로 제시됩니다.
  • 작업 간 데이터 전달 불가: 백그라운드 작업(Job) 간에는 CurrentAttributes를 통해 데이터를 전달할 수 없으므로, ID와 같은 필수 정보를 명시적으로 전달해야 합니다.
  • “명시적인 것이 암시적인 것보다 낫다”: 발표자는 Ryan Biggs의 의견에 동의하며, CurrentAttributes의 암시적인 특성보다는 Devisecurrent_user와 같이 명시적인 메서드를 통한 접근이 테스트 용이성 및 예측 가능성 측면에서 더 안전할 수 있다고 강조합니다. 모델 내부에서 요청 관련 정보를 필요로 하는 경우, CurrentAttributes는 편리할 수 있으나, 콘솔이나 테스트 환경에서의 설정 복잡성으로 인해 취약할 수 있습니다.

결론

Ruby의 다중 스레드 환경에서 공유 상태 관리는 애플리케이션의 안정성과 예측 가능성에 중대한 영향을 미치는 복잡한 문제입니다. ActiveSupport::CurrentAttributes는 Rails 요청 생명주기에 통합되어 스레드 또는 파이버 수준의 격리된 상태 관리를 제공함으로써 기존의 클래스 변수 사용이나 수동적인 스레드 로컬 변수 관리에 비해 개선된 솔루션입니다. 그러나 이는 여전히 전역 상태의 한계를 내포하며, 특히 파이버 기반 비동기 환경에서는 추가적인 고려가 필요합니다. 따라서 `CurrentAttributes`를 사용할 때는 그 편의성과 잠재적인 "마법"에 의존하기보다, 데이터 흐름과 설정 지점을 명확히 이해하고, "명시적인 것이 암시적인 것보다 낫다"는 원칙을 염두에 두어 신중하게 접근하는 것이 중요합니다. 필요한 경우, ID를 명시적으로 전달하거나 `Devise`와 같은 검증된 패턴을 활용하는 것이 더 견고한 아키텍처를 구축하는 데 도움이 될 것입니다.

댓글 0

댓글 작성

0/1000
정중하고 건설적인 댓글을 작성해 주세요.

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!