첫 기초프로젝트에서 내가 담당한 기능은 좋아요
를 게시물과 댓글에 추가할 수 있는 기능이다.
✅ 좋아요 기능
좋아요
를 등록
또는 취소
가능1회
좋아요 가능처음 팀원들과 테이블을 생성할 때 약간의 의견 차이가 있었다.
나는 likes
테이블을 하나로 가져가서 targetId
과 targetType
을 이용하여 board, comment를 구분하자는 의견이었고, 다른 팀원분은 commentLikes
와 boardLikes
테이블을 각각 만들자는 의견이었다.
📌 하나의 테이블로 관리할때
targetType
만 추가하면 되므로 유지보수가 편리targetType
을 기준으로 조건을 추가해서 작업해야하는 번거로움 존재📌 테이블을 나눠서 관리할때
외래키
제약을 걸 수 있기 때문에 데이터 무결성 보장각각의 장단점이 명확하기 때문에 논의를 했었고, 튜터님께도 문의드려본 결과 테이블을 나눠서 관리하기로 결정했다.
예시)
@RestController
@RequiredArgsConstructor
@RequestMapping("/comments")
public class CommentController {
private final CommentService commentService;
@PostMapping("/comments/{commentId}/likes")
public ResponseDto<?> addCommentLikes(@PathVariable Long commentId){
String userId = getCurrentMemberInfo().getUserId();
return likesService.addCommentLike(commentId, userId);
}
}
처음에는 위와 같이 댓글과 게시글에 종속시켜서 개발하려고 했었다.
이렇게 개발하면 좀 더 쉽게 개발할 수 있다는 장점이 있다.
그러나 처음 생각한것 처럼 개발하면 중복된 코드가 생기는 문제가 발생하기 때문에 고민이었다.
댓글 좋아요 기능과 게시글 좋아요 기능은 로직이 동일
하기 때문에 service를 공통 service를 만들어 사용하기로 했다.
abstract
키워드를 사용하여 공통 service를 만들었다.
📌 abstract 클래스
// 공통 코드
// 좋아요 등록
// T likeEntity : comment or board
public ResponseDto<?> addLike(String userId, String entityId, T likeEntity) {
if(userId.equals(likeEntity.getUsers().getId())){
throw new IllegalArgumentException("본인이 작성한 글에는 좋아요를 누를 수 없습니다.");
}
if(findLike(entityId, userId).isPresent()) {
throw new IllegalArgumentException("좋아요는 한 번만 가능합니다.");
};
likesRepository.save(likeEntity);
return ResponseDto.success("좋아요 등록 완료");
}
// 좋아요 취소
public ResponseDto<?> cancelLike(String entityId, String userId){
T likeEntity = findLike(entityId, userId)
.orElseThrow(() -> new IllegalArgumentException("좋아요 등록이 되어있지 않습니다."));
likesRepository.delete(likeEntity);
return ResponseDto.success("좋아요 취소 완료");
}
// 타입에 맞게 repository 넘겨주기
private Optional<T> findLike(String entityId, String userId){
if(likesRepository instanceof BoardLikesRepository boardLikesRepository){
return (Optional<T>) boardLikesRepository.findByBoardIdAndUsersId(entityId, userId);
}else if (likesRepository instanceof CommentLikesRepository commentLikesRepository) {
return (Optional<T>) commentLikesRepository.findByCommentIdAndUsersId(Long.parseLong(entityId), userId);
}
return Optional.empty();
이렇게 추상화 클래스를 만든 뒤 각각 commentLike, boardLikeService, repository를 생성하여 진행했다.
프로젝트를 마무리하고 한 가지 아쉬웠던 점은 인터페이스를 활용해보면 어땠을까? 라는 점이었다.
지금 현재 상황에서는 다양한 방식으로 likesService가 사용될 가능성이 없기 때문에 큰 문제가 없지만 만약에 추후 기능이 추가 ex) 대댓글 기능 추가 등
을 가정하여 개발을 시도해봤어도 좋았을 것 같다. 다음에 이와 같이 공통 코드를 만들어서 개발할 기회가 주어진다면 인터페이스까지 고려해서 개발을 진행해 봐야겠다.
// 기존코드
@PostMapping("/{type}/{typeId}")
public ResponseDto<?> addBoardLike(@UserSession AuthUsers authUser, @PathVariable String type, @PathVariable String typeId){
return switch (type){
// 타입별로 service를 지정해주어야함
case "boards" -> boardlikesService.addBoardLike(typeId, authUser.getUserId());
case "comments" -> commentlikesService.addCommentLike(Long.parseLong(typeId), authUser.getUserId());
default-> throw new IllegalArgumentException("올바른 타입이 아닙니다.");
};
}
@DeleteMapping("/{type}/{typeId}")
public ResponseDto<?> cancelBoardLike(@UserSession AuthUsers authUser, @PathVariable String type, @PathVariable String typeId) {
return switch (type){
case "boards" -> boardlikesService.cancelBoardLike(typeId, authUser.getUserId());
case "comments" -> commentlikesService.cancelCommentLike(Long.parseLong(typeId), authUser.getUserId());
default -> throw new IllegalArgumentException("올바른 타입이 아닙니다.");
};
}
// interface추가 코드
@PostMapping("/{type}/{typeId}")
public ResponseDto<?> addLike(@UserSession AuthUsers authUser, @PathVariable String type, @PathVariable String typeId){
return likesService.addLike(authUser.getUserId(), typeId);
}
@DeleteMapping("/{type}/{typeId}")
public ResponseDto<?> cancelLike(@UserSession AuthUsers authUser, @PathVariable String type, @PathVariable String typeId) {
return likesService.cancelLike(typeId, authUser.getUserId());
}
Mockto
를 이용할 때 인터페이스가 존재할 경우 간단하게 작업할 수 있음@Mock
private LikesService likesService; // 인터페이스 기반으로 Mock 주입
*** 만약 interface가 없다면?
@Mock으로 주입할 때 별도의 설정이 필요!