리뷰 테이블 및 관계 생성
먼저 reviews 테이블을 생성하기 위해 hanami g migration create_reviews 명령어를 사용합니다. 생성된 마이그레이션 파일에 book_id 외래 키, content, rating, created_at 컬럼을 정의합니다. 이후 hanami db migrate로 마이그레이션을 실행합니다.
다음으로 hanami g relation reviews 명령어를 통해 Reviews 관계 클래스를 생성합니다. Hanami 콘솔에서 app["relations.reviews"]를 통해 관계를 로드하고, insert 및 where 메서드를 사용하여 리뷰를 생성하고 조회하는 기본 작업을 수행할 수 있습니다.
Hanami의 연관 관계와 N+1 쿼리 방지
Rails와 달리 Hanami에서는 book.reviews와 같은 직접적인 연관 관계 메서드를 객체에 정의하지 않습니다. 이는 N+1 쿼리 문제를 근본적으로 방지하기 위한 설계 철학입니다. Hanami에서는 필요한 모든 데이터를 사전에 로드하여 쿼리 발생 시점을 명확히 합니다.
연관 관계 정의 및 데이터 로딩
app/relations/books.rb 파일의 schema 블록 내에 associations를 정의하여 has_many :reviews 연관 관계를 설정합니다. 마찬가지로 app/relations/reviews.rb에는 belongs_to :book을 정의합니다.
콘솔에서 books.by_pk(1).combine(:reviews).first와 같이 combine 메서드를 사용하여 단일 쿼리로 책과 해당 리뷰 데이터를 함께 로드할 수 있습니다. 이 방식은 책 쿼리 한 번, 리뷰 쿼리 한 번으로 데이터를 가져와 N+1 쿼리를 회피합니다.
Repository를 통한 연관 관계 노출
BookRepo 클래스(app/repos/book_repo.rb)에 find_with_reviews(id) 메서드를 정의하여 books.by_pk(id).combine(:reviews).one! 호출을 캡슐화합니다. 이를 통해 뷰(app/views/books/show.rb)에서 book_repo.find_with_reviews(id)를 사용하여 책과 리뷰 데이터를 쉽게 가져올 수 있으며, 템플릿(app/templates/books/show.html.erb)에서 reviews.each를 통해 반복하여 표시할 수 있습니다.
복잡한 쿼리 구현
“리뷰가 많은 책(>= 10개)”, “평균 평점이 3점 이상인 책”과 같은 복잡한 쿼리 요구사항을 처리하기 위해 app/relations/books.rb에 사용자 정의 메서드를 추가합니다.
-
인기 있는 책 (
popular):join(:reviews).group(:id).having { count(reviews[:id]) >= 10 } -
좋아요 받은 책 (
liked):join(:reviews).group(:id).having { avg(reviews.rating) >= 3 } -
싫어요 받은 책 (
disliked):join(:reviews).group(:id).having { avg(reviews[:rating]) <= 2 }
Sequel gem이 제공하는 Ruby 구문을 사용하여 SQL의 HAVING 절을 깔끔하게 작성할 수 있습니다. 여러 메서드를 체인으로 연결할 때 발생하는 중복 JOIN 문제를 해결하기 위해 with_reviews와 같은 공통 JOIN 메서드를 먼저 호출하도록 패턴을 개선합니다.
최종적으로 이러한 관계 메서드들을 BookRepo에 popular, popular_and_liked 등으로 캡슐화하여 애플리케이션에 더 깔끔한 인터페이스를 제공합니다. 관계는 복잡한 SQL 로직을 처리하고, 저장소는 이 관계의 메서드를 활용하여 애플리케이션에 명확한 파사드(facade)를 제공하는 역할 분담이 이루어집니다.