여러 게시판을 만들기로 결정한 후로, 테이블 상속과 관련하여 고민이 많았다. 게시글 엔티티의 구성요소에는 '게시글 id', '작성자', '제목', '내용', '댓글' 이 겹쳤다. 거의 다 겹쳤다고 보는게 맞다. 위의 초기 디자인을 보면 알 수 있듯이, 정말 많은 부분이 서로 다른 게시판에 공통적으로 포함됐다. 자연스럽게 게시판은 상속의 형태로 구현해야겠다고 생각했다.
마침 JPA에서 상속을 사용하는 방법에 대해 공부한 참이었다. 이를 적절히 적용해볼 수 있겠다 싶었다.
상속 전략은 단일 테이블 전략을 선택했다. 단일 테이블 전략을 선택한 이유는 다음과 같다.
- 단일 테이블 전략의 가장 큰 단점은 '테이블 하나가 지나치게 비대해질 수 있다'는 점과 'null을 허용해야 한다.'는 점이다.
- 내가 생각했을 때, 사실 두 문제가 발생하는 이유는 하나다. '지나치게 많은 요소들을 하나의 상위 객체로 추상화 하기 때문에 발생한다'는 점이다.
- 하나의 단일 테이블에 너무 많은 하위 테이블들을 상속시키면, 해당 테이블들이 가진 특성마다 새로운 칼럼이 추가되어야 한다. 당연히 테이블의 크기가 커진다. 이에 더해 그 테이블들이 가진 특성은 전부 nullable해야 한다. 허용해야 하는 null이 너무 많아진다.
- 그러나 내 프로젝트의 경우, 추가되는 개별 칼럼은 두 개밖에 없다. 물론 추후 추가될 여지가 없지는 않지만, 그럼에도 겨우 두 개 추가하는 데 단일 테이블 전략의 장점인 빠른 조회를 포기하긴 애매하다고 생각했다.
모든 프로그래밍 과정이 그렇듯, 이 역시 트레이드 오프에 놓여있다. 조인 전략을 사용해서 테이블 정규화를 할 것인가? 단일 테이블 전략을 사용해서 조회 성능에 이점을 둘 것인가? 여기서 선택의 기로에 놓여있는 것은 위에서 언급했듯이 '얼마나 많은 테이블들을 상속시킬 것인가?'에 놓여있다고 생각한다. 상속할 테이블들이 많다면, 그만큼 테이블 정규화 필요성이 높아지는 것이고 조인 전략을 선택하는 것이 더 낫다고 생각한다.
아무튼 이렇게 상속 전략을 선택했고, 이에 맞춰 각 클래스들을 만들었다.
@Entity
@Getter
@Setter
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Board{
@Id
@GeneratedValue
@Column(name = "BOARD_ID")
private long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@NotNull
@Column(name = "TITLE")
private String title;
@NotNull
@Column(name = "CONTENT")
private String content;
@OneToMany(mappedBy = "board")
private List<Comment> comments = new ArrayList<>();
}
우선 부모 테이블인 Board다. Board객체를 만들어서 사용할 일은 없다. 하위 객체들은 계속 만들어질 것이지만, Board자체는 만들 일이 없다. 따라서 추상 클래스로 만들었다. 상속 전략은 단일 테이블 전략으로 선택했으며, id, 작성한 Member, 제목, 내용, 댓글들이 담겨있다.
사실 처음에는 @MappedSuperClass를 사용해서 해당 정보들을 넘길까도 생각했는데, 댓글을 연관시키려니 그 방법이 불가능했다. 댓글은 결국 하나로 통일된다. 댓글이 뭐 게시판마다 다르게 달리지 않는다고 상정했다. 다시 말해, 댓글이 여러 갈래로 나뉘지는 않는다는 말이다. 그럼 결국 하나의 댓글 클래스만으로 큰 Board테이블과 연관되어야 한다는 뜻인데, 이를 위해서는 Board자체를 하나의 테이블로 둬야했다.
또한 DTO에 있던 @NotNull 제약 조건을 엔티티로 옮겼다. 포스트맨으로 돌려보니 @NotNull이고 뭐고 null로 그냥 들어오더라.
아무튼 이제 하위 클래스를 살펴보자. 자유 게시글 클래스는 아래와 같이 만들어졌다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DiscriminatorValue(value = "FREE")
public class FreeBoard extends Board {
@Builder
public FreeBoard(String title, String content){
this.setTitle(title);
this.setContent(content);
}
public void modify(FreeBoardDto.Put putDto) {
this.setTitle(putDto.getTitle());
this.setContent(putDto.getContent());
}
}
지금은 Member를 게시글에 넣는 로직이 없다. 로그인 로직을 아직 구현하지 않았기 때문이다. 추후 로그인 로직을 구현하고, 로그인 한 사용자가 글을 쓴다는 요구사항을 구현하는 과정에서 해당 로직도 추가되어야 할 것이다.
다음으로 구인구직 게시글이다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DiscriminatorValue(value = "RECRUITMENT")
public class Recruitment extends Board {
@Enumerated(value = EnumType.STRING)
@Column(name = "RECRUITMENT_TYPE")
private Type type;
@Getter
public enum Type{
OFFER("구인"),
SEARCH("구직");
private final String type;
Type(String type) {
this.type = type;
}
}
}
구인구직 게시글은 글머리를 통해 구인 게시글인지, 구직 게시글인지를 구분한다. 상위 테이블의 DTYPE과 구분하기 위해 해당 칼럼의 이름을 RECRUITMENT_TYPE으로 설정했다. 구인인지 구직인지를 구분하는 데에는 enum을 사용했다. 구인구직 게시판에서 글머리로 이 두 개 이외에 수정될 일이 있을까? 생각해보면 아마 서비스가 종료하는 순간까지 바뀔 일이 없을 것이었다. 따라서 enum으로 둘을 구분하고 상수처리했다.
마지막으로 리뷰 게시글이다.
@Entity
@Getter
@Setter
public class Review extends Board {
@ManyToOne
@JoinColumn(name = "BARBER_SHOP_ID")
private BarberShop barberShop;
}
하나의 바버샵에 여러 리뷰가 담길 수 있기에 @ManyToOne으로 바버샵과 다대일 매핑시켰다.
마지막으로 Member클래스를 한 번 살펴보려고 한다. 지금 Member를 살펴보는 이유는 매핑 시키는데 있어 궁금한 점이 있어서다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private long id;
private String name;
@Enumerated(value = EnumType.STRING)
private Role role;
@OneToMany(mappedBy = "member")
private List<FreeBoard> freeBoards = new ArrayList<>();
@OneToMany(mappedBy = "member")
private List<Review> reviews = new ArrayList<>();
@OneToMany(mappedBy = "member")
private List<Recruitment> recruitments = new ArrayList<>();
public void writeFreeBoard(FreeBoard freeBoard) {
this.freeBoards.add(freeBoard);
if (freeBoard.getMember() != this) {
freeBoard.setMember(this);
}
}
@Getter
public enum Role {
ADMIN("관리자"),
BARBER("바버"),
CUSTOMER("고객");
private final String role;
Role(String role) {
this.role = role;
}
}
}
현재 Member 테이블에는 각기 다른 게시글들이 일대다 관계로 매핑되어있다. 이게 추후에 작성할 때 Board테이블에 잘 들어가는지, 아니면 오류가 발생할지는 두고봐야 할 일이다. 일단은 해당 회원의 회원정보 페이지로 넘어갔을 때, 세 게시글들을 따로 구분해서 보여주고 싶어서 저렇게 설계했다. 마지막으로 이렇게 만들어진 애플리케이션을 실행했을 때, 테이블이 어떻게 생성되는지 확인해보자.
나름대로 크게크게 추상화가 잘 된 것 같아 만족스럽다. 특히 이번 시도의 목적이었던 게시판 테이블의 상속이라는 전략이 잘 완성된 것 같아 만족스럽다. 이후 각 게시판들의 CRUD기능들을 구현하면서 좀 더 수정하거나 리팩토링 할 부분이 있으면 진행해야겠다.
전체 코드는 링크에서 확인할 수 있다.
'Programming > TDD Project' 카테고리의 다른 글
Pull Request 016. Base Entity를 통해 데이터 생성일 / 수정일 설정하기 - 제어하기 어려운 코드 개선, static 메서드 mocking 테스트 (1) | 2023.11.28 |
---|---|
Pull Request 015. API 문서화 적용하기 - Spring Rest Docs (2) | 2023.11.28 |
Pull Request 013 - 자유 게시글 수정 기능 컨트롤러 계층 TDD로 구현하기 (1) | 2023.11.27 |
Pull Request 012 - 자유 게시글 등록 기능 컨트롤러 계층 TDD로 구현하기 (0) | 2023.11.20 |
Pull Request 011 - 자유게시글 삭제 기능 TDD로 구현해보기 (1) | 2023.11.11 |
댓글