기록하며 성장한다 - 개발, 회고

개발 회고록

[ 개발 회고록 ] 게시글 평균 점수 기능을 구현해보자

전대홍 2023. 11. 27. 20:26

 

해당 포스팅은 JPA native Query와 Spring Boot를 이용한 CRUD 개발 3편이다.

F-lab 멘토링 과제를 위한 개발을 진행중이며,

멘토님께 피드백을 받으며 개발을 할 때 신경써야 하는 부분을 찾아가고 있다.

 

이전 글 <<< 클릭하기

 


신규로 개발 해야하는 기능

1. 게시글 별로 평점 기능 구현

 


이슈 해결

오늘은 특별한 이슈가 없었음.

 


개발 Main 내용

1. 게시글 별로 평점 기능 구현

F-lab 멘토링 5주차에서 새로운 과제가 등장했다. 바로 평점 기능을 구현하는 것이었다.

현재 게시글에는 평점 기능이 존재하지 않는데, 각 회원들이 점수를 부여할 수 있고, 해당 게시글을 조회할 때 그 점수들의 평균 점수를 함께 반환하면 된다.

저번에 완료하였던 배타적 잠금을 레디스로 해결한 조회수 기능과, @Valid를 활용한 밸리데이션체크 그리고 전체적인 추가 피드백은 평점 기능까지 완료하면 해주신다고 했다.

그래서 오늘은 평점 기능을 설계하고 구현해보았다.

 

설계

평점을 어떻게 만들까 고민을 하다가 우선 신규 테이블을 만들고, 게시글 조회 시 점수 테이블에서 해당 게시글 ID의 모든 ROW를 찾아서 스코어를 더하고, 사람 수만큼 나누는 로직을 생각하였다.

그리고 점수는 추가, 수정 로직만 만들기로 하였다. 웹툰 사이트 등에서도 보면 점수를 주거나 변경은 가능하지만, 한 번 점수를 주면 취소하는게 없던 것에서 생각해내었다.

다만 회원 탈퇴나, 게시글 삭제시 외래키 문제가 발생할 수 있으므로 테이블에서 삭제하는 로직은 구현하기로 하였다.

 

DB

평점 관련 테이블을 새로 만들어야겠다는 생각을 하였고, 유저 테이블의 USER_ID와 게시판 테이블의 BOARD_ID를 모두 기본키, 외래키로 지정하여 연결하도록 개발하였다.

거창하게 만들지는 않았고

CREATE TABLE TB_BOARD_SCORE (
    USER_ID BIGINT(20),
    BOARD_ID BIGINT(20),
    SCORE INT,
    CRTE_DTTM DATETIME,
    UPDT_DTTM DATETIME,
    PRIMARY KEY (USER_ID, BOARD_ID),
    FOREIGN KEY (USER_ID) REFERENCES TB_USER(USER_ID),
    FOREIGN KEY (BOARD_ID) REFERENCES TB_BOARD(BOARD_ID)
);

이렇게 테이블을 추가하였다.

 

 

Entity

@Getter
@Entity
@ToString
@NoArgsConstructor
@Table(name = "TB_BOARD_SCORE")
public class Score {

    @Id
    @Column(name = "BOARD_ID", nullable = false)
    private Long boardId;

    @Column(name = "USER_ID", nullable = false)
    private Long userId;

    @Column(name = "SCORE", nullable = false)
    private int score;

    @Column(name = "CRTE_DTTM", nullable = false)
    private LocalDateTime createDateTime;

    @Column(name = "UPDT_DTTM", nullable = false)
    private LocalDateTime updateDateTime;
}

Entity를 위와 같이 구성하였는데, 복합키의 경우에는 Serializable 이라는걸 구현해야된다고 한다. 하지만 아직 해결을 하지 못하였고, 추후 이 부분을 수정하고자 한다.

우선은 boardId 만 ID로 지정한 채 개발을 진행하였다.

 

 

Repository

@Repository
public interface ScoreRepository extends JpaRepository<Score, Long> {

    @Modifying
    @Query ( value = "INSERT INTO TB_BOARD_SCORE (USER_ID, BOARD_ID, SCORE, CRTE_DTTM, UPDT_DTTM)" +
                     "VALUES(:userId, :boardId, :score, :createDateTime, :updateDateTime)",
             nativeQuery = true)
    void registerScore(@Param(value = "userId") Long userId,
                       @Param(value = "boardId") Long boardId,
                       @Param(value = "score") int score,
                       @Param(value = "createDateTime") LocalDateTime createDateTime,
                       @Param(value = "updateDateTime") LocalDateTime updateDateTime);

    @Modifying
    @Query ( value = "UPDATE TB_BOARD_SCORE SET SCORE = :score, UPDT_DTTM = :updateDateTime WHERE USER_ID = :userId AND BOARD_ID = :boardId",
             nativeQuery = true)
    void updateScore(@Param(value = "userId") Long userId,
                     @Param(value = "boardId") Long boardId,
                     @Param(value = "score") int score,
                     @Param(value = "updateDateTime") LocalDateTime updateDateTime);

    @Modifying
    @Query ( value = "DELETE FROM TB_BOARD_SCORE WHERE USER_ID = :userId",
             nativeQuery = true)
    void deleteScoreByUser(@Param(value = "userId") Long userId);

    @Modifying
    @Query ( value = "DELETE FROM TB_BOARD_SCORE WHERE BOARD_ID = :boardId",
            nativeQuery = true)
    void deleteScoreByBoard(@Param(value = "boardId") Long boardId);

    @Query( value = "SELECT IFNULL(ROUND(SUM(SCORE) / COUNT(*), 2), 0) AS AVERAGE_SCORE FROM TB_BOARD_SCORE WHERE BOARD_ID = :boardId",
            nativeQuery = true )
    double getBoardAverageScore(@Param(value = "boardId") Long boardId);

}

Respository는 위와 같이 개발하였다.

간단히 설명하면, 점수를 추가하는 로직과 수정하는 로직이 있고, 삭제 로직은 게시글을 삭제하였을 때 지워지는 boardId를 대상으로 하는 Delete와 유저가 회원탈퇴를 하였을 때 지워지는 userId를 대상으로 하는 Delete문을 만들었다.

마지막으로 게시글을 조회할 때마다 평점을 계산하여 불러오는 로직을 만들었다.

( 참고로 1편에서 언급하였듯 그리고 제목에도 있듯, 해당 프로젝트는 JPA 네이티브 쿼리를 이용하는 것이기 때문에, 위 처럼 작성하였다. )

 

 

Service

기본적인 Insert와 Update를 위한 로직은 이전 글에서 작성한 User나 Board 관련 코드와 동일하기 때문에 생략하고자 한다. 대신 게시글 정보를 가져올 때 

double averageScore = scoreRepository.getBoardAverageScore(boardId);
boardVo.setAverageScore(averageScore);

위 로직을 추가하였다.

Board는 입력할 때는 DTO -> VO -> Entity 과정을 거치는데, 불러올 때는 Entity -> VO 순서를 거친다. 불러올 때는 아직 DTO를 만들 필요가 없다고 느꼈기 때문이다.

아무튼 VO에 averageScore 라는 필드를 추가하여, 거기에 Set을 시켜준다.

 

 

Controller

@PostMapping("/board/score/register")
public ResponseEntity<?> registerScore(@Valid ScoreDto scoreDto,
                                       HttpSession session) {

    if (SessionUtil.checkSession(session)) return SessionUtil.redirect(); // Session 끊킬 시 redirect
    return ResponseEntity.ok(scoreService.registerScore(scoreDto));
}

@PostMapping("/board/score/update")
public ResponseEntity<?> updateScore(@Valid ScoreDto scoreDto,
                                     HttpSession session) {

    if (SessionUtil.checkSession(session)) return SessionUtil.redirect(); // Session 끊킬 시 redirect
    return ResponseEntity.ok(scoreService.updateScore(scoreDto));
}

그래도 Controller는 포스팅해야겠다 싶어서 적어보았다.

 

 

테스트

@SpringBootTest
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
public class ScoreControllerTest {

    @Autowired
    private ScoreService scoreService;

    @Autowired
    private BoardService boardService;

    @Test
    @Transactional
    @Rollback
    @DisplayName("스코어 추가 로직 테스트")
    void registerScoreTest() {

        // given
        Long testBoardId = 3L;
        Long testUserId = 5L;
        int testScore = 5;

        ScoreDto scoreDto = new ScoreDto();
        scoreDto.setBoardId(testBoardId);
        scoreDto.setUserId(testUserId);
        scoreDto.setScore(testScore);

        // when
        scoreService.registerScore(scoreDto);

        // then
        BoardVo boardVo = new BoardVo();
        boardVo = boardService.getBoardInfo(testBoardId);
        assertEquals(boardVo.getAverageScore(), testScore);

    }

    @Test
    @DisplayName("스코어 확인 테스트")
    void getBoardScoreTest() {

        // then
        BoardVo boardVo = new BoardVo();
        boardVo = boardService.getBoardInfo(2L);
        System.out.println(boardVo.toString());

    }

}

테스트는 위 코드처럼 간단하게 진행하였다.

결과는 모두 통과였다.

 


결론

관련 테스트 코드들도 통과하였다.

아마 피드백을 받아보면 수정해야 할 부분이 많이 나올 것이다.

다 작성해보고 생각이 드는 것이지만, 저렇게 Select 할 때마다 쿼리문에서 평점을 계산해준다면.. 한 게시글에 1만명이 동시에 접근하였을 때 엄청 느려질 거 같다는 생각이 들었다.

이제 개발하면서 스스로 생각하는 힘이 점점 늘어난다는 느낌을 받았다.

 

개발을 진행하면서 블로그에 포스팅까지 하는 것이 생각보다 어려웠지만, 완벽하게는 못해도 이렇게 다시 둘러보았을 때 기억에 남을 정도로는 작성을 하는게 좋다고 생각이 들었다.

읽는 분들께는 부족할 수 있는 글이지만, 필자의 성장을 위한 회고록으로 그 발자취를 남긴다.

 

다음에도 추가 피드백을 받아 수정하게 되다면, 다음편을 작성하겠다.

 


해당 CRUD 개발 깃허브 링크 : 

https://github.com/JeonDaehong/JpaNativeQuery-Redis-CRUD-Project

 

GitHub - JeonDaehong/JpaNativeQuery-Redis-CRUD-Project

Contribute to JeonDaehong/JpaNativeQuery-Redis-CRUD-Project development by creating an account on GitHub.

github.com