추천받은 restapi를 공유한 게시글의 좋아요를 누르도록 설계를 진행했다.
PostLike의 Entity
public class PostLike extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private Post post;
좋아요 서비스
// 좋아요 누르기
@Override
@Transactional
public void likePost(Long postId) {
Member currentMember = getCurrentMember();
Post post = postRepository.findByIdWriteLock(postId)
.orElseThrow(() -> new PostException(PostExceptionInfo.NOT_FOUND_POST, postId + "번 게시글을 찾지 못했습니다."));
if (post.getMember().equals(currentMember)) {
throw new PostLikeException(PostLikeExceptionInfo.DO_NOT_SELF_LIKE, currentMember.getId() + "번 유저가 본인의 게시글 좋아요를 눌렀습니다.");
}
if (postLikeRepository.existsByMemberAndPost(currentMember, post)) {
throw new PostLikeException(PostLikeExceptionInfo.ALREADY_LIKE_POST, "이미 " + postId + "번 게시글 좋아요를 눌렀습니다.");
}
PostLike postLike = PostLike.builder()
.post(post)
.member(currentMember).build();
postLikeRepository.save(postLike);
post.incrementLikeCount();
}
유저, 게시글과 일치하는 좋아요가 없다면 새로 생성해서 save를 진행하고 post의 좋아요 수를 1개 늘린다.
테스트코드를 작성하기는 내 실력이 부족해서 jmeter로 동시성 테스트를 진행.
TEST
10개의 동일 유저 스레드가 1번 게시글에 1000번 동시 좋아요 요청 전송(유저 id는 같다고 가정, 다양한 디바이스에서 좋아요 한다고 가정)
예상 -> 1개의 좋아요 데이터가 생성
하나만 생겨야 하는데 10개의 연속된 중복 데이터 저장 문제 발생.
애초에 Post를 조회할 때 비관적 쓰기락을 걸어두었기 때문에 문제가 없다고 판단했는데 초기 스레드 요청마다 데이터를 추가시키는 문제가 발생했다.
해결방법
post의 비관적 쓰기락과 더불어 PostLike의 테이블 자체에 고유 제약조건을 주는 방법을 선택했다.
PostLike는 Member, Post 2개의 값을 가지는데 이 두 개의 묶음이 중복되면 예외를 발생시키는 방법을 선택.
@Table 어노테이션을 통해 member_id, post_id를 하나로 묶어서 고유하게 처리해 주었다.
@Table(uniqueConstraints = {
@UniqueConstraint(columnNames = {"member_Id", "post_id"})
})
public class PostLike extends BaseTimeEntity {
만약에 고유한 값이 동일하게 db에 삽입된다면? DataIntegrityViolationException 예외를 발생시키게 된다.
그래서 적절한 예외처리를 진행해 주면 해당 추천은 안전하게 동작할 수 있게 된다.
// 좋아요 누르기
@Override
@Transactional
public void likePost(Long postId) {
Member currentMember = getCurrentMember();
Post post = postRepository.findByIdWriteLock(postId)
.orElseThrow(() -> new PostException(PostExceptionInfo.NOT_FOUND_POST, postId + "번 게시글을 찾지 못했습니다."));
if (post.getMember().equals(currentMember)) {
throw new PostLikeException(PostLikeExceptionInfo.DO_NOT_SELF_LIKE, currentMember.getId() + "번 유저가 본인의 게시글 좋아요를 눌렀습니다.");
}
if (postLikeRepository.existsByMemberAndPost(currentMember, post)) {
throw new PostLikeException(PostLikeExceptionInfo.ALREADY_LIKE_POST, "이미 " + postId + "번 게시글 좋아요를 눌렀습니다.");
}
try {
PostLike postLike = PostLike.builder()
.post(post)
.member(currentMember).build();
postLikeRepository.save(postLike);
post.incrementLikeCount();
} catch (DataIntegrityViolationException e) {
throw new PostLikeException(PostLikeExceptionInfo.ALREADY_LIKE_POST, "이미 " + postId + "번 게시글 좋아요를 눌렀습니다.");
}
}
예외도 잘 잡히고 안전하게 처리되는 것을 확인할 수 있었다.
반응형
'프로젝트 > RESTAPI 추천 서비스' 카테고리의 다른 글
프로젝트 코드리뷰 (2) (0) | 2024.06.07 |
---|---|
OFFSET 페이지네이션 쿼리 시간 단축과정 (1) | 2024.06.06 |
프로젝트 코드리뷰 (1) (0) | 2024.06.03 |
N+1 해결 과정에서 마주친 고민 (Feat, 추가 쿼리 or 페치조인) (0) | 2024.05.31 |