1. 프로젝트 소개와 주요 기능 — TFT 유저가 파티를 모집하고 채팅하는 게임 커뮤니티
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.java와SocialMemberCreationServiceTest.java에서 provider 식별과 unique 충돌 후 재조회 경로를 확인했습니다.- 결론
- 편리한 이메일 병합보다 계정 소유권을 먼저 지키는 방향으로 인증 모델을 설계했습니다.
stale 인증 상태
로그아웃 후 남은 참여 상태가 UI를 오염시키지 않도록 현재 인증 상태를 먼저 판단합니다.
from stale cache
checked first
button disabled
no stale action
| 문제 | 이전 참여 상태가 남아 비로그인 사용자에게 참여중 액션이 보일 수 있음 |
|---|---|
| 판단 | 참여 상태보다 현재 인증 상태를 먼저 검사 |
| 결과 | 비로그인 상태에서는 stale 참여값이 있어도 로그인 필요 상태로 고정 |
중복 참여 방지
중복 참여는 버튼 문제가 아니라 저장 전 서버 도메인 정합성 문제로 차단합니다.
party action
for mutation
accepted party
repository.save
| 문제 | 동시 요청이나 다른 클라이언트 요청은 프론트 버튼만으로 막기 어려움 |
|---|---|
| 판단 | 회원 row 잠금과 활성 작성글/참여 신청 확인을 저장 전에 수행 |
| 결과 | 중복 참여 경로에서는 save가 호출되지 않음 |
채팅 권한 경계
커뮤니티 공개성은 유지하되, 메시지 작성 책임은 인증 사용자에게만 둡니다.
public
principal required
unauthorized
keep token
| 문제 | 읽기까지 잠그면 공개성이 떨어지고, 쓰기까지 열면 사용자 식별이 무너짐 |
|---|---|
| 판단 | GET/SSE 읽기는 공개, POST 쓰기는 인증 사용자로 분리 |
| 결과 | 조회는 열어두고 작성 책임은 인증 정보로 남김 |
SSE 연결 생명주기
실시간 연결 실패를 하나의 에러가 아니라 재연결, 복구 실패, 인증 만료 상태로 나눕니다.
connected
query cache
max 3
failed recovery
| 문제 | 연결 끊김, 재시도, 인증 만료, 복구 실패가 모두 같은 오류처럼 보일 수 있음 |
|---|---|
| 판단 | 재연결 가능 여부와 delay를 별도 정책으로 분리 |
| 결과 | 복구 가능한 실패와 종료해야 하는 실패를 UI 상태로 구분 |