티스토리 뷰
안녕하세요! 열심히 서비스를 만들었지만,, 트러블슈팅도 많이 해봤지만,, 노션에만 정리하고 정작 블로그에 포스팅한 트러블 슈팅이 없다는 것을 깨닫고 하나씩 작성해보려합니다.
처음엔 단순한 매칭 실패처럼 보였는데, 깊이 파고들다 보니 null 처리 누락, DB 접근 예외, 컬렉션 수정 중 예외, 구조 분리 등
제법 복잡하고 중요한 문제들이 얽혀 있더라고요.
이번 글에서는 그 과정을 어떻게 추적하고, 해결했는지를 공유해보려 합니다.
문제 상황
매칭 요청을 보냈는데, 클라이언트에서는 응답이 오지 않고, 소켓 서버(3000번 포트)에서도 matching-started가 emit되지 않는 현상이 발생했습니다.
딱 봐도 어딘가 중간에 끊긴 것 같았죠.
그래서 API 서버 로그와 Docker 로그를 확인해봤습니다.
org.springframework.dao.InvalidDataAccessApiUsageException: The given id must not be null!
nested exception is java.lang.IllegalArgumentException: The given id must not be null!
딱 보니 findById(null)이 호출된 상황이더라고요.
JPA에서 ID가 null이면 IllegalArgumentException을 바로 던지기 때문에,
이게 전체 흐름을 막아버린 겁니다.
원인 1 - gameStyleIdList 내부에 null이 포함되어 있었음
매칭 요청에서 사용자의 게임 스타일 ID 리스트는 아래와 같이 전달됩니다.
{
"gameStyleIdList": [1, null, null]
}
이 상태로 서버에 넘어오면, 다음 로직에서 문제가 터집니다:
gameStyleRepository.findById(null); // 여기서 예외 발생
Spring Data JPA는 findById()에 null을 넘기면
런타임에서 IllegalArgumentException을 발생시키고,
이 예외가 처리되지 않으면 전체 요청이 실패하게 됩니다.
해결 1 - null 필터링 처리
프론트엔드에서 gameStyleIdList를 구성할 때,
null 값은 미리 걸러주도록 필터링을 추가했습니다.
request.gameStyleIdList = request.gameStyleIdList.filter(item => item !== null);
이걸로 프론트 → 백으로 넘어오는 데이터 정합성이 어느 정도 확보됐고,
IllegalArgumentException은 더 이상 발생하지 않았습니다.
또 다른 문제
GameStyle 수정 API를 호출했더니 이번엔 서버에서 다음과 같은 예외가 터졌습니다:
java.util.ConcurrentModificationException
원인 2 - Stream 반복 중 컬렉션 수정
기존에는 기존 게임 스타일을 지우기 위해 다음과 같은 코드를 쓰고 있었습니다.
member.getMemberGameStyleList().stream()
.filter(mgs -> !requestGameStyleList.contains(mgs.getGameStyle()))
.forEach(mgs -> {
mgs.removeMember(member); // 양방향 연관관계 제거
memberGameStyleRepository.delete(mgs);
});
여기서 문제는 Stream으로 순회하면서 동시에 컬렉션을 수정하고 있다는 점입니다.
Java는 이런 상황에서 fail-fast 전략을 적용하고 있어서
ConcurrentModificationException이 발생하게 됩니다.
해결 2 - 제거 대상을 임시 리스트에 모아 한 번에 처리
삭제할 대상들을 먼저 toRemove 리스트에 모아두고, 그 리스트만 순회하면서 제거하는 방식으로 변경했습니다.
List<MemberGameStyle> toRemove = new ArrayList<>();
for (MemberGameStyle mgs : member.getMemberGameStyleList()) {
if (!requestGameStyleList.contains(mgs.getGameStyle())) {
toRemove.add(mgs);
}
}
for (MemberGameStyle mgs : toRemove) {
mgs.removeMember(member);
memberGameStyleRepository.delete(mgs);
}
이제 컬렉션을 안전하게 수정할 수 있게 됐고, 예외도 발생하지 않게 되었습니다.
위의 수정 과정에서 느낀 건, MemberService 안에서 GameStyle 처리 로직이 너무 많은 책임을 가지고 있다는 점이었습니다.
그래서 MemberGameStyleService라는 전담 클래스로 분리했습니다.
변경 전
profileService.addMemberGameStyles()
변경 후
memberGameStyleService.updateGameStyle()
로직도 단계별로 명확하게 나눴습니다:
findRequestGameStyle() | 요청으로 들어온 ID 리스트 → GameStyle Entity 변환 |
findCurrentMemberGameStyleList() | 현재 DB에 존재하는 GameStyle 리스트 조회 |
removeUnnecessaryGameStyles() | 현재에는 있지만 요청에는 없는 스타일 제거 |
addNewGameStyles() | 요청에는 있지만 현재에 없는 스타일 추가 |
'개발 프로젝트 정리 > Gamegoo 롤 매칭 서비스' 카테고리의 다른 글
☄️컨트롤러 예외도 인증 실패? EntryPoint에서 500으로 통일한 사연 (1) | 2025.07.02 |
---|---|
☄️ RefreshToken 수정 시 동시성 예외 (ObjectOptimisticLockingFailureException) 발생 해결하기 (3) | 2025.06.19 |
☄️ matching-found-sender 누락 원인 분석과 UUID 검증 로직 추가 (1) | 2025.06.18 |
- Total
- Today
- Yesterday
- 프로그래머스
- nodejs
- 프로젝트
- 자바스크립트
- 운영체제
- 스페인
- 교환학생
- 해커톤
- JavaScript
- 백준
- 리눅스
- 개발
- Process
- JS
- MySQL
- 공룡책
- 개발일지
- 혼공단
- C++
- 스페인 교환학생
- AWS
- 혼공단 9기
- SQL
- 깃 예제
- Linux
- Signal
- 혼공단 SQL
- googleapis
- 혼공학습단
- 혼공
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 |