
프로젝트 MVP 기능 구현이 마무리되고, 고도화 주차가 시작됐다. 프론트엔드 개발자 분들도 구해서 같이 프로젝트를 진행하려고 예정 중이기 때문에 API가 변경되는 추천/비추천 기능 구현을 우선적으로 수행하게 됐다.
MVP 구현 당시에는 추천 및 추천 취소 기능만 구현했었는데, 이제 이를 추천/비추천 기능으로 구현하려고 한다.
메서드와 클래스가 잘 분리되어 있었기 때문에 코드를 크게 변경하지 않고 기능을 구현할 수 있었다.
VoteTypepublic enum VoteType {
UP,
DOWN,
NONE
}
VoteResultpublic record VoteResult(
VoteType voteType,
Long upvoteCount,
Long downvoteCount
) {
}
BaseVoteServicetoggleVote 메서드 이름을 manageVote로 변경afterVote(...) 메서드를 호출하여 알림 발행 등의 처리 수행@RequiredArgsConstructor
public abstract class BaseVoteService<T extends BaseVote, D extends BaseVoteDomainService<T, ?>> {
protected final D voteDomainService;
private final UserDomainService userDomainService;
protected VoteResponse manageVote(Long voterId, Long targetId, VoteType voteType) {
User voter = userDomainService.getUserById(voterId);
VoteResult voteResult = voteDomainService.manageVote(voter, targetId, voteType);
if (voteResult.voteType() == VoteType.UP) {
afterVote(voter, targetId);
}
return VoteResponse.from(voteResult);
}
@Transactional(readOnly = true)
public VoteResponse getVoteStatus(Long userId, Long targetId) {
VoteResult voteResult = voteDomainService.getVoteStatus(userId, targetId);
return VoteResponse.from(voteResult);
}
protected abstract void afterVote(User voter, Long targetId);
}
BaseVoteDomainService@RequiredArgsConstructor
public abstract class BaseVoteDomainService<T extends BaseVote, R extends BaseVoteRepository<T>> {
protected final R voteRepository;
public VoteResult manageVote(User voter, Long targetId, VoteType voteType) {
Optional<T> existing = voteRepository.findByVoterIdAndTargetId(voter.getId(), targetId);
if (existing.isPresent()) {
// 추천 또는 비추천 기록이 존재
T vote = existing.get();
// 요청 타입이 NONE 이면 기존 기록 삭제
if (voteType == VoteType.NONE) {
voteRepository.delete(vote);
} else {
// 아닐 경우 타입 업데이트
voteRepository.update(vote, voteType);
}
} else {
// 기록이 없으면 새로 생성
T vote = buildVote(voter, targetId, voteType);
voteRepository.save(vote);
}
Long upvoteCount = voteRepository.countUpvotesByTargetId(targetId);
Long downvoteCount = voteRepository.countDownvotesByTargetId(targetId);
return new VoteResult(voteType, upvoteCount, downvoteCount);
}
public VoteResult getVoteStatus(Long voterId, Long targetId) {
Optional<T> existing = voteRepository.findByVoterIdAndTargetId(voterId, targetId);
VoteType voteType = existing.isPresent() ? existing.get().getVoteType() : VoteType.NONE;
Long upvoteCount = voteRepository.countUpvotesByTargetId(targetId);
Long downvoteCount = voteRepository.countDownvotesByTargetId(targetId);
return new VoteResult(voteType, upvoteCount, downvoteCount);
}
protected abstract T buildVote(User voter, Long targetId, VoteType voteType);
}
public record VoteResult(
VoteType voteType,
// 이전 추천 상태. 알림 도배 방지 검증용
VoteType prevVoteType,
Long upvoteCount,
Long downvoteCount
) {
}
afterVote 메서드 호출 시 해당 필드를 검사하도록 함protected VoteResponse manageVote(Long voterId, Long targetId, VoteType voteType) {
User voter = userDomainService.getUserById(voterId);
VoteResult voteResult = voteDomainService.manageVote(voter, targetId, voteType);
// 알림 도배 방지용 검증 로직
// voteType=UP인 요청을 반복해서 날릴 경우 알림이 도배될 수 있는 문제를 방지
if (voteResult.voteType() == VoteType.UP && voteResult.prevVoteType() != VoteType.UP) {
afterVote(voter, targetId);
}
return VoteResponse.from(voteResult);
}
vote_type=NONE인 값이 생성됐다 삭제됐다를 반복함BaseVoteDomainService 다음과 같이 수정해서 해결public VoteResult manageVote(User voter, Long targetId, VoteType voteType) {
Optional<T> existing = voteRepository.findByVoterIdAndTargetId(voter.getId(), targetId);
VoteType prevVoteType = VoteType.NONE;
if (voteType == VoteType.NONE) {
existing.ifPresent(voteRepository::delete);
} else {
if (existing.isPresent()) {
T vote = existing.get();
prevVoteType = vote.getVoteType();
voteRepository.update(vote, voteType);
} else {
T vote = buildVote(voter, targetId, voteType);
voteRepository.save(vote);
}
}
Long upvoteCount = voteRepository.countUpvotesByTargetId(targetId);
Long downvoteCount = voteRepository.countDownvotesByTargetId(targetId);
return new VoteResult(voteType, prevVoteType, upvoteCount, downvoteCount);
}