뉴스피드_좋아요

지혜·2025년 2월 21일
0

💡 https://github.com/H5-dev-project/newsfeed

첫 기초프로젝트에서 내가 담당한 기능은 좋아요를 게시물과 댓글에 추가할 수 있는 기능이다.

요구사항

좋아요 기능

  • 사용자는 게시물 또는 댓글에 좋아요등록 또는 취소 가능
  • 본인이 작성한 게시물과 댓글에는 좋아요 불가능
  • 동일 게시물, 댓글에는 사용자 당 1회 좋아요 가능

ERD

처음 팀원들과 테이블을 생성할 때 약간의 의견 차이가 있었다.
나는 likes테이블을 하나로 가져가서 targetIdtargetType을 이용하여 board, comment를 구분하자는 의견이었고, 다른 팀원분은 commentLikesboardLikes 테이블을 각각 만들자는 의견이었다.

📌 하나의 테이블로 관리할때

  • 장점
    • 코드관리가 용이함
    • 향후 좋아요 기능이 게시물, 댓글 이외의 다른 기능에도 추가될 때 targetType만 추가하면 되므로 유지보수가 편리
  • 단점
    • 매번 targetType을 기준으로 조건을 추가해서 작업해야하는 번거로움 존재
    • 추후 데이터가 많아질 경우 쿼리 성능에 영향

📌 테이블을 나눠서 관리할때

  • 장점
    • 동일한 테이블로 관리할 때 보다 데이터가 적으므로 빠른 성능 기대
    • board, comment 테이블과 각각 외래키 제약을 걸 수 있기 때문에 데이터 무결성 보장
  • 단점
    • 동일한 코드가 각각 생성되기 때문에 유지보수 및 코드 관리가 어려움
    • 향후 좋아요 기능이 게시물, 댓글 이외의 다른 기능에도 추가될 때 새로운 테이블 및 코드를 추가해야함

각각의 장단점이 명확하기 때문에 논의를 했었고, 튜터님께도 문의드려본 결과 테이블을 나눠서 관리하기로 결정했다.

📝 기능 개발 과정

1차 시도

예시)

@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);
    }
}

처음에는 위와 같이 댓글과 게시글에 종속시켜서 개발하려고 했었다.
이렇게 개발하면 좀 더 쉽게 개발할 수 있다는 장점이 있다.
그러나 처음 생각한것 처럼 개발하면 중복된 코드가 생기는 문제가 발생하기 때문에 고민이었다.

2차 시도

댓글 좋아요 기능과 게시글 좋아요 기능은 로직이 동일하기 때문에 service를 공통 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) 대댓글 기능 추가 등 을 가정하여 개발을 시도해봤어도 좋았을 것 같다. 다음에 이와 같이 공통 코드를 만들어서 개발할 기회가 주어진다면 인터페이스까지 고려해서 개발을 진행해 봐야겠다.

💡 인터페이스로 만들 때 좋은점

  1. 다형성이 필요한 경우
  • 다양한 구현체를 쉽게 교체할 수 있음
  1. 특정 구현을 주입 받아서 사용할 때
  • 인터페이스를 활용한다면 구현체 변경이 용이해짐
  • 현재 코드를 기준으로 interface를 추가한다면, controller에서 인터페이스만 의존하도록 변경 가능
// 기존코드
    @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());
    }
  1. 테스트 코드에서 Mocking을 이용할 때
  • 테스트 코드 작성 시 Mockto를 이용할 때 인터페이스가 존재할 경우 간단하게 작업할 수 있음
@Mock
private LikesService likesService; // 인터페이스 기반으로 Mock 주입

*** 만약 interface가 없다면?
@Mock으로 주입할 때 별도의 설정이 필요!

그외 트러블 슈팅

0개의 댓글

관련 채용 정보