해당 포스팅은 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
'개발 회고록' 카테고리의 다른 글
[ 개발 회고록 ] Thread 와 Thread의 데이터 공유 (1) | 2024.01.02 |
---|---|
[ 개발 회고록 ] 비동기 메서드 동시성 문제 발견 및 수정 (0) | 2023.12.04 |
[ 개발 회고록 ] 병렬 프로그래밍과 비동기 구현 (0) | 2023.11.29 |
[ 개발 회고록 ] 배타적 잠금 문제와 Validation (0) | 2023.11.22 |
[ 개발 회고록 ] JPA nativeQuery와 SpringBoot를 이용한 CRUD 개발 (0) | 2023.11.21 |