본문 바로가기
Ruby on Ralis

[코드레뷰로 배우는 Ruby on Rails 2판] Model편

by sky1to 2024. 6. 8.

일본에 기술서적 관련 이벤트가 매번 진행되는데 2만 원 3만 원 하는 책이 아닌 150~180? 정도 되는 정도에 기술책을 판매하는 이벤트인 것 같다. 이번에 온라인이 있길래 보고 있었는데 코드레뷰로 배우는 루비 온 레일즈 책이 있어서 1000엔(만원) 정도 이길래 구매해 보았습니다.

아직 모델 편 밖에 읽지 않았지만 잘 샀다고 생각하고 있습니다!

https://techbookfest.org/product/50CLAnEpRNhFNKzgq1aK78?productVariantID=gmDHaevdhz4T3T4eiGjWHe

 

 

읽으면서 새롭게 배웠던 내용을 정리해 보고자 합니다. 좀 더 자세히 알고 싶은 내용은 따로 블로그를 작성해나가고 싶네요.

 

1. 중간 테이블 모델은 관계성을 나타내는 모델명을 사용한다

Rails를 처음 배우다 보면 관계 테이블을 작성할 때 User N : M Community 테이블이 있으면 UserCommunities 같은 테이블을 작성한다. 이게 왜 나쁜가 하면 UserPosts와 같은 테이블을 보면 이 테이블의 관계가 명확하지 않기 때문이다. 이 테이블을 만약 Memberships 테이블로 작성한다면, 유저가 커뮤니티에 멤버인 것을 나타내는 테이블임을 명확히 알 수 있다.

 

일반적인 Rails규칙에 따라 테이블명, 모델명을 작성하고 있다면 좀 더 구체적인 이름으로 바꿀 수 없는지 확인하면 좋을 것 같습니다!

 

2. 일반 유저와 관리자 유저의 모델은 나누는 것이 좋은가?

이건 아직 개발해 본 경험이 없어서 그렇게 와닿는 부분은 아니었는데 결론부터 얘기해 보자면 아래와 같다.

일반 유저와 관리자를 같은 모델로 하면 보안 문제나 요구 사항의 차이로 인해 문제가 생길 가능성이 있으므로 모델을 나누는 것이 좋다.

 

유저 관련 로직은 유저 모델에 작성하는 경향이 있는데, 좀 더 클래스를 세분화해 나간다면(OOP) 그렇게 문제가 되지 않는 부분이지 않을까 생각했습니다.

3. default_scope는 사용하지 않는다

default_scope를 사용하면 예기치 않은 에러가 발생하기 때문에 사용을 하지 않는 편이 좋다는 부분은 알고 있었다. 이 부분이 잘 와닿지 않는 부분도 있었는데, 예시가 나와서 좀 와닿았다.

 

논리 삭제를 사용하기 위해 default_scope를 사용하는 경우가 있었는데, 만약 scope를 사용하고 싶지 않아 unscope 하는 경우 의도하지 않은 레코드도 같이 검색되는 경우가 생길 수 있어 안전하지 않다고 한다.(이 부분은 처음 알았다)

 

결론적으로는 가독성과 안정성을 위해서는 default_scope를 최대한 피하는 것이 좋을 것 같다.

 

4. default 값의 정의에는 attribute 메서드를 사용하자

이 부분은 아직 경험이 없어서 패스

5. 마이그레이션 중에 모델을 참조하지 않는다

마이그레이션에 복잡한 로직을 넣는 경우가 있다고 한다(처음 알았다) 

마이그레이션이 아닌 rake파일로 처리하자라는 내용이었다.

마이그레이션에서는 테이블 작성, 칼럼 작성, 변경, 삭제 같은 것만 하는 것을 개인적으로는 추천한다.

 

6, 7, 8 DB관련 내용

6. 데이터베이스에 처리를 맞긴다

대량의 데이터를 Ruby 측에서 처리하면 부하 문제가 발생할 수 있으니, 데이터의 집계나 단순한 값 리스트의 취득만이라면 데이터베이스 측에서 처리함으로써 성능을 향상할 수 있다는 내용이었다.

데이터베이스에서 처리할 수 있는 내용은 데이터베이스에서 하자!

 

7.  has_many관계에서는 eager_load보다 preload를 사용하자 

SELECT "users".* FROM "users"
SELECT "books".* FROM "books" WHERE "books"."user_id" IN (?)

 

preload의 경우 처음에 User 데이터를 로드하고, 다음으로 해당 User의 ID를 기반으로 관련된 Book을 로드합니다. eager_load와 비교하면 SQL 발행 횟수는 늘어나지만, 가져오는 데이터 양이 적어져서 응답 속도가 향상될 수 있다고 한다.

 

has_many 관련을 다룰 때, 관련된 데이터가 많은 경우에는 eager_load보다 preload를 사용하는 것이 효과적이다.

이를 통해 데이터의 중복을 피하고, 슬로 쿼리를 방지하며, 필요한 정보를 효율적으로 가져올 수 있다고 하니 has_many를 사용할 때는 참고하면 좋을 것 같다.

 

8. 데이터베이스에 제약을 걸자

Rails단에서 validate로 값을 받지 않게 하는 경우가 있는데, 그렇게 되면 직접 DB에서 레코드를 변경하는 경우 확실하게 방지하지 못하게 된다. DB단에 NULL제약이나 디폴트 값을 설정할 수 있으니, DB에서 가능한 것은 DB에서 해결하자라는 내용이었다.

(유니크 인덱스도)

9. scope는 ActiveRecord::Relation을 반환한다

처음 안 사실이어서 부끄러웠다. scope를 ActiveRecord만 있을 경우 사용해야지라고 생각하고 있었다. 그러나... Rails의 리퍼런스에 "scope는 ActiveRecord::Relation을 반환하는 것이 예상된다"라고 아주 명확하게 명시되어 있었다.(열심히 공부하겠습니다...)

Adds a class method for retrieving and querying objects. 
The method is intended to return an ActiveRecord::Relation object, 
which is composable with other scopes. If it returns nil or false, 
an all scope is returned instead.

 

번역: scope 메서드는 객체를 조회하고 쿼리하기 위한 클래스 메서드를 추가하며, 이 메서드는 다른 스코프와 결합 가능한 ActiveRecord::Relation 객체를 반환하는 것이 의도되었다. 만약 nil 또는 false를 반환하면, 대신 all 스코프가 반환된다.

 

11 dependent: :destroy는 적절한가?

이 장에 결론을 한 문장으로 하면

 

> 자식 레코드가 삭제되면 문제가 발생할 수 있으니, dependent 옵션 설정을 신중히 고려하세요.

 

지금까지 깊게 생각해 본 적 없는 문제였는데, 이 책에서 나온 예제는 아래와 같다

  • 주문 데이터는 비즈니스상 중요한 기록이다.
  • 데이터 유지보수 중에 실수로 삭제할 수 있다.

해결책으로는 아래와 같이 제안되어 있었다.

  • restrict_with_exception을 설정한다.(처음 봤다. 공부...)
  • nullify를 설정한다.
  • 부모 레코드 삭제를 대체하는 새로운 상태를 정의한다.

항상 생각하지 않고 dependent: :dstroy를 설정하는 것이 아닌 정말 설정해도 되는 부분인가를 생각하고 하는 게 중요한 것 같다. 왜냐하면 수정이 광범위하게 이루어질 수 있고 시스템의 복잡도도 증가할 수 있기 때문이다. 그러므로 항상 삭제를 피하는 대신, 시스템의 중요도와 개발 비용의 총균형을 고려하여 결정하자.

 
책에서 나온 좋은 문장
> 초기에 충분히 검토한 후 설정하면 의도하지 않은 삭제를 방지하고 데이터의 일관성을 유지할 수 있습니다. dependent 옵션을 설정할 때는 "이 데이터는 비즈니스적으로 중요한가?"라는 관점에서 한 번 더 고민해보세요.
 
 

13. 실행되는 SQL을 파악하자

 

아무리 ActiveRecord를 잘 이해하고 있다고 해도 의도하지 않는 레코드를 반환할 수 있다. 그러므로 실행되는 SQL을 꼭 확인하거나, 테스트 케이스를 잘 만들어야 할 필요가 있다.

쿼리가 실행되는 타이밍을 이해하고 적절히 preload, eager_load를 활용하는 것이 중요합니다. 루프 내에서 쿼리가 실행되는 처리 (where, order, exists?, count 등)은 피하세요!

 

14. 트랜잭션을 사용하여 데이터 일관성을 지키자

트랜잭션을 사용하여 여러 레코드가 한 번에 변경되는 것을 방지하자는 내용입니다.

개발하다 보면 당연한 내용이지 않나?라고 생각할 수 있지만 신입이 들어오는 경우 레뷰를 많이 했으니 머릿속에 넣어놓으면 좋을 것 같습니다.

15. find_each를 사용하는 것이 좋다

User.each를 사용하는 경우 모든 User 객체가 한 번에 메모리에 로드된다고 합니다. 레코드 수가 매우 많을 경우, 만약 1000만 레코드가 있다면?? 메모리 사용량이 크게 증가하여 메모리 부족이 발생할 수 있습니다. 기본적으로 1,000건씩 메모리에 로드되므로 메모리 소비를 줄일 수 있기 때문에 find_each를 사용하는 편이 좋다(로드할 건수는 옵션으로 조정할 수 있다)

그러나 id를 기준으로 실행시키기 때문에 다른 조건이 있는 겨우는 따로 고려해야 할 필요가 있다.

 

대용량 레코드를 다루지 않으면 직면하지 않은 문제여서 공부가 됐다.

 

마무리

Model 편을 한번 정리해 보았습니다. 이 책에 장점이 Model, View, Controller, 테스트 별로 나눠 저 있어 각 분야별로 보기 편해서 좋은 것 같습니다. 이미 아는 내용은 좀 더 정리해 보고 몰랐던 내용은 다음에 사용할 때 꼭 주의해서 사용해 보아야겠네요.