도메인 설계

8개 도메인이 무엇을 책임지고 어떻게 이어지는지. 그림은 실제 build.gradle 과 어댑터에서 뽑은 값으로 그립니다.

도메인
8
계층
4
domain · app · infra · web
공용 모듈
6
shared/common-*
도메인 간 의존
2
적을수록 좋다

① 전체 지도 — 도메인과 실행 서버

서버는 둘이다. wvctesol-auth 가 신원을 확인해 토큰을 발급하고, wvctesol-api 는 그 토큰을 검증만 한다. 발급과 검증을 나눠 두면 API 서버가 늘어도 비밀키를 쥔 곳은 하나로 남는다.

100%휠로 확대 · 끌어서 이동

도메인 간 직접 의존은 두 개뿐이다 — commerce → catalog(주문이 가격을 읽는다), identity → notification(아이디 안내를 보낸다). 나머지는 서로를 모른다. 함께 봐야 하는 것은 shared/common-domain 에 둔다.

② 계층 — 화살표 방향이 규칙이다

100%휠로 확대 · 끌어서 이동

domain 은 스프링도 JPA 도 모른다. 그래서 테스트가 빠르고, 프레임워크를 바꿔도 규칙이 남는다. infrastructuredomain 의 포트를 구현하므로 화살표가 안쪽을 향한다 — 의존이 뒤집히는 지점이다.

계층별 의존 대상

계층의존하는 일
domainshared/common-domain 만스프링도 JPA 도 모른다. 순수 자바다
appdomain + 다른 도메인의 app/domain유스케이스와 트랜잭션 경계
infrastructuredomain + appJPA 어댑터. 레거시 표현 변환이 여기서 끝난다
webdomain + appHTTP 경계. infrastructure 를 모른다

webinfrastructure 를 모르는 것이 중요하다. 컨트롤러가 EntityManager 를 직접 쓰기 시작하면 계층이 무너진다 — 실제로 관리자 계정 조회를 만들 때 그렇게 썼다가 되돌렸다.

③ 수강권 — 도메인을 가로지르는 흐름

이 서비스에서 가장 중요한 상태는 수강권이다. 돈을 낸 사람만 강의를 보고 시험을 칠 수 있다. 그 값을 commerce 가 켜고 learning · assessment 가 읽는다 — 세 도메인이 member 한 테이블을 통해 이어진다.

100%휠로 확대 · 끌어서 이동
원본과 다른 점 — 원본은 결제가 회원 플래그를 건드리지 않았다. 관리자가 admin/member/mb_proc.php 에서 손으로 켰다. 카드 승인·입금 확인 시 자동으로 여는 것은 이쪽에서 정한 규칙이다.

④ 함께 보는 테이블

레거시 스키마를 그대로 쓰기 때문에 한 테이블을 여러 도메인이 본다. 이 지점이 도메인 경계가 흐려지기 쉬운 곳이라 그림으로 드러내 둔다.

100%휠로 확대 · 끌어서 이동

member 를 넷이 본다. 다만 쓰는 것은 identity 와 commerce 뿐이고 나머지는 읽기만 한다. apply 는 assessment(레벨테스트 신청)와 marketing(상담 관리)이 각각 다른 관점으로 본다.

⑤ 알림

메일과 문자를 보낸다. 실패해도 부른 쪽을 되돌리지 않는다.

자세히 →
wvctesol-auth :8081API 0
100%휠로 확대 · 끌어서 이동

핵심 타입

Notification보낼 알림 한 건. 받는 사람이 없으면 만들 수 없다
NotificationSender발송 포트. 실패해도 예외를 던지지 않는다
AccountNotifications아이디 안내 · 재설정 링크. 문구는 원본 그대로

다루는 테이블

DB 를 보지 않는다. 바깥으로 내보내는 일만 한다.

원본 대응

mail/* · member/find_id_new_proc.php

모듈

notification
모듈 README 원문

notification 모듈

책임: SMS/이메일/카카오 알림톡 발송, 알림 템플릿 관리, 다른 모듈에서 발생한 이벤트를 알림으로 변환.

의존 관계:

  • 사용 주체: 거의 모든 모듈 (identity, learning, assessment, commerce) 이 이벤트 발생 시 이 모듈을 호출
  • 외부 서비스: SMS 게이트웨이 (예: NHN Toast / Aligo / Coolsms), 이메일 (SES / SendGrid), 카카오 알림톡 (비즈메시지)

관련 DB 테이블:

  • sms_content — SMS 템플릿 (제목/본문)
  • sms_send — 발송 로그 (수신번호/시간/상태/응답)

특이사항: 이 모듈은 프론트에서 직접 호출하지 않음. 다른 모듈의 사이드이펙트로 호출됨. 따라서 UI 가 없어도 API 구현이 가능하고, 오히려 다른 모듈 구현과 함께 자연스럽게 등장함.


예상 API 엔드포인트

이벤트 기반 알림 (권장 방식)

POST /api/notification/events

도메인 이벤트를 받아서 적절한 알림 템플릿을 선택해 발송.

  • Auth: 내부 서비스 토큰 (외부 노출 금지)
  • Body:
    {
      eventType:
        | 'member.registered'
        | 'member.password_changed'
        | 'payment.completed'
        | 'payment.failed'
        | 'enrollment.activated'
        | 'exam.passed'
        | 'exam.all_completed'
        | 'certificate.issued'
        | 'certificate.shipped'
        | 'level_test.submitted'
        | 'consultation.requested';
      memberId?: number;
      payload: Record<string, unknown>;
    }
    
  • 처리:
    1. eventType 에 매핑된 템플릿 조회 (sms_content 또는 상수)
    2. memberId 로 수신자 정보 조회 (hp, email)
    3. 사용자 알림 수신 동의 확인
    4. 실제 발송 (내부적으로 POST /api/notification/sms/send 등)
    5. sms_send 에 발송 이력 기록
  • Response: { dispatched: Array<{ channel: 'sms'|'email'|'kakao'; to: string; ok: boolean }> }

이벤트 기반 방식의 장점:

  • 호출하는 쪽은 "결제 완료됐어" 만 알려주면 됨
  • 채널 선택 로직이 한 곳에 집중
  • A/B 테스트, 채널 변경이 쉬움

직접 발송 API (내부 호출용)

POST /api/notification/sms/send

  • Auth: 내부 서비스 토큰
  • Body:
    {
      to: string;           // 전화번호
      templateCode?: string;  // sms_content.code
      body?: string;          // templateCode 없을 때 직접 본문
      variables?: Record<string, string>;  // 템플릿 치환 변수
    }
    
  • Response: { messageId: string; status: 'queued' | 'sent' | 'failed' }
  • 사이드이펙트: sms_send INSERT

POST /api/notification/email/send

  • Body:
    {
      to: string;
      subject: string;
      bodyHtml?: string;
      bodyText?: string;
      templateCode?: string;
      variables?: Record<string, string>;
    }
    

POST /api/notification/kakao/send

카카오 알림톡 (비즈니스 메시지). 템플릿 사전 등록 필수.

  • Body:
    {
      to: string;           // 전화번호
      templateCode: string; // 카카오에 사전 등록된 템플릿 ID
      variables: Record<string, string>;
    }
    

템플릿 관리 (관리자)

GET /api/notification/admin/templates

SMS/이메일 템플릿 목록.

  • Auth: admin
  • Response: sms_content 행 목록

POST /api/notification/admin/templates

템플릿 생성/수정.

POST /api/notification/admin/test-send

테스트 발송 (관리자가 자기 번호로 템플릿 미리보기).


발송 로그 조회

GET /api/notification/admin/logs

  • Auth: admin
  • Query: ?channel=sms&status=failed&from=...&to=...
  • Response: sms_send 행 목록 + 실패 사유
  • 용도: 결제 완료됐는데 SMS 실패한 케이스 등 수동 개입 모니터링

⚠️ 구현 시 주의사항

1. 직접 호출 vs 큐 기반

  • MVP: 이벤트 수신 시 동기 발송 (단순하지만 장애 시 손실 위험)
  • 정석: 이벤트를 큐 (Redis / DB queue) 에 쌓고 워커가 처리 → 재시도 가능
  • 결제 완료 같이 놓치면 안 되는 이벤트 는 반드시 큐 기반 권장

2. 수신 동의 관리

  • member 테이블에 sms_consent, email_consent, marketing_consent 플래그 필요 (현재 없음 — 나중에 추가)
  • 법적 요구사항: 광고성 알림은 명시적 동의 필수, 거래성 알림 (결제/수강 확인 등) 은 동의 없이 가능

3. 발송량/비용

  • SMS 는 건당 비용 (10~30원). 중복 발송 dedup 필수.
  • 이메일은 무료에 가깝지만 bounce 관리 필요 (SES reputation)

4. 실패 재시도

  • SMS 게이트웨이가 5xx 반환 시 exponential backoff 로 최대 3회 재시도
  • 3회 실패 시 관리자 알림 큐에 넣기

5. 민감정보 로깅

  • sms_send.content 에 OTP / 임시 비밀번호 등을 평문 저장 금지
  • 최소 마스킹 or 해시 저장

이벤트 → 알림 매핑 예시 (참고)

이벤트 채널 발송 시점 템플릿 예시
member.registered SMS + 이메일 즉시 "WVC테솔 가입을 환영합니다"
payment.completed SMS + 이메일 즉시 "결제 완료: {productName} {amount}원"
enrollment.activated 카카오 알림톡 결제 완료 직후 "강의실 오픈! 지금 수강 시작하세요"
exam.passed SMS 즉시 "Unit {N} 합격! 축하합니다"
exam.all_completed SMS + 이메일 즉시 "최종 합격! 자격증이 곧 발급됩니다"
certificate.issued 이메일 즉시 "자격증 PDF" 첨부
certificate.shipped SMS 즉시 "자격증 발송 완료, 송장번호 {code}"
level_test.submitted 이메일 즉시 결과 + 상담 예약 링크
consultation.requested 카카오 알림톡 (관리자 번호) 즉시 새 상담 요청 알림

⑥ 공용 모듈 — 무엇을 shared 에 두는가

기준은 하나다. 여러 도메인이 함께 봐야 하는 것만 둔다. 편해서 두는 게 아니다 — shared 가 커지면 모든 도메인이 그것에 묶인다.

모듈담은 것왜 여기 있는가
common-domain
값 객체와 도메인 예외
CourseCodeEntitlementsDomainException 외 4종
Entitlements 는 identity 가 만들고 assessment · learning 이 판정에 쓴다. 한쪽에 두면 반대편이 그 도메인을 통째로 의존해야 한다.
common-security
요청 주체
ActorRequireAdmin
조회는 언제나 Actor 기준이다. 컨트롤러가 받은 파라미터로 남의 자료를 열지 않게 한다.
common-web
응답 포맷과 예외 변환
ApiErrorPageResponseGlobalExceptionHandler
예외 종류가 곧 상태 코드다. 컨트롤러마다 상태 코드를 정하지 않는다.
common-utils
레거시 표현 변환
LegacyFormat
varchar(14) 'YYYYMMDDHHmmss' 날짜와 Y/N 플래그를 여기 한 곳에서만 다룬다.
common-app
유스케이스 공통 타입
PageRequest
-
common-infrastructure
영속성 공통 설정
-