[ezcode] 추천/비추천 기능 구현

NCOOKIE·2025년 6월 25일
1

ezcode

목록 보기
5/8

프로젝트 MVP 기능 구현이 마무리되고, 고도화 주차가 시작됐다. 프론트엔드 개발자 분들도 구해서 같이 프로젝트를 진행하려고 예정 중이기 때문에 API가 변경되는 추천/비추천 기능 구현을 우선적으로 수행하게 됐다.

MVP 구현 당시에는 추천 및 추천 취소 기능만 구현했었는데, 이제 이를 추천/비추천 기능으로 구현하려고 한다.

구현

메서드와 클래스가 잘 분리되어 있었기 때문에 코드를 크게 변경하지 않고 기능을 구현할 수 있었다.

  • VoteType
    • UP, DOWN, NONE 필드가 있음
    • NONE 이면 기존의 추천 기록 삭제
public enum VoteType {
	UP,
	DOWN,
	NONE
}
  • VoteResult
public record VoteResult(

	VoteType voteType,

	Long upvoteCount,

	Long downvoteCount

) {
}
  • BaseVoteService
    • 기존의 toggleVote 메서드 이름을 manageVote로 변경
    • 추천(UP)이 발생하면 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);
}

버그 해결

알림 중복 발생

  • VoteType=UP인 추천 요청을 반복해서 날리면 이전 상태와 상관 없이 그 때마다 알림이 발생함
  • 프론트엔드와 함께 사용하는 일반적인 경우에는 이 버그가 발생하지 않겠지만… 방지하는 것이 좋아보임
  • VoteResult에 이전 추천 상태 필드 추가
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);
}

DB에 잘못된 값 저장됨

  • VoteType=NONE 요청을 계속해서 날리게 되면 로직 버그 때문에 vote_type=NONE인 값이 생성됐다 삭제됐다를 반복함
    • 기존 추천 데이터가 있는지 먼저 검사하기 때문에 else 분기로 들어감
  • 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);
}
profile
일단 해보자

0개의 댓글