사이드프로젝트 slecs

푸시 알림 콜백 테스트 기능 추가

목차

푸시 알림 콜백 테스트 기능을 추가했다. 작업 자체는 내부 클래스 수정이라 규모가 크지 않았는데, 막상 손대보니 신경 써야 할 지점이 꽤 있었다.

푸시 알림 시스템에서 콜백은 알림 발송 이후 수신 상태를 피드백받는 구간이다. 발송은 성공했는데 실제로 단말에 도달했는지, 열어봤는지-이런 상태 값들이 외부 플랫폼에서 우리 서버로 POST 요청으로 돌아온다. 이 콜백이 제대로 처리되지 않으면 발송 이력과 실제 상태 사이에 괴리가 생기고, 나중에 통계를 뽑거나 재발송 조건을 판단할 때 근거가 흔들린다. 테스트 기능을 별도로 만든 이유가 바로 이거다. 실제 단말 없이도 콜백 수신 흐름 전체를 안전하게 검증할 수 있어야 한다.

구현하면서 챙긴 것들

Spring MVC + MyBatis 조합에서 내부 클래스가 요청을 받아 비즈니스 로직까지 처리하는 구조다. 콜백 테스트 기능도 같은 레이어에 얹었다.

파라미터 바인딩부터 시작. 외부 플랫폼 콜백은 필드 구성이 제각각이라 들어오는 값 자체가 불완전한 경우가 많다. null, 빈 문자열, 예상 범위 밖의 상태 코드-이런 케이스를 먼저 정리하고 들어가야 나중에 SQL 쪽에서 NPE나 맵핑 오류가 안 터진다. 테스트 요청도 동일한 검증 경로를 타게 해서 실제 콜백과 동일한 흐름을 재현하도록 했다.

// 콜백 파라미터 바인딩 예시 (단순화)
public class PushCallbackRequest {
    private String pushId;
    private String status;   // "delivered", "opened", "failed" 등
    private String errorCode; // nullable

    public boolean isValid() {
        return pushId != null && !pushId.isBlank()
            && status != null && !status.isBlank();
    }
}

이걸 컨트롤러에서 받을 때 바로 비즈니스 로직으로 넘기지 않고 isValid() 체크를 먼저 거치게 했다. 당연한 거 같아도 테스트 경로에서 이 검증을 건너뛰면 나중에 운영 콜백과 테스트 콜백의 처리 결과가 달라진다.

트랜잭션 범위. 콜백 수신 → 상태 업데이트 → 이력 INSERT 이 세 단계가 한 트랜잭션 안에 있어야 한다. 상태 업데이트는 성공했는데 이력 INSERT에서 실패하면 데이터가 반쪽짜리가 된다. @Transactional 어노테이션 범위를 서비스 메서드 단위로 명확히 끊어두고, 테스트 기능도 동일한 서비스 메서드를 거치게 했다.

예외 처리 결정. RuntimeException을 그냥 위로 전파할지, 잡아서 응답 코드로 변환할지-콜백 처리에서 이게 애매했다. 외부 플랫폼 입장에서는 우리가 200을 돌려줘야 재시도를 멈춘다. 내부에서 예외가 터졌을 때 500을 그대로 내보내면 플랫폼이 같은 콜백을 계속 재발송한다. 그래서 처리 가능한 예외(알 수 없는 pushId, 중복 콜백 등)는 잡아서 200 + 에러 메시지로 내보내고, 진짜 시스템 예외만 500으로 흘리는 방향으로 정리했다.

예외 유형 처리 방식 이유
알 수 없는 pushId 200 + 에러 메시지 플랫폼 재시도 방지
중복 콜백 수신 200 + 무시 처리 멱등성 보장
DB 연결 실패 등 500 전파 재시도가 맞는 케이스

SQL 쪽은 간단했음. 상태 업데이트 + 이력 INSERT 두 쿼리. MyBatis XML에서 <if test="errorCode != null"> 조건으로 null 필드는 쿼리에서 빼고, 결과 매핑은 resultMap으로 처리했다. 딱히 복잡한 건 없었고, 기존 쿼리 패턴 그대로 따라갔다.

로그를 어떻게 남겼나

운영에서 콜백 문제가 생겼을 때 디버깅하려면 들어온 값과 처리 결과가 로그에 있어야 한다. DEBUG 레벨로 다 남기면 운영에선 보이지 않고, ERROR는 정상 처리까지 오염된다. 그래서 입력 pushId, 수신 상태 코드, 처리 결과를 INFO로 남겼다.

log.info("push callback received. pushId={}, status={}", request.getPushId(), request.getStatus());
// 처리 완료 후
log.info("push callback processed. pushId={}, result={}", pushId, result);

테스트 기능으로 들어온 요청도 동일한 로그가 찍힌다. 운영 로그와 섞이는 게 싫으면 [TEST] 접두어를 붙이는 방법도 있는데, 이번엔 그냥 동일하게 뒀다. 어차피 테스트 요청은 특정 pushId 패턴으로 구분할 수 있어서.

배포 흐름

로컬 기동 확인하고 개발 DB에 반영한 다음 배포했다. 순서가 단순한데 이게 중요한 게, 로컬에서만 돌리고 개발 DB 없이 바로 올리면 스키마 차이나 데이터 차이로 올라가서 뒤집히는 경우가 있다. 특히 이번처럼 이력 테이블이 엮이는 경우는 로컬 메모리 DB랑 실제 개발 DB 동작이 미묘하게 다를 수 있어서 중간 단계를 건너뛰지 않는 게 낫다.

작업 규모로는 작은 편이었지만, 콜백 처리 테스트 경로가 없으면 실제 플랫폼 연동 전에 흐름을 검증할 방법이 없다. 테스트 기능은 기능 자체보다 그 기능을 믿을 수 있게 해주는 장치라는 게 작업하면서 다시 체감됐다.

댓글 0

첫 댓글 달아줘.