Rails 앱을 개발하다 보면 다른 데이터 저장소를 활용하면 훨씬 쉽게 해결할 수 있는 문제를 만나곤 합니다. 예를 들어, 사이트에서 질문에 답변한 사용자에게 부여된 포인트를 기준으로 순위를 매기는 리더보드(leaderboard)가 있다고 생각해 봅시다. Redis의 Sorted Set을 사용하면 구현의 상당 부분이 이미 준비되어 있어 매우 편리합니다. 그런데 여기서 한 가지 고민이 남습니다. Redis와 통신하는 코드는 대체 어디에 두어야 할까요?
1. User 모델이 직접 Redis와 통신하는 방식
가장 먼저 떠오르는 방법은 User 모델이 직접 Redis와 통신하는 것입니다.
class User < ActiveRecord::Base
def award_points(points)
Redis.current.zincrby("leaderboard", points, id)
end
end
하지만 이렇게 되면 User 모델은 사용자를 표현하는 본래의 역할에 더해, Redis와 통신하고 리더보드까지 관리하는 책임을 떠안게 됩니다. 그 결과 User 클래스는 이해하기 어려워지고 테스트하기도 까다로워집니다. 이는 우리가 원하는 방향과 정반대죠.
2. Redis 통신을 전담하는 별도 클래스 만들기
더 나은 접근 방식은, Redis 요청을 "Redis를 어떤 용도로 사용하는지"를 나타내는 새로운 객체로 감싸는 것입니다. 예를 들어 Redis 통신을 전담하는 Leaderboard 클래스를 만드는 것이죠. 이 클래스는 ActiveRecord::Base를 상속하지 않으면서도 여전히 app/models 디렉터리에 위치할 수 있습니다.
class Leaderboard
def award_points_to_user(user_id, points)
Redis.current.zincrby("leaderboard", points, user_id)
end
end
class User < ActiveRecord::Base
def award_points(leaderboard, points)
leaderboard.award_points_to_user(id, points)
end
end
두 클래스 모두 app/models 디렉터리에 함께 둘 수 있지만, 이제는 ActiveRecord 모델에 불필요한 로직이 섞여 들어가지 않습니다. Leaderboard 클래스가 Redis와의 통신을 전담하기 때문에, User 클래스는 리더보드가 내부적으로 어떻게 구현되었는지 신경 쓸 필요가 없습니다. 덕분에 테스트 작성도 한결 수월해집니다.
새로운 책임에는 새로운 클래스를
새로운 책임이 생길 때마다 그 책임을 맡을 새로운 클래스를 만드는 것은 큰 이득이며, app/models는 그런 클래스들을 보관하기에 아주 좋은 장소입니다. 외부 서비스의 도움을 받아 기능을 구현하는 이점은 그대로 누리면서도, 코드는 여전히 읽기 쉽고 다루기 편한 상태로 유지할 수 있습니다.
직접 적용해 보세요
ActiveRecord 모델, 컨트롤러, 혹은 뷰에서 네트워크 서비스와 직접 통신하는 코드가 있나요? 그 코드를 ActiveRecord가 아닌 별도의 데이터 모델로 옮겨 보세요. 코드가 이해하기 쉽고, 다루기 편하며, 테스트하기 좋아졌는지 스스로 확인할 수 있을 것입니다. 시도해 보신 뒤 결과가 어땠는지 이메일로 알려주셔도 좋습니다!
마지막으로 이 말을 기억해 두세요.
컴퓨터 과학의 모든 문제는 간접 계층(indirection)을 하나 더 추가하면 해결할 수 있다.
— 데이비드 휠러(David Wheeler)
…단, 간접 계층이 너무 많아지는 문제만은 예외다.
— 케블린 헨니(Kevlin Henney)