요즘카페 도메인 설계

ttomy·2023년 7월 10일
0

애플리케이션의 기능

  • 가게에 대한 정보를 제공한다.
    • 가게에 대한 1개 이상의 이미지를 보여줄 수 있다.
    • 해당 가게의 이름, 주소, 좋아요 개수를 보여준다.
    • 해당 가게의 상세정보를 보여준다.
  • 로그인한다.

    • 본인 개인 정보를 조회한다.
    • 본인 개인정보를 수정한다.
    • 로그아웃한다.
  • 가게에 좋아요 추가/취소를 한다.

    • 가게에 좋아요를 할 수 있다.
    • 가게에 좋아요를 취소할 수 있다.
    • 회원이 좋아요한 목록을 보여줄 수 있다.
  • 회원이 아직 보지 않은 카페부터 보여준다.

    • 회원이 이미 본 카페를 저장 vs 회원이 아직 보지 않은 카페를 저장

1) 회원이 이미 본 카페를 저장하면, 전체 카페에서 이미 본 것을 제외하며 쿼리해야한다. 이때는 db인덱스를 이용할 수 없기에 조회 성능이 떨어질 수 있다는 단점이 존재.
2) 회원이 아직 보지 않은 카페를 저장하면, 회원이 등록되면 모든 카페에 대한 데이터를 저장해야 한다. 이는 저장공간을 1)의 방법보다 더 사용할 가능성이 높다는 단점이 존재.
-> 카페의 데이터의 크기는 최악의 경우에도 감당할만하다 판단했다. 국내의 모든 카페를 다 등록한다 하더라도 엄청 부담되는 크기는 아니다. 반면 조회의 속도는 서비스에서 가장 많이 사용되는 부분이기에 더 중요하다 생각해 2) 방법을 채택했다.

설계해보기

  • Member
    name, image(프로필이미지)

  • Cafe

    • Detail/Images
  • LikedCafe

  • UnviewedCafe

LikedCafe,UnviewedCafe는 중간 엔티티이다.
회원은 여러 카페에 좋아요 할 수 있고, 카페들도 여러 회원에 의해 좋아요 될 수 있기에 다대다 관계이다.

(한 카페는 여러 회원에 의해 좋아요 될 수 있고,한 회원이 한 카페만을 좋아요할 수 있다면
-> 회원-카페가 다대일 관계였을 것이다
-> Member 테이블이 cafe_id를 가짐)

(한 카페는 한 회원에게만 좋아요될 수 있고 한 회원은 여러 카페를 좋아요할 수 있다면
-> 회원-카페의 관계는 일대다였을 것이다.
-> Cafe 테이블이 member_id를 가지도록 구성됨 )

마찬가지로 아직 보지 않은 카페 - UnViewedcafe도 다대다 관계이다.

이때 중간 엔티티를 굳이 두지 않고 @manyToMany을 이용해 중간테이블만을 두고 중간 엔티티는 만들지 않도록 할 수도 있다. 하지만 이러면 중간테이블에 대한 필드 추가나 애플리케이션 레벨에서의 로직추가를 하기 힘들기에 직접 중간 엔티티를 만드는 방향을 택했다.

  • cafe

  • Images, Detail

  • member
  • likedCafe
  • unviewedCafe

lazy로딩? eager로딩?

가능하면 lazy로딩을 기본으로하고 필요하면 eager로딩을 도입하는 방향으로 생각했다. 요즘카페에서는 어떤 기준으로 lazy로딩/eager로딩을 택했는지 정리한다.

Images클래스의 urls는 eager로딩이다.
예를 들어 Cafe엔티티를 불러올 때, 요즘카페 서비스에서 cafe의 images는 최소 1개는 항상 불러와지기 때문이다. (카페에 대한 정보 모든 정보 제공 기능에서 최소 1개의 이미지는 보여준다.)

반면, 영업시간 정보는 카페에 대한 세부정보 페이지에서만 필요로 하기에 항상 불러올 필요가 없다. 때문에 Detail클래스의 availableTimes는 lazy로딩이다.

Member엔티티의 likedCafes와 unViewedCafes는 필요한걸까? 회원이 카페를 조회할 때 요즘카페 서비스는 1)해당 카페가 이미 회원이 좋아요 한 카페이면 빨갛게 칠해진 하트를 보여주어야 한다.

또한 회원이 카페 카드를 넘겨 새로운 카페를 보려할 때, 아직 보지 않은 카페 중에서 응답을 하려면 unViewedCafes를 가지고 그 중에서 응답하기에 unViewedCafes를 필드로 가진다. 하지만 member 객체는 이외에도 로그인 시 토큰 검증을 위해 불러와지기도 하기에 항상 member와 그에 따른 likedCafes와 unViewedCafes가 함께 필요하지는 않다. 그렇기에 lazy로딩을 한다.

이렇게 엔티티에서 likedCafes,unViewedCafes를 가지지 않고 member_id를 통해서 db에서 쿼리를 통해 해당하는 cafe를 가져오는 방법은 왜 채택하지 않았는지 궁금할 수 있다.
-> 회원이 새로운 카페를 보고, 카페에 좋아요를 하는 등의 로직을 가능하면 도메인이 수행하게 하려했다. 로직이 그저 카페를 보고/ 카페에 좋아요를 하면 db에 unViewedCafe 데이터를 삽입하고/likedCafe데이터를 삽입하는 방식으로 직접 쿼리를 작성하는 방식으로하면 영속성에 의존적으로 서비스 로직이 만들어지기에 이를 경계했다.

eager로딩을 결정할 때 감안해야 하는 점이 또 있다.
eager로딩으로 인해 너무 많은 데이터를 불러와 out of memory상황이 일어날 수 있는지도 생각해보아야 한다.

casacade, orphan remove옵션

jpa의 casacade란? 영속성 상태를 어느정도까지 다른 엔티티로 전이시킬지 정하는 옵션. 아래와 같이 사용된다.

	@OneToMany(mappedBy = "parent",cascade = CascadeType.ALL)
    private List<Child> childes = new ArrayList<>();

이 옵션은
-ALL
-PERSIST

  • 부모엔티티를 저장할 때 자식 엔티티도 함께 저장
    -REMOVE
  • 부모 엔티티의 삭제 시, 자식 엔티티도 함께 삭제
    -MERGE
  • 부모가 merge()에 의해 변경사항이 생기면, 자식도 그에 맞게 변경된다.
    -REFERESH
    -DETACH
    이렇게 6가지가 있다.

Member의 likedCafes와 unviewedCafes에는 어떤 casacade옵션이 적용될까?
부모인 member가 저장되거나, 변경되면 이것이 함께 적용되어야 하므로 persist,merge가 적용되었다.

이때 Member객체가 삭제되면 likedCafes,unviewedCafes도 삭제되어야 하므로
CascadeType.REMOVE나 orphanRemoval = true
를 걸어야 하지 않을까? 이 둘은 무슨 차이가 있을까?

참고 - https://tecoble.techcourse.co.kr/post/2021-08-15-jpa-cascadetype-remove-vs-orphanremoval-true/

부모가 자식 엔티티의 관계를 끊을 때도 자식 엔티티를 삭제하게 만들기 위해 orphanRemoval = true를 적용했다.

남은 고민

  • Detail이란 테이블을 분리할 필요가 있었나, 한 테이블에 두어도 되지 않을까?

  • 좋아요에 대한 설계
    좋아요의 개수를 likedCafe의 개수를 세는 방식으로 할 수도 있고, 좋아요할 때마다 cafe의 likeCount를 조정하는 방식으로 할 수도 있다. 이 둘의 개수를 맞추는 것이 필요하다.
    https://tecoble.techcourse.co.kr/post/2022-10-10-like-count/

  • Member가 unViewedCafes를 가지고 있기에,
    lazy로딩이더라도 서비스에 등록되는 카페의 개수가 많아질수록
    메모리에 로드된는 카페의 수로 많아져 부담이 될 수 있을 것 같다.

  • 객체 참조를 어디까지 해야 좋을까. Menu같은 엔티티는 Cafe를 직접 참조하지 않고 cafe_id를 가지도록 해볼 수 있지 않을까?

reference

https://www.baeldung.com/jpa-cascade-types

https://github.com/woowacourse-teams/2022-sokdak/blob/dev/backend/sokdak/src/main/java/com/wooteco/sokdak/like/domain/PostLike.java

0개의 댓글