질문, 피드백 등 모든 댓글 환영합니다.
요구사항에 맞춰 엔티티를 개발합니다. 개발 순서는 핵심 필드 -> JPA 연관관계 매핑 -> Auditing -> 비지니스 로직
순으로 개발합니다.
Member -> Post -> Comment -> Heart 순으로 개발하겠습니다.
Member
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id @GeneratedValue @Column(name = "member_id")
private Long id;
private String loginId;
private String password;
private String name;
}
Post
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {
@Id @GeneratedValue @Column(name = "post_id")
private Long id;
private String title;
private String body;
private int heartNum;
private int commentNum;
}
#0 기획에서 보셨듯이 이 예제에선 Post 테이블에 좋아요 수, 댓글 수 컬럼을 사용했습니다.
이런 방식으로 개발한 이유는 게시글을 리스트로 조회할 때 좋아요 수, 댓글 수를 함께 조회하게 되는데 게시글과 좋아요, 댓글은 모두 1 : N 관계이므로 이를 모두 조회 시 쿼리 데이터의 양이 많아지게 됩니다.
이 프로젝트는 Heroku echo dyno를 통해 배포할 예정인데 헤로쿠 무료 db는 너무 느리기에 db 조회를 최소한으로 하기 위해 좋아요 수, 댓글 수 컬럼을 Post에 따로 생성했습니다.
사실 이러한 구조로 개발하면 테이블(엔티티)가 UI에 종속적이게 됩니다. 때문에 후에 UI가 변경되게 된다면 테이블부터 전반적인 비지니스 로직의 변경이 일어나므로 절대 좋은 설계가 아니라 생각하지만 이 예제에서는 어쩔 수 없이 이러한 방식을 사용했습니다.
추가) 초반에 생각 했던 부분 외에도 여러 부분에서 문제를 발생시켜 이후에 결국 heartNum, commentNum 컬럼을 제거한 정규화 된 테이블로 바꾸어 사용했습니다.
Comment
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment {
@Id @GeneratedValue @Column(name = "comment_id")
private Long id;
private String body;
}
Heart
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Heart extends BaseTimeEntity {
@Id @GeneratedValue @Column(name = "heart_id")
private Long id;
}
엔티티의 연관관계를 매핑할 때 주의사항이 있습니다. 연관관계의 주인을 잘 설정을 해주어야 하는데 이 내용은 JPA #4 연관관계 매핑에 기술해 두었으니 참고 바랍니다.
Member
public class Member {
...
@OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE)
private List<Post> posts = new ArrayList<>();
@OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE)
private List<Comment> comments = new ArrayList<>();
@OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE)
private List<Heart> hearts = new ArrayList<>();
public static Member createMember(String loginId, String password, String name) {
Member member = new Member();
member.loginId = loginId;
member.password = password;
member.name = name;
return member;
}
}
Member와 Post, Comment, Heart 모두 1:N 관계이며 비지니스 로직 상 만약 Member가 삭제된다면 Member와 연관된 엔티티 모두 삭제되어야 하므로 CascadeType.REMOVE
를 설정했습니다.
CascadeType.REMOVE
로 엔티티를 제거하게 되면 list 조회 쿼리가 발생하고 이를 하나씩 삭제하게 됩니다. 때문에 후에 CascadeType.REMOVE
를 제거하고 벌크 삭제 쿼리를 생성하여 직접 삭제하도록 했습니다.
Post
public class Post {
...
@OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE)
private List<Heart> hearts = new ArrayList<>();
@OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE)
private List<Comment> comments = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id")
private Member member;
public static Post createPost(String title, String body, Member member) {
Post post = new Post();
post.title = title;
post.body = body;
post.heartNum = 0;
post.commentNum = 0;
// Member 연관관계 추가
post.member = member;
member.getPosts().add(post);
return post;
}
}
Post는 Heart, Comment와 1 : N 관계이며 Member와는 N : 1 관계로 연관관계의 주인이 됩니다.
때문에 생성 메서드에 Member와의 연관관계를 추가하는 로직을 포함했습니다.
Comment
public class Comment {
...
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "posts_id")
private Post post;
public static Comment createComment(String body, Member member, Post post) {
Comment comment = new Comment();
comment.body = body;
comment.post = post;
post.getComments().add(comment);
comment.member = member;
member.getComments().add(comment);
return comment;
}
}
Comment는 Member, Post와 N : 1 관계로 연관관계의 주인이 됩니다.
Heart
public class Heart {
...
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id")
private Post post;
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id")
private Member member;
public static Heart createHeart(Post post, Member member) {
Heart heart = new Heart();
heart.post = post;
heart.member = member;
post.getHearts().add(heart);
member.getHearts().add(heart);
return heart;
}
}
Heart는 Member, Post와 N : 1 관계로 연관관계의 주인이 됩니다.
Auditing
은 JPA에서 제공하는 기능입니다. 일반적인 서비스에서 테이블을 구성할 때 공통 컬럼이 생기는 경우가 많습니다. ex) 생성일, 수정일, 수정한 사람, 삭제일 등
때문에 이런 부분에서 중복 코드가 필연적으로 발생하는데 Auditing
이 이를 자동화 해줍니다.
BaseTimeEntity
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
저는 간단히 생성일, 수정일에 관한 필드만 만들어주었습니다.
@EntityListeners
는 콜백을 요청하는 어노테이션으로 AuditingEntityListener.class
를 인자로 넘기면 Auditing 기능을 적용할 수 있습니다.
@MappedSuperclass
는 엔티티의 공통 매핑 정보를 모으는 역할을 합니다. 해당 클래스에 정의된 필드를 상속 받는 클래스에 매핑 정보를 제공할 수 있습니다. 테이블과는 상관이 없고 단순히 개발할 때 편리함을 주는 어노테이션입니다.
@CreatedDate
생성일에 대한 정보를 자동으로 생성하는 어노테이션입니다. 이와 비슷한 어노테이션은 @CreatedDateBy
, @LastModifiedDate
등이 있습니다.
Member
public class Member extends BaseTimeEntity {...
}
BaseTimeEntity를 상속해주면 해당 필드가 자동으로 매핑되어 사용할 수 있습니다.
Member를 포함하여 모든 엔티티에 BaseTimeEntity를 상속해줍니다.(코드 생략)
XxxApplication
@EnableJpaAuditing
@SpringBootApplication
public class CommunityApplication {..
스프링 애플리케이션 메인 클래스에 @EnableJpaAuditing
를 적용해야만 Auditing 기능을 사용할 수 있습니다.
실제로 개발할 당시에는 Service를 개발하며 엔티티에 비지니스 로직을 작성했지만 가독성을 위해 해당 게시글에 포함했습니다.
Post
public class Post {
...
public void update(String title, String body) {
this.title = title;
this.body = body;
}
public void minusHeartNum() { this.heartNum--; }
public void plusHeartNum() {this.heartNum++; }
public void minusCommentNum() {this.commentNum--; }
public void plusCommentNum() {this.commentNum++; }
}
게시글 수정 메서드와 게시글에 댓글, 좋아요가 추가될 때 사용되는 메서드입니다.
위에서 언급했듯이 update()
를 제외한 메서드는 추후에 제거될 예정입니다.
Comment
public class Comment {
...
public void update(String body) { this.body = body; }
}
엔티티 개발이 완료되었습니다. 다음 블로그엔 Controller, Service, Repository 개발 내용에 대해 작성하겠습니다.