본문 바로가기
  • Let's study
카테고리 없음

3차 프로젝트: GameLog - 알림 서비스 추가

by 코딩고수이고파 2026. 10. 26.

사용자 활동 알림을 추가하고, 사용자 게임 도메인의 프로필 기능을 분리했다. 팀에서는 Java 테스트의 Kotlin 전환과 기존 Kotlin 코드 정리도 함께 진행했다.

하루 작업을 돌아보니 두 가지 질문이 연결되어 있었다. 새 기능이 기존 기능과 어떤 관계를 가져야 하는지, 그리고 기능이 늘어난 뒤 코드를 어떤 단위로 나누어야 하는지였다.

알림은 생성 이후의 상태까지 설계

좋아요나 댓글, 팔로우가 발생하면 알림을 저장하는 것만으로 끝날 것 같지만, 실제로는 결정할 조건이 많았다.

내가 내 글에 좋아요를 눌렀을 때도 알림을 보낼 것인가? 좋아요를 취소하면 기존 알림은 어떻게 할 것인가? 댓글이나 사용자가 사라진 뒤에도 알림 링크를 유지할 것인가?

이번에는 본인의 활동으로 생기는 알림을 제외하고, 취소된 활동에 대응하는 알림도 제거하도록 구성했다. 신고 승인으로 리뷰가 숨겨지는 경우에는 알림을 제공하되 신고자의 비공개 내용을 응답에 담지 않도록 했다.

특히 신경 쓴 부분은 원본 활동과 알림의 트랜잭션 관계다. 팔로우가 롤백됐는데 알림만 남으면 사용자는 실제로 일어나지 않은 일을 전달받는다. 이를 막기 위해 동기 이벤트 리스너가 원본 트랜잭션에 참여하도록 했다.

 

@EventListener
@Transactional(propagation = Propagation.MANDATORY)
fun receive(event: NotificationRequested) {
    // 원본 활동과 같은 트랜잭션에서 알림을 저장한다.
}

 

이벤트를 사용했다고 비동기로 처리한 것은 아니다. 이번 구현에서는 활동과 알림을 함께 커밋하거나 함께 롤백하는 관계가 필요했다. 통합 테스트에도 원본 동작을 롤백했을 때 알림이 남지 않는 시나리오를 포함했다.

알림의 완성도를 좌우하는 것은 생성 시점의 성공뿐 아니라, 원본 데이터의 상태가 바뀐 뒤에도 사용자에게 맞는 정보를 보여 주는지라는 점을 배웠다.

 

읽음과 보관 정책

 

알림은 계속 쌓이기 때문에 언제까지 보관할지도 정해야 했다. 일반 알림은 읽은 시점부터 7일, 미읽음 상태에서는 생성 후 30일을 기준으로 정리하고, 리뷰 숨김 알림은 생성 후 90일이라는 별도 기준을 적용했다.

여기서 중요한 것은 같은 날짜 계산이라도 기준 시각이 다르다는 점이다. 읽은 알림을 생성 시각으로 정리하면 사용자가 뒤늦게 확인한 알림이 너무 빨리 사라질 수 있다. 이미 읽은 알림을 다시 읽을 때 최초 읽음 시각이 바뀌지 않는지도 확인했다.

정리 쿼리는 한 번에 최대 1,000건을 삭제하도록 제한했다. 목록은 커서 기반으로 조회하고, 미읽음 필터를 조회 조건에 반영했다. 화면에 가져온 일부 목록만 필터링하면 페이지 크기와 다음 페이지 판단이 어긋날 수 있기 때문이다.

테스트도 정상 목록 조회에서 멈추지 않았다. 보관기간에 정확히 도달한 데이터와 아직 1초가 남은 데이터, 여러 번에 나누어 삭제해야 하는 데이터, 다른 사용자의 알림에 영향을 주지 않는 읽음 처리를 함께 확인했다.

 

Kotlin 표현을 정리하면서 읽기 어려운 이유

사용자 게임 도메인에는 Java의 표현을 그대로 옮긴 코드가 남아 있었다. 조회 결과를 처리하는 긴 체인, 불필요한 타입 표기, 문자열 연결 등을 정리했다.

예를 들어 필수 조회 결과가 없을 때의 예외 처리는 다음처럼 표현할 수 있었다.

private fun findUser(userId: Long): User =
    userRepository.findByIdOrNull(userId)
        ?: throw ServiceException("404-1", "사용자를 찾을 수 없습니다.")

다만 한 줄로 줄일 수 있는 모든 코드를 줄이는 방향으로 접근하지는 않으려고 했다. 검색어의 LIKE 특수문자를 이스케이프하는 부분은 중간 값에 이름을 붙여 처리 순서를 드러내는 편이 읽기 좋았다. 이름 있는 인자도 여러 숫자나 null이 이어지는 생성자에서 각 값의 의미를 설명해 줬다.

Kotlin 관용구를 적용할 때의 기준은 문법을 얼마나 많이 쓰느냐보다, 다음 사람이 조건과 결과를 쉽게 따라갈 수 있느냐에 두고 싶다.

프로필 API, 서비스 분리

기존 사용자 게임 컨트롤러에는 내 라이브러리를 변경하는 기능과 특정 사용자의 공개 프로필을 조회하는 기능이 함께 있었다.

이번에는 UserGameProfileController를 분리하고 프로필 관련 API를 /api/v1/users 아래로 옮겼다. 사용자 게임 기록을 다룬다는 공통점은 있지만, 내 기록을 관리하는 요청과 다른 사람의 프로필을 보는 요청은 사용자 관점과 인증 조건이 달랐다.

경로 변경은 백엔드에서 끝나지 않았다. 보안 설정, 프론트엔드 API 호출과 프록시 경로, 관련 테스트도 함께 맞춰야 했다. API 경로가 여러 계층이 공유하는 약속이라는 점을 다시 확인했다.

이후에는 UserGameProfileService를 분리해 프로필 통계, 취향 계산, 장르 분포, 인생 게임, 최근 리뷰와 관련된 책임을 옮겼다. UserGameService는 라이브러리와 게임 기록 관리에 집중하도록 정리했다.

분리 후에도 프로필 서비스가 최근 플레이 게임 조회를 위해 기존 서비스를 호출하는 부분은 남아 있다. 모든 의존성을 없애는 것보다, 각 서비스에 무엇을 변경하는 코드가 모여 있는지 설명할 수 있는 구조를 만드는 데 의미가 있었다.

테스트도 새 책임에 맞춰 정리

서비스를 나누면서 관련 테스트 역시 새 서비스 기준으로 재구성했다. 기존 클래스 이름을 유지하는 데 집중하기보다, 프로필 조회와 계산의 기대 결과가 어디에서 검증되는지 따라갈 수 있도록 정리했다.

알림 테스트에서는 존재하지 않는 사용자, 삭제된 댓글로 이동하는 링크, 반복해서 읽은 알림의 시각 같은 사례를 보완했다. 정상적인 데이터만 있을 때는 드러나지 않는 문제들이다.

이번 변경을 돌아보면 테스트를 판단하는 기준도 조금 더 구체화할 필요가 있다. 비슷한 코드가 여러 번 실행되더라도 소유권이나 경계값을 각각 확인한다면 남길 이유가 있다. 반대로 같은 결과를 같은 조건에서 반복 확인한다면 검증을 어디에 모을지 검토할 수 있다.

다음에는 테스트를 추가하거나 옮길 때마다 “이 테스트가 없으면 어떤 잘못된 동작을 놓치는가?”를 먼저 적어 보려고 한다.

다음 작업에서 이어갈 것

이번에는 알림 기능의 생성·취소·조회·보관을 함께 다루고, 프로필 기능의 책임을 컨트롤러와 서비스에서 분리했다. 기능을 추가하는 일과 기존 구조를 정리하는 일이 서로 떨어져 있지 않다는 점이 가장 크게 남는다.

앞으로는 새 기능을 붙일 때 정상 응답뿐 아니라 취소, 삭제, 탈퇴, 롤백 이후의 상태를 먼저 적어 두려고 한다. 책임을 옮기는 리팩터링에서는 API 호출부와 보안 설정, 테스트가 함께 따라왔는지도 확인할 것이다.

코드의 길이보다 변경 이유가 분명한 구조를 만드는 것, 그리고 그 구조가 지켜야 할 동작을 테스트로 남기는 것을 다음 작업의 기준으로 삼고 싶다.

댓글