[프로젝트] 실시간 데이터 처리 회고

2025. 12. 8. 12:57·Spring/Spring 실습

❤️‍🔥 과제 목표

  • 실시간 데이터 처리를 위한 아키텍처 설계 흐름을 복습
  • Kafka + WebSockect(STOMP) 기반의 실시간 메시징 구조를 직접 구현해본다
  • DB 설계를 통해 메시지/이벤트 기반 데이터 모델링 능력을 강화한다.

☠️ 트러블 슈팅

  • 발생한 Issue 
    • 1. 이전 과제 및 프로젝트 진행 간 @Transactional을 어디에, 무엇을 써야 하는가?
    • 2. 엔티티가 아닌 DTO 중심으로 실시간 메시지/응답을 어떻게 설계하면 좋은가?
    • 3. 투표 수 집계 과정을 구현하면서 기존 엔티티에 별도로 컬럼값으로 매칭을 시켜주지 않아도 구현이 가능한가?
  • 해결방법 / 찾아본 내용
    • 1. 이전에 정리했던 Transactional의 개념처럼 DB의 수정이 일어나는 구간에서의 안정적인 변경과정을 위한 @Transactional 사용이 필요했다. 
      → 실제 사용한 메서드는 DB 변경이 이루어지는 투표 생성, 투표 종료 메서드에 해당 어노테이션을 사용하였다. 
	// 투표하기
	@Transactional // DB 변경을 안전하게 처리하기 위한 어노테이션
	public VoteRecordResponseDto vote(Long voteId, VoteRequestDto requestDto) {

		Vote vote = voteRepository.findById(voteId)
			.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 투표입니다."));

		if (vote.getVoteStatus() == VoteStatus.CLOSED) {
			throw new IllegalStateException("이미 종료된 투표입니다.");
		}

		User user = userRepository.findByLoginId(requestDto.getVoterId())
			.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다."));

		Candidate candidate = candidateRepository.findById(requestDto.getCandidateId())
			.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 후보입니다."));

		if(!candidate.getVote().getId().equals(voteId)) {
			throw new IllegalArgumentException("해당 투표의 후보가 아닙니다.");
		}

		VoteRecord voteRecord = voteRecordRepository
			.findByCandidate_Vote_IdAndUser_LoginId(voteId, requestDto.getVoterId())
			.orElseGet(() -> {
				VoteRecord newRecord = new VoteRecord();
				newRecord.setUser(user);
				newRecord.setCreatedAt(LocalDateTime.now());
				return newRecord;
			});

		voteRecord.setCandidate(candidate);
		VoteRecord saved = voteRecordRepository.save(voteRecord);

		//알림 발송 부분 추가
		VoteNotificationDto notification = VoteNotificationDto.builder()
			.voteId(vote.getId())
			.title(vote.getTitle())
			.message("누군가 [" + vote.getTitle() + "] 투표에 투표를 진행했습니다!")
			.build();

		messagingTemplate.convertAndSend("/topic/announcements", notification);

		return VoteRecordResponseDto.from(saved);
	}

	//투표 종료
	@Transactional
	public VoteCloseResponseDto closeVote(Long voteId, String authorLoginId) {

		// 투표 조회
		Vote vote = voteRepository.findById(voteId)
			.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 투표입니다."));

		// 투표 생성자 여부 검증
		String ownerLoginId = vote.getUser().getLoginId();
		if (!ownerLoginId.equals(authorLoginId)) {
			throw new IllegalStateException("투표를 생성한 사용자만 종료할 수 있습니다.");
		}

		// 투표 상태가 이미 CLOSED인지 체크
		if (vote.getVoteStatus() == VoteStatus.CLOSED) {
			throw new IllegalStateException("이미 종료된 투표입니다.");
		}

		// 투표 상태 변경
		vote.close();

		// 응답 DTO 반환
		return VoteCloseResponseDto.from(vote);
	}
  • 2. 엔티티를 바로 보내게 되면 지나치게 많은 정보들이 노출될 가능성이 있고, 양방향 관계에서는 무한루프에 빠지게 될 수 있다.
    엔티티는 내부에서 사용할 구조이고, dto는 외부에서 사용할 구조라고 정리하는 것이 필요하다.
    → 실시간 데이터 처리하는 과정에서는 그 종류가 늘어날 가능성이 높기 때문에 요구하는 응답문의 양식에 맞춰 필요한 요소들만 dto에 담아 처리하도록 진행했다.
public class VoteDetailResponseDto {
    // 투표 기본 정보
    private Long voteId;
    private String title;
    private VoteStatus status;

    // 후보 & 득표 정보
    private List<CandidateResultDto> candidates; // (id, name, voteCount)

    // 전체 투표 수
    private long totalVotes;
}

 

  • 3. 서비스 레이어에서 계산하도록 처리하여 후보별 득표 수를 countMap으로 계산하도록 처리했다. 이후 CandidateDtos로 감싸서 전달하였다.
	// 투표 상세 조회
	public VoteDetailResponseDto getVoteDetail(Long voteId) {

		// 투표 Entity 조회
		Vote vote = voteRepository.findById(voteId)
			.orElseThrow(() -> new IllegalArgumentException("해당 투표가 없습니다. id = " + voteId));

		// 후보 목록 조회
		List<Candidate> candidates = candidateRepository.findByVote_Id(voteId);

		// 해당 후보에 대한 모든 투표 기록 조회
		List<VoteRecord> records = voteRecordRepository.findByCandidate_Vote_Id(voteId);

		// 후보 별 득표 수 집계
		Map<Long, Long> countMap = records.stream()
			.collect(Collectors.groupingBy(
				r-> r.getCandidate().getId(),
				Collectors.counting()
			));

		// 후보 DTO 변환
		List<CandidateResultDto> candidateDtos = candidates.stream()
			.map(c -> new CandidateResultDto(
				c.getId(),
				c.getName(),
				countMap.getOrDefault(c.getId(), 0L)
			))
			.toList();

		// totalVotes = 전체 투표 기록 수
		long totalVotes = records.size();

		return new VoteDetailResponseDto(vote, candidateDtos, totalVotes);
	}
  • stream().collect(Collectors.groupingBy(...)) 를 통해 후보의 id값을 확인하고, 그 값에 해당하는 VoteRecord의 개수를 세도록 한다.
    • 이후 후보 Dto를 통해 후보의 번호를 조회, 키 값이 있는 경우는 그 값을 그대로 반환하고, 없는 경우는 0표로 보여주기 위한 과정을 구현했다.

'Spring > Spring 실습' 카테고리의 다른 글

[과제 회고] K사 서버 개발하기  (0) 2026.01.14
[프로젝트 회고] 실시간 채팅 프로젝트  (0) 2025.12.31
[프로젝트] 포인트 기반 결제 시스템 프로젝트 회고  (0) 2025.11.28
[Spring 실습] 이커머스 백오피스 프로젝트  (0) 2025.11.03
[Spring 실습] 점심 추천 & 투표 서비스 만들기 V2  (0) 2025.10.29
'Spring/Spring 실습' 카테고리의 다른 글
  • [과제 회고] K사 서버 개발하기
  • [프로젝트 회고] 실시간 채팅 프로젝트
  • [프로젝트] 포인트 기반 결제 시스템 프로젝트 회고
  • [Spring 실습] 이커머스 백오피스 프로젝트
stark77
stark77
하마의 IT 자기개발 이모저모, 백엔드 개발자로 거듭나기
  • stark77
    하마의 개발자 성장일기
    stark77
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 컴퓨터구조와 운영체제
        • 컴퓨터구조
        • 운영체제
      • SQL 기초
      • Spring
        • 백엔드 기초
        • Spring 실습
      • JAVA
        • Java 실습
      • HTML&CSS
        • HTML&CSS 실습
      • Git&GitHub
        • Git&GitHub 실습
      • 내배캠 끄적끄적
        • Today I Learned
      • 유용한 툴 및 사이트 정리
      • 취미
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    algorithm
    JPA
    Til
    객체지향
    프로세스와 쓰레드
    RestTemplate
    Stomp
    git
    백엔드 기초다지기
    String.format
    경합조건과 교착상태
    SpringSecurity
    for문
    thymleaf
    Github
    실시간 데이터 처리
    HTML&CSS
    Java 문법기초
    MVC
    다형성
    웹소켓
    java
    Spriingboot
    네트워크 기초
    WebSocket
    jsp
    백엔드 기초
    Spring
    BEAN
    객체지향프로그래밍
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
stark77
[프로젝트] 실시간 데이터 처리 회고
상단으로

티스토리툴바