Spring Example: Community #1 Entity 개발

함형주·2023년 1월 2일
0

질문, 피드백 등 모든 댓글 환영합니다.

요구사항에 맞춰 엔티티를 개발합니다. 개발 순서는 핵심 필드 -> JPA 연관관계 매핑 -> Auditing -> 비지니스 로직 순으로 개발합니다.

Entity 개발

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

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 개발 내용에 대해 작성하겠습니다.

github , 배포 URL (첫 접속 시 로딩이 걸릴 수 있습니다.)

profile
평범한 대학생의 공부 일기?

0개의 댓글