게시판 통합 댓글 기능을 만들면서 생각했던 고민과 코드 구조에대해 말하고자 한다.
해당 기능의 목표는 Board 와 댓글간 연관 관계는 없다.
단 Reply Table 에서 각각의 Board 의 ID 와 Type 정보를 들고 있으며 게시글을 조회해오고 싶을때
Board ID 와 Type 에 맞는 row를 반환 할 수 있도록 하였다.
이에 따른 Reply Save 구조를 살펴 볼 것 이다.
소스코드를 먼저 살펴보자.
@Service
@RequiredArgsConstructor
@Transactional
public class ReplyService {
public ResponseReply replySave(Long boardId, RequestSaveReply requestSaveReply){
//TODO SAVE 하는 로직
}
위에서 설명한 기능의 목표에 맞는 메서드를 하나 만들고 내부에서 이제 SAVE 로직을 작성하면된다.
해당 댓글 저장 기능은 연관관계가 없음으로 검증해야 하는 부분이 많은데.
- BoardID 와 BoardType 에 맞는 게시글이 정말로 존재하는지 검사(existsById)
- 해당 부모 댓글인지 ?, 자식 댓글인지 ?, 자식에 자식 댓글인지 ? 검사를 해줘야한다.
중요 검증 로직은 다음과 항목과 같은데 이번 글에서 말하고자 하는 내용은 1번 검증에 대한 코드를 작성하면서
적용했던 구조와 여러가지 구조를 살펴 볼 것이다.
Save 에서 중요 로직은 무엇인가 ?
위 질문에 대한 대답은 사람마다 다를 수 있다고 생각한다.
(나는 DB 저장하는 것이 Save 의 메서드의 중요 로직이자 목표라고 생각한다.)
즉 메서드 내부에서 검증같은 부분은 검증을 하는전용 메서드를 만들어서 할 일이지 Save 메서드에서 할 일이 아니라고 생각한다.
그렇다면 ? 현재 Save 메서드에서 들어오는 파라미터를 보자.
replySave(Long boardId, RequestSaveReply requestSaveReply)
설명을 하자면 Board ID 는 말그대로 게시글의 PK 값이 될것이다.
RequestSaveReply 는 내부에 reply 에대한 정보들이 들어있다 (BoardType , content , writer 등) 이 존재하는데.
우리가 집중적으로 바라볼 값은 Board ID 와 RequestSaveReply.BoardType 이다.
존재여부 검사
BoardID 와 BoardType 에 맞는 게시글이 정말로 존재하는지 검사(existsById) 를 하기위해서 어떻게 로직을 작성할 것인가?
예를 들어 커뮤니티 게시판 과 스터디 게시판이 각각의 Entity 로 있다고 가정 하였을때 각각의 엔티티에 대한 repo 에 존재하냐고 물어 볼 것이다.
- 어떻게 프로젝트 구조를 가져가야 변경에 취약하지 않고 관리하기 쉬운가?
- 통합 댓글 기능으로써 가져다가 사용할때 얼마나 쉽게 사용할수 있는가?
통합 기능을 만들때는 다음과 같은 항목을 생각하고 코드를 작성해야 서로가 편하고 유지보수 하기 쉽다고 생각한다.
그러면 존재여부 검사를 할때 어떠한 구조를 가져 갈것인가?
피드백 받은 내용으로는 다음과 같다.
- 팩토리 메서드 패턴
- 인터페이스를 이용
하나씩 해보자.
Factory method 패턴
우리가 구분 할 수 있는 값은 BoardType 이다.
해당 type 을 이용하여 메서드 내부에서 type 에 맞는 repo를 호출 해주기만 한다면 매우 간단하게 이용할 수 있다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class BoardValidationService {
private final UsedItemRepository usedItemRepository;
private final RecruitRepository recruitRepository;
private final BoardRepository boardRepository;
public Long existBoard(Long boarId, BoardType boardType) {
boolean result = false;
if (BoardType.USEDITEMS.equals(boardType)) {
result = usedItemExist(boarId);
}
if (BoardType.RECRUIT.equals(boardType)) {
result = recruitExist(boarId);
}
if (BoardType.SEMINAR.equals(boardType) || BoardType.STUDY.equals(boardType)) {
result = boardExist(boarId);
}
if (!result) {
throw new NotFoundResource(HttpStatus.NOT_FOUND.toString(),
String.valueOf(boarId));
}
return boarId;
}
public ResponseReply replySave(Long boardId, RequestSaveReply requestSaveReply) {
Long provenBoardId = boardValidationService.existBoard(boardId, requestSaveReply.getBoardType());
}
그렇다면 이러한 구조는 문제점이 없는것인가?
내가 생각하는 문제는 다음과 같다.
- 보드의 엔티티가 증가할 수록 해당 서비스에는 의존성이 추가 된다.
- 만약 보드의 종류가 200개라면 if 문이 200개? (Switch 사용 해도 case 가 늘어나는건 같다).
- 보드가 추가 되면 댓글기능을 사용하고자하면 해당 서비스는 무조건 코드 변경이 일어남
그렇다면 장점은 무엇일까?
- 로직이 흩어지지 않음으로 한 클래스에서 관리할 수 있음
- 한 클래스에서 관리함으로 로직을 파악하기 쉽다.
이러한 장 단점이 있다고 생각한다.
인터페이스 이용
인터페이스를 이용한다는 의미는 매우 간단하다.
현재 각각 엔티티(보드) 들은 엔티티에 대한 서비스를 하나씩 가지고 있을 것이다.
그렇다면 현재 각각의 보드들의 공통적인 기능을 빼고 서비스에 상위 인터페이스를 둠으로써 구현 클래스 타입이 아닌
인터페이스 타입을 이용함으로써 각각 서비스에서 구현체를 파라미터로 받는 다면? 다음과 같은 구조가 만들어 질 것이다.
공통 서비스(상위 인터페이스)
public interface CommonService {
boolean existBoard(Long id);
}
구체클래스(MemberService)
@Service
@Transactional
@RequiredArgsConstructor
public class MemberService implements CommonService {
private final MemberRepository memberRepository;
@Override
public boolean existBoard(Long id) {
return memberRepository.existsById(id);
}
}
구체클래스(UsedSerivce)
@Service
@RequiredArgsConstructor
@Transactional
public class UsedItemService implements CommonService {
private final UsedItemRepository usedItemRepository;
@Override
public boolean existBoard(Long id) {
return usedItemRepository.existsById(id);
}
}
모든 서비스가 가지는 공통적인 기능을 인터페이스로 묶은 뒤 replySave 를 호출하는 각각의 엔티티의 컨트롤러들은
각자의 엔티티에 맞는 서비스를(구체클래스)를 메서드로 넘겨준다면 사용하는 쪽(replySave) 에서는 구체타입을 몰라도 간단하게 존재 유무를 알 수 있다.
메서드를 호출하는 컨트롤러 부분(UsedController)
@PostMapping("/item/{no}/reply")
public ResponseEntity<ResponseReply> saveReply(@PathVariable Long no, @Valid @RequestBody RequestSaveReply requestSaveReply) {
replyService.replySave(no, reply, usedItemService)
//생략
}
repleSave
public ResponseReply replySave(Long boardId, RequestSaveReply requestSaveReply , CommonService commonService){
boolean result = commonService.existBoard(boardId);
//생략
}
해당 방식의 장점은 무엇일까?
- 인터페이스의 구현체를 인자로 받음으로써 새로운 타입의 보드가 추가되어도 인터페이스 타입을 참조하고 있음으로 변동이 일어나지 않는다.
- 댓글 서비스는 매우 코드가 간결해짐
해당 방식의 단점은 무엇일까?
- 새로운 보드 타입이 생길때 마다 CommonService 를 구현하고 있어야함.
결론
앞서 살펴본 방식에 대해서는 정답이 없는거 같다. 각각의 프로젝트를 진행함에 따라 어떤 방식이 현재 진행하고 있는 프로젝트에 더 어울리는지 파악하고
좀 더 진행하는 프로젝트에 어울리는 패턴을 사용하는 것이 정답이다.
이러한 프로젝트 구조에 대해선 정답이 없으니 여러가지 방법으로 풀어낼수 있는 구조를 다양하게 아는 것이 정답일 것 같다.
긴글 읽어 주셔서 감사합니다.
-끝-
'나만의공부(이슈정리)' 카테고리의 다른 글
Entity 설계 다시하기 (0) | 2021.06.23 |
---|---|
토이프로젝트 다시한번 살펴보기! (1) | 2021.06.16 |
어설픈 객체지향은 안티패턴을 만들어낸다. (3) | 2021.03.05 |
[Spring-boot] CORS , SOP 개념 및 설정 (0) | 2021.01.24 |
[JAVA] live -study 대시보드 만들어보기 (gitHub 라이브러리 사용) (0) | 2020.12.06 |