본문 바로가기
Web/프로젝트

놀토 - 1편. 도메인 설계/JPA 전략

by 조엘 2021. 11. 22.

안녕하세요! 조엘입니다! 🙌🙌

우아한테크코스 레벨 3, 4는 팀 프로젝트를 진행하는데요. 

저는 부담없이 자랑하는 작고 소중한 내 프로젝트 라는 미션을 가진 놀러오세요 토이프로젝트 팀과 함께했어요!!

 

저희 팀은 프론트엔드 2명, 백엔드 4명으로 구성되어있어요. 저는 백엔드 개발자로 참여했고요. 

저희 팀원들은 열심히 만든 토이프로젝트를 홍보하고, 프로젝트에 대한 피드백을 받기 어렵다는 문제에 공감했어요. 

또한 어떤 토이프로젝트가 요즘에 진행되는지 한눈에 보고 싶다는 니즈가 있다고 생각했고요. 

 

그렇게 시작한 놀러오세요 토이프로젝트는 4개월의 개발 기간을 거쳐 출시가 되었답니다 :)

 

서비스 URL과 깃헙 레포에서 놀러오세요 토이프로젝트 (이하 놀토) 를 만나보세요!

🎁 서비스 URL: https://nolto.app/
🎯 GitHub: https://github.com/woowacourse-teams/2021-nolto

 

놀토 프로젝트에서는 JPA를 통해 DB를 다뤘는데요. 

레벨 3에 처음 JPA를 학습하다 보니, 프로젝트와 학습을 병행하면서 정신없이 공부하고 적용했던 것 같아요. 

 

1편. 도메인 설계/JPA 전략에서는 어떻게 놀토 프로젝트에 필요한 엔티티를 설계하여 관계를 매핑하고, 어떻게 JPA를 활용했는지 알아보도록 할게요!

 

 

1. 도메인 설계하기

맨 처음 필요한 도메인들을 팀원들과 함께 화이트보드에서 그려가면서 설계했어요. 

그래서 나온 초창기 도메인들은 다음과 같았는데요! 

 

초창기 도메인 설계

 

게시글을 Feed로, 피드에 사용된 기술 스택을 Tech로, 사용자를 User로, 좋아요를 Like로 명명했어요. 

도메인 사이의 관계를 살펴보면 다음과 같아요. 

  - User <-> Feed 일대다 

  - User <-> Like 일대다

  - Feed <-> Tech 일대다

  - Feed <-> Like 일대다 

 

사실 맨 처음 이해하기 힘들었던 부분은 Like 도메인에 대한 것이었어요. 

좋아요를 따로 객체로 분리해야 할 이유를 공감하지 못했었어요. 

(내가 배운 객체지향은 이게 아닌데! 라고 했던 기억이...)

 

"User가 좋아요 한 피드를 List<Feed>로 가지고, Feed에 좋아요를 누른 사용자를 List<User>로 가져야 하지 않나?"

라고 팀원들에게 주장했던 기억이 나네요. 

 

사실 저렇게 설계하면 UserFeed다대다 관계를 가지게 될 텐데요.

이를 Like 연결 테이블로 일대다-다대일 관계로 풀어내야 한다고 팀원들이 알려줬어요. 

JPA로 다대다 매핑하면 자체적으로 연결 테이블 생기기 때문에, 명시적으로 연결 엔티티를 생성하여 직접 관리할 수 있도록 하는 것이 좋다고 얘기해줬어요. 

 

결국 도메인을 DB에 저장하여 엔티티로 활용하기 위해서는, DB가 데이터를 어떻게 저장하는지 잘 알아야 하는구나 배울 수 있었어요.

 

 

2. 엔티티 구조

초창기 설계에서 Feed와 Tech가 다대다 관계라는 것을 뒤늦게 깨닫고 FeedTech를 새로 만들었어요. 

(Feed 하나가 여러 개의 Tech를 사용했을 수 있고, 하나의 Tech가 여러 개의 Feed에서 사용됐을 수 있어요)

 

또한 구현해야 할 기능이 추가되고, 요구 사항이 증가함에 따라 자연스럽게 새로운 엔티티도 생겨났어요. 

따라서 초창기 구조에 비해 추가된 엔티티들은 다음과 같아요. 

  - Feed와 Tech 사이 다대다 관계를 풀어낸 FeedTech 연결 엔티티

  - Feed에 달린 댓글 Comment 엔티티

  - Comment에 눌린 좋아요를 나타내는 CommentLike 엔티티

 

알림에 대한 정보를 저장하는 Notification 엔티티도 추가되었는데, 핵심 로직과는 거리가 있어서 이번 포스팅에선 다루지 않을게요. 

 

최종적인 엔티티 구조과 연관 관계는 다음과 같아요. 

 

놀토 엔티티 구조

 

저희 팀의 고민이 담긴 독특한 엔티티로는 Comment를 뽑고 싶어요. 

놀토 팀에서는 피드에 댓글을 작성할 수 있고, 댓글에 대댓글을 작성할 수 있도록 했어요. 다음과 같이 말이죠!

 

"찜꽁" 화이팅!

 

대댓글 구현을 Reply와 같은 새로운 엔티티를 만들어서 할 수 있었을 것이에요. 

Comment 엔티티와 Reply 엔티티를 일대다 매핑하고, ReplyLike 엔티티를 만들어 좋아요를 저장하는 방식으로요. 

 

하지만 보다시피 Reply 기능은 Comment와 너무너무 유사했어요.

비슷한 구조를 거의 복사 붙여 넣기 하여 Reply 엔티티를 만들고 싶지 않았어요. 

그 결과 대댓글을 구현은 Comment의 자기 참조 관계로 구현할 수 있었어요. 

 

자기 참조 관계란 하나의 엔티티가 다른 엔티티가 아닌 자기 자신과 관계를 맺는 것을 의미하는데요. 

계층 구조를 나타낼 때 유용하게 쓰일 수 있어요. 

 

따라서 Comment 엔티티를 다음과 같이 작성했어요!

 

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;

    @Column(nullable = false)
    private boolean helper;

    @ManyToOne
    @JoinColumn(nullable = false)
    private Feed feed;

    @ManyToOne
    @JoinColumn(nullable = false)
    private User author;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Comment parentComment;

    @OneToMany(mappedBy = "comment", cascade = CascadeType.REMOVE)
    private List<CommentLike> likes = new ArrayList<>();

    @OneToMany(mappedBy = "parentComment", cascade = CascadeType.REMOVE)
    private List<Comment> replies = new ArrayList<>();
    
    // Methods...
}

 

만약 댓글이라면 parentComment는 null이 들어가고, 대댓글이라면 parentComment에 해당 댓글 엔티티가 들어가는 형식이에요. DB에는 이런 방식으로 저장되고요. 

 

308번, 309번은 댓글, 310번은 대댓글!

 

현시점에서 바라본 놀토 프로젝트의 엔티티 구조에서 아쉬운 점은 바로 양방향 매핑이 너무 많다는 것인데요. 

양방향 매핑이 많으면 아무래도 단방향 매핑보다 관리 포인트가 증가하게 되어요. 

 

레벨 4 미션을 진행하면서 직접적인 객체 참조 대신, 참조할 객체의 Id만을 들고 있는 느슨한 결합 방식을 배웠는데요. 

매번 객체 매핑을 통해 직접적인 관계 맺기보다, Id+repository를 통해 간접적인 관계 맺기를 했더라면 양방향 매핑을 조금 쳐낼 수 있었을 것이에요. 

구조가 좀 더 느슨해지니 유지 보수와 확장에도 오히려(!) 더 유리할 수도 있고요. 

 

 

3. JPA 전략

[FetchType]

저희 팀은 기본적으로 연관 관계에 있는 엔티티를 가져옴에 있어서 FetchType.LAZY 전략을 사용하고 있어요. 

해당 엔티티가 꼭 필요할 때 쿼리를 날려 조회하자는 생각이었어요. 

 

또한 FetchType.EAGER가 가지는 단점이 많다고 판단한 것도 이유였어요. 

즉시 로딩은 쿼리에 바로 Join을 걸어 많은 결과를 반환하게 되는데, 이것이 성능에 문제를 일으킬 수도 있고,

findAll() 등의 전체 조회 메서드에서 N+1 문제를 발생시킬 수도 있어요. 

 

따라서 FetchType.Lazy를 기본적인 전략으로 가져가고, 연관 엔티티가 즉시 필요하다면 fetchJoin을 활용한 JPQL을 직접 작성하도록 했어요. 

 

[BatchSize]

Feed 엔티티의 경우 Like 엔티티와 FeedTech 엔티티 둘 모두와 일대다 연관관계를 갖는데요. 

관리자용 API를 만들면서, 둘 모두에 대한 정보를 가져와야 했던 경우가 생겼어요. 

 

FetchJoin의 경우, 일대다 연관관계가 2개 이상인 경우에 사용할 수 없는데요. 

따라서 BatchSize를 도입해 N+1 쿼리를 예방하도록 했어요. 

하위 엔티티 로딩 시, 한 번에 상위 엔티티의 ID를 IN 쿼리로 로딩하도록 하는 것이었죠.

 

현재는 프로젝트 전역에 BatchSize를 도입해 N+1 문제가 발생하는 것을 막고 있어요. 

 

 

 

읽어주셔서 감사합니다!

궁금하거나 이야기 나누고 싶은 주제 있다면 댓글로 남겨주세요 😁😁

 

반응형

'Web > 프로젝트' 카테고리의 다른 글

크립토하우스 - 2편. 개발  (4) 2022.05.16
크립토하우스 - 1편. 시작한 이유/느낀 점  (0) 2022.04.04
Easy Deploy - 3편. 운영  (2) 2021.11.14
Easy Deploy - 2편. 개발  (2) 2021.10.24
Easy Deploy - 1편. 기획  (0) 2021.10.23

댓글