TFTgogo

Java / Spring Boot / MySQL / JWT / SSE

커밋 기록에는 이전 GitHub 닉네임 beancan0325로 보일 수 있습니다.

1. 프로젝트 소개와 주요 기능 — TFT 유저가 파티를 모집하고 채팅하는 게임 커뮤니티

팀 프로젝트 | 2026.05 ~ 2026.06

TFTgogo는 롤토체스(TFT) 유저가 전적과 게임 정보를 확인하고, 같이 플레이할 사람을 모집한 뒤, 참여자와 채팅할 수 있는 커뮤니티 서비스입니다. 사용자는 단순히 “참여하기” 버튼을 누르지만, 서비스는 로그인 여부, 파티 정원, 이미 참여한 파티, 닫힌 모집글, 채팅 권한을 동시에 확인해야 합니다. 저는 이 상태들이 화면과 서버에서 어긋나지 않도록 파티 참여와 실시간 채팅 흐름을 정리했습니다.

담당 범위

파티 참여 가드, 채팅 권한 경계, 인증 상태 UI 규칙, SSE 복구 흐름 증거를 정리했습니다.

판단

UI가 오래된 상태이거나 우회되더라도 백엔드 서비스 규칙이 잘못된 참여와 쓰기를 막아야 한다고 보았습니다.

검증

stale 인증 상태, 중복 참여, 채팅 쓰기 권한 경계는 회귀 테스트와 코드 증거로 확인했습니다.

한계

파티 정원 동시성에 대한 전체 멀티스레드 부하 테스트는 아직 완료하지 못해 남은 작업으로 표시합니다.

기술 사례 1. 캐시 무효화가 빠지는 세 실행 경로

위험한 상태
덱 집계가 DB에 반영됐는데도 이전 메타덱 순위가 캐시에 남아 사용자에게 오래된 결과가 보이는 상태
원인
트랜잭션 커밋 전 무효화, 예외 종료 시 @CacheEvict 생략, 비동기 람다의 Spring 프록시 우회가 각각 다른 누락 경로를 만들었습니다.
결정
TransactionAwareCacheManagerProxy로 커밋 이후 제거를 보장하고, 예외·비동기 경로는 finally에서 명시적으로 정리했습니다. AtomicBoolean.compareAndSet으로 중복 집계도 거절했습니다.
실제 코드
global/config/CacheConfig.java, domain/deck/service/impl/MetaDeckServiceImpl.java, domain/deck/service/impl/AdminDeckServiceImpl.java
검증·한계
MetaDeckServiceImplTest.java로 중복 집계와 플래그 복구를 확인했습니다. Caffeine은 단일 인스턴스 로컬 캐시이므로 다중 서버에서는 분산 무효화가 추가로 필요합니다.

기술 사례 2. 마지막 한 자리에 두 요청이 동시에 참여하는 경우

위험한 상태
두 요청이 동시에 정원을 읽으면 둘 다 여유가 있다고 판단해 정원을 초과하거나 같은 사용자가 중복 참여하는 상태
원인
단순한 read-check-update는 검사와 저장 사이에 다른 트랜잭션이 끼어드는 경쟁 조건을 막지 못합니다.
결정
하나의 트랜잭션에서 member → party 순서로 PESSIMISTIC_WRITE 잠금을 잡고, 3초 타임아웃으로 무한 대기를 제한했습니다.
실제 코드
domain/community/service/impl/CommunityPartyServiceImpl.java, domain/community/repository/PartyPostRepository.java, domain/member/repository/MemberRepository.java
검증·한계
CommunityPartyServiceImplTest.java에서 정원 초과·중복 참여·잠금 순서를 검증했습니다. 실제 멀티스레드 부하 테스트는 아직 수행하지 않았습니다.

기술 사례 3. 이메일이 같아도 다른 소셜 계정인 경우

위험한 상태
이메일만으로 소셜 계정을 연결하면 서로 다른 제공자의 계정 소유권이 잘못 합쳐질 수 있습니다.
결정
provider + socialId를 소유권 식별자로 사용하고 DB 복합 unique constraint를 추가했습니다. 동시 최초 가입 충돌 시 기존 계정을 다시 조회해 한 결과로 수렴시켰습니다.
실제 코드
domain/member/service/impl/MemberServiceImpl.java, domain/member/repository/MemberRepository.java, db/migration/V1__init_schema.sql
검증
MemberServiceImplTest.javaSocialMemberCreationServiceTest.java에서 provider 식별과 unique 충돌 후 재조회 경로를 확인했습니다.
결론
편리한 이메일 병합보다 계정 소유권을 먼저 지키는 방향으로 인증 모델을 설계했습니다.

stale 인증 상태

로그아웃 후 남은 참여 상태가 UI를 오염시키지 않도록 현재 인증 상태를 먼저 판단합니다.

InputisJoined=true
from stale cache
Auth checkisAuthenticated
checked first
Guardunauthenticated
button disabled
Outputlogin required
no stale action
문제이전 참여 상태가 남아 비로그인 사용자에게 참여중 액션이 보일 수 있음
판단참여 상태보다 현재 인증 상태를 먼저 검사
결과비로그인 상태에서는 stale 참여값이 있어도 로그인 필요 상태로 고정

중복 참여 방지

중복 참여는 버튼 문제가 아니라 저장 전 서버 도메인 정합성 문제로 차단합니다.

Requestjoin/create
party action
Lockmember row
for mutation
Checkowned post or
accepted party
Stopthrow before
repository.save
문제동시 요청이나 다른 클라이언트 요청은 프론트 버튼만으로 막기 어려움
판단회원 row 잠금과 활성 작성글/참여 신청 확인을 저장 전에 수행
결과중복 참여 경로에서는 save가 호출되지 않음

채팅 권한 경계

커뮤니티 공개성은 유지하되, 메시지 작성 책임은 인증 사용자에게만 둡니다.

GETmessages/stream
public
POSTsend message
principal required
401clear token
unauthorized
403client error
keep token
문제읽기까지 잠그면 공개성이 떨어지고, 쓰기까지 열면 사용자 식별이 무너짐
판단GET/SSE 읽기는 공개, POST 쓰기는 인증 사용자로 분리
결과조회는 열어두고 작성 책임은 인증 정보로 남김

SSE 연결 생명주기

실시간 연결 실패를 하나의 에러가 아니라 재연결, 복구 실패, 인증 만료 상태로 나눕니다.

OpenSSE stream
connected
Mergesnapshot/message
query cache
Retry1s / 2.5s / 5s
max 3
Stopexpired auth or
failed recovery
문제연결 끊김, 재시도, 인증 만료, 복구 실패가 모두 같은 오류처럼 보일 수 있음
판단재연결 가능 여부와 delay를 별도 정책으로 분리
결과복구 가능한 실패와 종료해야 하는 실패를 UI 상태로 구분

2. 설계

인증 먼저, 참여 상태는 그다음

로그아웃 뒤 이전 참여 상태가 남아도 참여중 버튼이 보이지 않도록 현재 인증 상태를 먼저 판단했습니다.

서비스 레이어 중복 참여 방지

버튼 비활성화만 믿지 않고 회원 row 잠금, 활성 작성글 확인, 활성 참여 신청 확인을 저장 전에 수행했습니다.

읽기/쓰기 권한 분리

채팅 조회와 SSE 스트림은 공개로 두고, 메시지 전송은 인증 사용자 기준으로 처리했습니다.

실시간 복구 규칙

SSE snapshot/message는 Query Cache에 병합하고 재연결 제한과 인증 만료는 명확히 분리했습니다.

3. 프로젝트별 문제-해결

Auth State

로그아웃 뒤 이전 참여 상태가 남아 잘못된 버튼이 보이는 문제

문제

로그아웃 뒤 stale isJoined 값이 남아 비로그인 사용자에게 참여중 액션이 보일 수 있었습니다.

원인

프론트 캐시의 참여 상태와 현재 인증 상태가 서로 다른 시점에 갱신될 수 있었습니다.

해결

참여 상태보다 isAuthenticated를 먼저 검사하고, 비로그인 조합을 회귀 테스트로 고정했습니다.

결과

stale 참여값이 남아도 비로그인 상태에서는 로그인 필요 상태로 고정되어 잘못된 액션이 노출되지 않습니다.

Party Domain Rule

동시 요청이나 우회 요청으로 중복 참여가 저장될 수 있는 문제

문제

프론트 버튼을 비활성화해도 다른 탭이나 직접 요청으로 같은 사용자가 여러 파티에 참여할 수 있습니다.

원인

참여 가능 여부를 화면에서만 막으면 서버 저장 직전의 도메인 정합성을 보장할 수 없습니다.

해결

회원 row를 기준으로 활성 작성글과 기존 참여 신청을 저장 전에 확인하고, 중복이면 save 이전에 예외로 중단했습니다.

결과

중복 참여 경로에서는 repository save가 호출되지 않는 것을 테스트로 확인할 수 있게 됐습니다.

Realtime Chat

채팅 읽기와 쓰기 권한이 섞여 사용자 책임이 흐려지는 문제

문제

채팅을 모두 잠그면 공개성이 떨어지고, 모두 열면 메시지 작성자의 책임을 남기기 어렵습니다.

원인

GET/SSE 읽기와 POST 쓰기를 같은 인증 기준으로 처리하면 서비스 사용성과 권한 경계가 충돌합니다.

해결

메시지 조회와 SSE 스트림은 공개로 두고, 메시지 전송은 AuthenticationPrincipal이 있는 사용자만 가능하게 분리했습니다.

결과

사용자는 채팅을 볼 수 있고, 작성 행위는 인증 사용자에게 연결되어 기록 책임이 남습니다.

4. 주요 기능과 코드 증거

TFTgogo 인증 우선 버튼 상태 코드
인증 우선 버튼 가드

isAuthenticated를 isJoined보다 먼저 검사해 비로그인 사용자의 참여중 표시를 막습니다.

TFTgogo 중복 참여 저장 전 가드 코드
저장 전 중복 참여 차단

다른 활성 참여가 있으면 저장 전에 예외로 중단합니다.

TFTgogo 중복 참여 save 미호출 테스트
save 미호출 회귀 테스트

중복 참여 상황에서는 저장 로직이 호출되지 않음을 테스트로 확인했습니다.

TFTgogo 채팅 읽기 공개 SecurityConfig 코드
채팅 읽기 공개 설정

채팅 메시지 조회와 SSE 스트림 GET 요청은 공개로 둡니다.

TFTgogo 인증 사용자 메시지 전송 컨트롤러 코드
인증 사용자 쓰기

메시지 전송은 AuthenticationPrincipal을 기준으로 사용자 책임을 남깁니다.

TFTgogo SSE 재연결 정책 코드
SSE 재연결 정책

재연결 가능 여부와 delay를 별도 함수로 분리해 테스트 가능하게 했습니다.

5. 협업 프로세스

Feature Branch

기능 단위 브랜치로 변경 범위를 나누고 PR에서 작업 이유와 코드를 함께 검토했습니다.

Pull Request 리뷰

인증/인가, DTO 응답, Service 검증, 테스트 관점을 PR 단위로 확인했습니다.

Issue 관리

커뮤니티, 파티, 채팅, SSE처럼 기능 단위로 이슈를 나누고 담당 범위를 추적했습니다.

라벨과 릴리즈 증거

bug, feature, frontend, backend, priority 라벨과 릴리즈 노트를 협업 증거로 남겼습니다.

6. GitHub 협업 증거