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

Ruby on Rails 개발자가 반드시 알아야 할 일반적인 문제와 핵심 정리

Ruby on Rails 패턴과 안티패턴 시리즈의 마지막 편에 오신 것을 환영합니다. 이 주제들을 연구하고 글로 옮기는 과정은 꽤 긴 여정이었습니다. 이번 포스트에서는 지난 몇 년간 Rails 애플리케이션을 개발하고 배포하면서 직접 겪었던 가장 흔한 문제들을 다뤄보려고 합니다.

여기서 소개하는 내용들은 코드 어디에서나 적용할 수 있는 보편적인 원칙입니다. 따라서 MVC(Model-View-Controller) 패턴에 국한된 이야기가 아니라 일반적인 관점으로 읽어주시면 좋겠습니다. Rails MVC와 관련된 패턴과 안티패턴이 궁금하시다면 모델, 뷰, 컨트롤러 각각을 다룬 이전 블로그 포스트를 참고해 주세요.

그럼 지금부터 일반적인 문제들과 핵심 교훈을 살펴보겠습니다.

이기적인 객체와 데메테르의 법칙(Law of Demeter)

데메테르의 법칙은 Demeter 프로젝트를 진행하던 팀에서 유래한 휴리스틱입니다. 핵심 아이디어는 객체가 한 번에 하나의 메서드만 호출하고, 여러 메서드 호출을 체인으로 연결하지 않는다면 그 객체는 건강하다는 것입니다. 실제로는 다음과 같습니다.

# 나쁜 예
song.label.address
 
# 좋은 예
song.label_address

이렇게 하면 song 객체는 더 이상 주소가 어디서 오는지 알 필요가 없습니다. 주소를 제공하는 책임은 label 객체에 있습니다. 메서드 호출은 한 단계만 체인하고, 객체를 '이기적으로' 만들어 전체 정보를 직접 노출하지 않고 헬퍼 메서드를 통해서만 공유하도록 하는 것이죠.

다행히 Rails에서는 헬퍼 메서드를 일일이 작성할 필요 없이 delegate 헬퍼를 사용할 수 있습니다.

def Label < ApplicationModel
  belongs_to :song
 
  delegate :address, to: :song
end

delegate가 받는 다양한 옵션은 공식 문서에서 확인해 볼 수 있습니다. 개념과 실행 방식은 매우 간단합니다. 데메테르의 법칙을 적용하면 구조적 결합(coupling)을 줄일 수 있고, 강력한 delegate와 함께라면 더 적은 코드로 깔끔하게 처리할 수 있습니다.

데메테르의 법칙과 비슷한 또 다른 원칙이 바로 단일 책임 원칙(Single-responsibility Principle, SRP)입니다. 이 원칙은 모듈, 클래스, 함수가 시스템의 한 가지 부분에 대해서만 책임져야 한다고 말합니다. 다른 방식으로 표현하면 다음과 같습니다.

같은 이유로 변경되는 것들은 함께 모으고, 다른 이유로 변경되는 것들은 분리하라.

사람마다 SRP를 이해하는 방식은 조금씩 다를 수 있지만, 핵심은 빌딩 블록 하나하나가 단 하나의 일에 책임지도록 유지하는 것입니다. Rails 앱이 커질수록 SRP를 지키기란 쉽지 않지만, 리팩토링할 때는 항상 염두에 두어야 합니다.

기능을 추가하고 코드량(LOC)이 늘어나다 보면 사람들은 종종 빠른 해결책을 찾곤 합니다. 그래서 다음 섹션에서는 '편한 길'에 대해 이야기해 보겠습니다.

아는 사람 있으면 끝? (그 루비 젬, 정말 필요한가?)

Rails가 뜨거운 감자였던 시절에는 오픈소스 협업 붐이 일면서 곳곳에서 새로운 루비 젬(gem)들이 쏟아져 나왔습니다. 요즘 자바스크립트 라이브러리들이 우후죽순으로 등장하는 것과 비슷하지만, 규모는 훨씬 작았습니다.

어쨌든 당시 흔했던 접근법은 자신의 문제를 해결해 줄 기존 젬을 찾아 설치하는 것이었습니다.

이것 자체는 전혀 잘못된 게 아니지만, 젬 설치를 결정하기 전에 몇 가지 조언을 드리고 싶습니다.

먼저 스스로에게 다음 질문을 던져보세요.

  • 그 젬의 기능 중 실제로 사용할 부분은 얼마나 되는가?
  • '더 단순하거나' 더 최신으로 유지되는 유사한 젬이 있는가?
  • 필요한 기능을 직접 쉽고 확신 있게 구현할 수 있는가?

젬의 모든 기능을 사용할 계획이 없다면 직접 구현하는 것이 가치 있는지 평가해 보세요. 또는 젬의 구현이 너무 복잡해서 더 단순하게 만들 수 있다고 생각한다면 커스텀 솔루션을 선택하는 것이 낫습니다.

또 하나 고려할 요소는 젬 저장소가 얼마나 활발한지입니다. 활동 중인 메인테이너가 있는가? 마지막 릴리스는 언제였는가?

젬의 의존성도 주의 깊게 살펴야 합니다. 특정 버전의 의존성에 갇히는 상황은 피해야 하므로, 항상 Gemfile.spec 파일을 확인하세요. RubyGems의 버전 명시 방식을 참고하는 것도 좋습니다.

젬 이야기를 하다 보면 관련된 또 다른 개념을 만나게 됩니다. Rails/Ruby 세계에도 적용되는 'Not Invented Here'(NIH) 현상입니다. 다음 섹션에서 자세히 살펴보겠습니다.

Not Invented Here (어쩌면 그 젬이 필요할지도?)

커리어를 쌓으면서 저는 몇 차례, 사람들(저 자신 포함)이 NIH 증후군에 빠지는 모습을 목격했습니다. 이것은 '바퀴를 재발명하는 것'과 비슷한 개념입니다. 때로 팀이나 조직은 자신이 통제할 수 없는 라이브러리(젬)를 신뢰하지 못합니다. 이런 불신이 이미 존재하는 젬을 굳이 새로 만들게 만드는 방아쇠가 되곤 합니다.

NIH를 경험하는 것이 좋은 결과로 이어지는 경우도 있습니다. 사내 솔루션을 직접 만드는 것은, 특히 기존 솔루션보다 개선할 수 있다면 훌륭한 선택입니다. 그 솔루션을 오픈소스로 공개하기까지 한다면 더욱 좋겠죠(Ruby on Rails나 React를 생각해 보세요). 하지만 바퀴를 재발명하는 것 자체가 목적이라면 하지 마세요. 바퀴는 이미 충분히 훌륭합니다.

이 주제는 꽤 미묘합니다. 만약 이런 상황에 처하게 된다면 스스로에게 다음 질문을 던져보세요.

  • 기존 솔루션보다 더 나은 것을 만들 수 있다는 확신이 있는가?
  • 기존 오픈소스 솔루션이 우리 요구사항과 다르다면, 오픈소스에 기여해서 개선할 수 있는가?
  • 더 나아가 그 오픈소스의 메인테이너가 되어 수많은 개발자들의 삶을 개선할 수 있지 않은가?

하지만 때로는 자신만의 길을 가서 직접 라이브러리를 만들어야 할 수도 있습니다. 조직이 오픈소스 라이브러리 라이선싱을 선호하지 않아 어쩔 수 없이 직접 만들어야 하는 경우도 있죠. 어떤 선택을 하든, 제 조언은 바퀴의 재발명만큼은 피하라는 것입니다.

출동하는 구조대 (과도한 예외 rescue)

사람들은 원래 의도했던 것보다 더 많은 예외를 rescue하는 경향이 있습니다.

이 주제는 앞선 주제들보다 코드에 좀 더 밀접한 관련이 있습니다. 어떤 사람에게는 상식처럼 느껴질 수 있지만, 실제 코드에서 종종 발견되는 문제입니다. 예를 들어 다음과 같은 코드가 있습니다.

begin
  song.upload_lyrics
rescue
  puts 'Lyrics upload failed'
end

rescue할 예외를 명시하지 않으면, 계획하지 않았던 예외까지 잡아버리게 됩니다.

이 경우 문제는 song 객체가 nil이라는 것일 수 있습니다. 이 예외가 에러 트래커에 보고되면 업로드 과정에 문제가 있다고 생각하겠지만, 실제로는 완전히 다른 문제를 겪고 있을 수 있습니다.

따라서 안전하게 예외를 처리하려면, 발생 가능한 모든 예외 목록을 파악한 후 rescue해야 합니다. 어떤 이유로든 모든 예외를 파악할 수 없다면, 과도하게 rescue하는 것보다 부족하게 rescue하는 편이 낫습니다. 아는 예외만 rescue하고 나머지는 이후 단계에서 처리하세요.

너무 많이 요구하네요 (과도한 SQL 쿼리)

이번 섹션에서는 웹 개발과 관계형 데이터베이스의 또 다른 문제를 다뤄보겠습니다.

하나의 요청에서 웹 서버에 너무 많은 SQL 쿼리를 쏟아붓는 문제입니다. 이 문제는 어떻게 발생할까요? 하나의 요청에서 여러 테이블의 여러 레코드를 가져오려고 할 때 발생할 수 있습니다. 하지만 가장 흔한 원인은 악명 높은 N+1 쿼리 문제입니다.

다음 모델을 상상해 보세요.

class Song < ApplicationRecord
  belongs_to :artist
end
 
class Artist < ApplicationRecord
  has_many :songs
end

특정 장르의 노래 몇 곡과 그 아티스트를 보여주고 싶다고 합시다.

songs = Song.where(genre: genre).limit(10)
 
songs.each do |song|
  puts "#{song.title} by #{song.artist.name}"
end

이 코드는 열 곡의 노래를 가져오는 SQL 쿼리 하나를 실행합니다. 그다음 각 노래의 아티스트를 가져오기 위해 추가 SQL 쿼리가 노래마다 하나씩 실행됩니다. 총 열한(11) 개의 쿼리입니다.

노래를 더 많이 로드한다면 어떻게 될까요? 모든 아티스트를 가져오려고 데이터베이스에 훨씬 큰 부하를 주게 됩니다.

대신 Rails의 includes를 사용해 보세요.

songs = Song.includes(:artists).where(genre: genre).limit(10)
 
songs.each do |song|
  puts "#{song.title} by #{song.artist.name}"
end

includes를 사용하면 이제 몇 곡을 보여주든 SQL 쿼리는 두 개뿐입니다. 아주 깔끔하죠.

과도한 SQL 쿼리를 진단하는 한 가지 방법은 개발 환경에서 확인하는 것입니다. 같은 테이블에서 데이터를 가져오는 비슷한 SQL 쿼리 그룹이 보인다면, 무언가 수상한 일이 벌어지고 있는 것입니다. 그래서 개발 환경에서 SQL 로깅을 켜두는 것을 강력히 권장합니다. 또한 Rails는 쿼리가 코드의 어디에서 호출되었는지 보여주는 verbose query logs를 지원합니다.

로그를 보는 게 취향이 아니거나 더 강력한 도구가 필요하다면, AppSignal의 성능 측정 및 N+1 쿼리 감지 기능을 사용해 보세요. 문제가 N+1 쿼리에서 비롯되었는지 훌륭하게 알려줍니다.

마치며

이 블로그 시리즈를 읽어주셔서 감사합니다. Rails에서 패턴과 안티패턴을 소개하는 것부터 MVC 패턴 내부 탐색, 그리고 이번 일반적인 문제를 다룬 마지막 포스트까지, 즐거운 여정에 함께해 주셔서 기쁩니다.

많은 것을 배우셨거나, 적어도 이미 알고 있던 내용을 복습하고 다지셨기를 바랍니다. 모든 내용을 외우려고 스트레스받지 마세요. 어려움에 부딪힐 때 언제든 이 시리즈를 다시 찾아보면 됩니다.

패턴과 안티패턴은 분명 마주하게 될 것입니다. 이 세상(특히 소프트웨어 엔지니어링)은 완벽하지 않으니까요. 그래도 걱정할 필요는 없습니다.

패턴과 안티패턴을 마스터하면 훌륭한 소프트웨어 엔지니어가 됩니다. 하지만 여기서 한 단계 더 나아가게 만드는 것은, 완벽한 해결책은 존재하지 않기 때문에 언제 그 패턴과 틀을 깨야 하는지 아는 것입니다.

함께해 주셔서 다시 한번 감사합니다. 다음 포스트에서 만나요. 건승을 빕니다!

P.S. Ruby Magic 포스트가 발행되는 즉시 읽고 싶으시다면 Ruby Magic 뉴스레터를 구독하고 어떤 포스트도 놓치지 마세요!