관리자 채팅 백엔드 로직 개선
목차
관리자 채팅 백엔드 작업은 언뜻 보면 단순한 리팩터링처럼 보이지만, 실제로는 그게 아니다. 관리자용 기능은 일반 사용자 화면보다 훨씬 촘촘하게 짜야 한다. 장애가 나면 피해 범위가 넓고, 권한·세션·로그 같은 민감한 로직이 얽혀있어서 엣지 케이스 하나가 보안 구멍이 되기도 한다. 관리자 채팅처럼 실시간 커뮤니케이션이 들어간 도메인은 "조용히 실패"하는 버그가 특히 위험하다. 에러가 안 나는 것처럼 보이는데 메시지가 실은 전달이 안 된 상태.
이번 작업에서 변경이 백엔드 6개 파일에만 집중됐다는 게 중요하다. JSP도, 쿼리 XML도, CSS도 손대지 않았다.
| 레이어 | 파일 수 | 주요 변경 |
|---|---|---|
| 백엔드 로직 | 6개 | 핵심 처리 로직 개선 |
| 화면 (JSP) | 0개 | - |
| 쿼리 (XML) | 0개 | - |
| 스타일 | 0개 | - |
순수하게 처리 로직 레이어만 건드렸다는 건 검증 범위가 명확해진다는 뜻이다. UI가 같고 쿼리도 같으니, 변경 후 동작이 달라졌다면 그건 100% 내가 건드린 코드에서 생긴 문제다. 이 명확함이 있어야 배포를 두려움 없이 할 수 있다.
중복 제거는 왜 이렇게 반복해서 해야 하나
중복 코드는 처음엔 무해하게 시작된다. "이 클래스에도 같은 로직이 필요한데, 빨리 복붙하고 나중에 정리하자." 나중은 오지 않는다. 결국 똑같은 코드가 세 군데에 존재하고, 한 군데 버그를 고치면서 나머지 두 군데를 빠뜨리는 상황이 온다.
관리자 채팅처럼 메시지 발송·수신·상태 변경이 여러 흐름에서 발생하는 도메인은 이 문제가 특히 심해진다. 채팅 상태 검증 로직, 발송자 권한 체크, 메시지 형식 정규화 같은 코드가 컨트롤러, 서비스, 헬퍼 클래스에 각각 흩어져있으면 어느 순간 세 곳이 다 조금씩 다른 동작을 하게 된다. "왜 A 경로로 들어오면 validation이 다르게 작동하지?" 이런 질문을 하는 순간 이미 늦었다.
이번에 공통화한 로직은 서비스 레이어로 단일화했다. 패턴을 단순하게 정리하면 이런 식이다:
// Before: 각 핸들러마다 비슷한 검증 코드가 산재.
// 어떤 핸들러는 예외를 던지고, 어떤 핸들러는 그냥 리턴하고,
// 어떤 핸들러는 에러 코드를 반환 - 세 군데 다 미묘하게 다름
if (msg == null || msg.trim().isEmpty()) {
log.warn("메시지 없음");
return;
}
// After: 공통 검증 메서드로 위임
// 실패 시 내부에서 일관된 예외를 던지므로 상위 핸들러가 예측 가능하게 처리 가능
adminChatValidator.validateMessage(msg);
별거 아닌 것처럼 보이지만, 예외 처리 방식이 통일되면 상위 레이어에서도 예측 가능하게 처리할 수 있다. 그리고 검증 정책이 바뀌었을 때 한 군데만 수정하면 된다는 게 시간이 지날수록 진짜 자산이다.
에러 로그 - 새벽 3시의 동료
에러 메시지 개선은 눈에 잘 안 띄는 변경이지만, 로그 메시지의 가치는 개발할 때가 아니라 새벽에 알람 받고 로그를 뒤져볼 때 판가름난다.
// Bad - 이 로그로 할 수 있는 것: 에러가 났다는 사실 확인. 끝.
log.error("처리 실패");
// Good - 어느 채팅방에서 누가 어떤 타입의 메시지를 보낼 때 터졌는지 즉시 파악 가능
log.error("관리자 채팅 메시지 전송 실패 [roomId={}, senderId={}, msgType={}]",
roomId, senderId, msgType);
컨텍스트가 없는 에러 로그는 그냥 소음이다. 관련 ID와 상태가 찍혀있으면 DB 조회 없이도 원인 후보를 좁힐 수 있다. 로그 하나를 잘 쓰면 나중에 디버깅 시간을 30분씩 아낄 수 있다.
엣지 케이스 처리도 같은 맥락이다. 서비스가 릴리즈되기 전에는 과잉처럼 느껴진다. "이런 상황이 실제로 올까?" 싶은데, 온다. 예상 못한 순서로, 예상 못한 조합으로. 관리자 채팅의 경우, 메시지를 전송하는 주체가 시스템일 수도 있고 사람일 수도 있다. 세션이 만료된 상태에서 전송이 시도될 수 있고, 채팅방 상태가 변경되는 도중에 메시지가 들어올 수도 있다. 이 분기들이 명확하지 않으면 에러가 아닌 곳에서 NullPointerException이 터지거나, 에러인 상황을 조용히 씹어버린다. 후자가 더 위험하다.
코드를 어떻게 읽을 건지를 먼저 생각하고 짜기
이번 작업을 진행하면서 다시 확인한 것들:
- 미래의 나: 6개월 후에 이 코드를 다시 봤을 때 이해할 수 있는가
- 다음 개발자: 나 말고 다른 사람이 이 코드를 보면 어떻게 느낄까
- 운영 상황: 새벽 3시에 장애가 났을 때 이 코드가 문제를 빨리 파악하게 해주는가
이 세 가지를 두고 코드를 짜면 자연스럽게 "영리한 코드보다 읽기 쉬운 코드"로 수렴한다. 트릭을 쓴 한 줄짜리 코드보다, 세 줄이어도 의도가 바로 보이는 코드가 낫다. "단일 책임"도 마찬가지다. 하나의 함수가 하나의 역할만 한다는 게 교과서 얘기처럼 들리지만, 실제로 지키고 있으면 테스트 작성이 자연스러워지고 변경 범위가 예측 가능해진다.
| 좋은 코드의 기준 | 나쁜 코드의 신호 |
|---|---|
| 읽으면 의도가 바로 보임 | 주석 없으면 이해 불가 |
| 변경이 한 곳에만 영향 | 한 곳 바꾸면 여러 곳 수정 필요 |
| 테스트 작성이 자연스러움 | 테스트하려면 구조 바꿔야 함 |
| 에러 케이스가 명시적으로 처리됨 | 에러를 조용히 삼키는 catch 블록 |
운영 중인 서비스 변경 시 가장 중요한 건 기존 동작을 깨지 않는 것이다. 변경 전후 동작을 직접 확인하고, 배포 후 모니터링을 빠뜨리지 않는 것. 이런 작업들이 쌓이면서 시스템이 점점 견고해진다는 걸 느낀다. 당장 티가 안 나는 작업이지만 나중에 디버깅할 때 시간을 크게 줄여준다는 게 지금까지의 경험이다. 백로그에 묻혀있던 "코드 품질 개선" 티켓을 쳐낼 때마다, 미래의 내가 조금 더 편해진다는 생각으로 한다.
댓글 0
첫 댓글 달아줘.