SideProject

Docker 기반 웹 푸시 알림 시스템 - 4

싹다배워 2026. 2. 2. 22:59
반응형

Web Push 알림 시스템 구현 (DB 설계 변경 이후 실제 코드 중심)

웹 푸시 알림 로직

 

앞선 글에서는 Web Push 시스템의 DB 설계를 사용자 중심 → 구독(endpoint) 중심으로 변경한 배경을 정리했다.

이번 글에서는 그 설계가 실제 코드에서 어떻게 구현되었는지, Web Push 기술 특성과 어떤 식으로 맞물리는지를 코드 중심으로 정리해보았다.


전체 흐름 요약 (기술 관점)

관리자 --(발송 요청)-> Controller --> SendService 
--> PushSender --> FCM/PushServer --> Browser Service Worker

 큰 변경점은 사용자가 아니라 구독 단위로 발송이 이뤄진다는 점이다.


1. 구독 정보 도메인 (BDomain)

/*
 * 편의성을 위하여 Lombok 사용
 * Web Push 발송에 필요한 최소 정보만 보관
 */
 
 public class BDomain {

    private Long subsNumb;

    // 브라우저가 발급한 Push Endpoint
    private String endpoint;

    // Web Push 암호화 키
    private String p256Keyy;
    private String authKeyy;

    // 사용 여부 플래그
    private String isActive;
}

기술적 포인트

  • endpoint, p256dh, auth는 Web Push 표준 스펙 필수 요소
  • 로그인 정보 없이도 푸시 발송 가능

2. 발송 메시지 도메인 (CDomain)

/*
 * 편의성을 위하여 Lombok 사용
 * 관리자가 작성한 발송 메시지 정보
 */
 
 public class CDomain {

    private Long mesgNumb;

    private String titlText; //제목
    private String bodyText; //본문

    // 발송 여부, 예약 발송 확장 고려
    private String sendStat;
}

3. 발송 서비스 (SendService)

발송 로직의 책임은 Service 레이어에서 조합만 담당한다.

@Service
@RequiredArgsConstructor
@Slf4j
public class SendServiceImpl implements SendService {

    @Override
    public void sendPush(Long mesgNumb) {
        MesgDomain mesg = mesgDao.selectMesg(mesgNumb);
        List<CDomain> cList = cDao.selectActiveSubs();
        for (CDomain c : cList) {
            log.info("PUSH SEND Endpoint TO: {}", c.getEndpoint());
            log.info("PUSH SEND SubsNumb TO: {}", c.getSubsNumb());
            pushSender.send(c, mesg);
        }
    }
}

기술적 포인트

  • Service는 제어 흐름만 담당
  • 실제 Web Push 프로토콜 처리 로직은 PushSender로 완전 분리
  • 향후 조건별 발송/사용자 기준 발송으로 확장 용이

4. Web Push 핵심 구현부 (PushSender)

실제 Web Push 프로토콜을 다루는 핵심 클래스

@Component
@RequiredArgsConstructor
@Slf4j
public class PushSender {

    @Value("${vapid.public-key}")
    private String publicKey;

    @Value("${vapid.private-key}")
    private String privateKey;

    @Value("${vapid.subject}")
    private String subject;

    public void send(CDomain c, MesgDomain mesg) {

        // 필수 키 검증
        if (c.getP256Keyy() == null || c.getAuthKeyy() == null) {
            log.error("PUSH KEY NULL - cNumb={}", c.getCNumb());
            return;
        }

        try {
            Security.addProvider(new BouncyCastleProvider());

            PushService pushService =
                new PushService(publicKey, privateKey, subject);

            Notification notification = new Notification(
                    c.getEndpoint(),
                    c.getP256Keyy(),
                    c.getAuthKeyy(),
                    createPayload(mesg)
            );

            pushService.send(notification);

        } catch (Exception e) {
            log.error("PUSH SEND FAIL - cNumb={}", c.getCNumb(), e);
        }
    }

    private String createPayload(MesgDomain mesg) {
        return """
        {
          "title": "%s",
          "body": "%s"
        }
        """.formatted(
                mesg.getTitlText(),
                mesg.getBodyText()
        );
    }
}

기술적 포인트

  • VAPID 기반 인증 사용
  • BouncyCastle Provider 필수
  • Web Push는 동기 방식으로도 충분히 안정적이라고 판단
  • 비동기/큐 구조는 추후 고려 - 초기 단계에서는 구조를 억지로 넣지 않는 것이 디버깅과 장애 분석 측면에서 효율적이라 판단

5. Service Worker (수신 측)

브라우저는 Service Workder를 통해 푸시를 수신한다.

self.addEventListener("push", function (event) {
  console.log("PUSH EVENT RECEIVED");

  let data = {};

  try {
    if (event.data) {
      data = event.data.json();
    }
  } catch (e) {
    console.error("PUSH PAYLOAD PARSE ERROR", e);
  }

  const title = data.title || "알림";
  const options = {
    body: data.body || "새로운 알림이 도착했습니다.",
    tag: "notify-sideproject",
    renotify: true,
    data: {
      url: "/push"
    }
  };

  event.waitUntil(
    self.registration.showNotification(title, options)
  );
});

기술적 포인트

  • 서버는 JSON Payload만 전달

구현 후 느낀 점

  • Web Push는 사용자 계정 중심 사고를 버리는 순간 설계가 단순해진다.
  • endpoint 중심 설계는 테스트가 쉽고 장애 분석이 빠르다는 장점이 있었다.

다만, 5개의 환경에서 5건의 동일한 알림을 보냈을때, 유실되는 경우가 30% 확률로 발생했다.

매우 높은 실패 확률이라고 생각한다.

현재는 발송 가능여부 테스트가 우선이라 상세 내역은 분석하지 못했다.

추후 이러한 예외 사항들을 잡아가며 진행해야겠다.


반응형