알림톡 발송 개발자 가이드

📌 개요

NotificationService를 통해 카카오 알림톡을 쉽게 발송할 수 있습니다.
다른 MSA 서비스(MemberService, AuthService 등)에서 REST API를 호출하여 알림톡을 발송하세요.


🚀 빠른 시작 (5분 완성)

1단계: 사용 가능한 템플릿 확인

# 활성화된 템플릿 목록 조회
curl -X GET "http://localhost/api/alimtalk/templates?activeOnly=true"

응답 예시:

{
  "success": true,
  "data": [
    {
      "templateId": "amano_hub_member_registered",
      "templateName": "회원가입 완료",
      "message": "[아마노파킹]\n회원가입을 축하합니다!\n...\n아이디: #{아이디명}\n가입 일시: #{가입날짜}",
      "requiredParams": "아이디명,가입날짜"
    }
  ]
}

2단계: 알림톡 발송

curl -X POST http://localhost/api/alimtalk/send \
  -H "Content-Type: application/json" \
  -H "X-Service-Name: MemberService" \
  -d '{
    "templateId": "amano_hub_member_registered",
    "phoneNumber": "010-1234-5678",
    "templateParams": {
      "아이디명": "hong@example.com",
      "가입날짜": "2025-11-16 14:30"
    }
  }'

💻 서비스 코드에서 사용하기

방법 1: RestTemplate 사용 (권장)

Step 1: RestTemplate Bean 등록

@Configuration
public class RestTemplateConfig {
    
    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        
        // X-Service-Name 헤더 자동 추가 (서비스명은 application.yml에서 관리)
        restTemplate.getInterceptors().add((request, body, execution) -> {
            request.getHeaders().set("X-Service-Name", "MemberService"); // 서비스명 변경 필요
            return execution.execute(request, body);
        });
        
        return restTemplate;
    }
}

Step 2: 알림톡 발송 클라이언트 작성

package com.akc.b2c.memberservice.client;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;

/**
 * 알림톡 발송 클라이언트
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class AlimtalkClient {
    
    private final RestTemplate restTemplate;
    
    @Value("${notification-service.url:http://localhost}")
    private String notificationServiceUrl;
    
    /**
     * 회원가입 환영 알림톡 발송
     */
    public void sendWelcomeMessage(String phoneNumber, String email) {
        String url = notificationServiceUrl + "/api/alimtalk/send";
        
        // 요청 데이터 생성
        Map<String, Object> request = new HashMap<>();
        request.put("templateId", "amano_hub_member_registered");
        request.put("phoneNumber", phoneNumber);
        
        // 템플릿 파라미터
        Map<String, String> params = new HashMap<>();
        params.put("아이디명", email);
        params.put("가입날짜", LocalDateTime.now()
            .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")));
        request.put("templateParams", params);
        
        try {
            // NotificationService 호출
            ResponseEntity<Map> response = restTemplate.postForEntity(
                url, 
                request, 
                Map.class
            );
            
            log.info("알림톡 발송 성공: phoneNumber={}, response={}", 
                     phoneNumber, response.getBody());
        } catch (Exception e) {
            // 알림톡 실패는 핵심 비즈니스에 영향 주지 않도록
            log.error("알림톡 발송 실패: phoneNumber={}, error={}", 
                      phoneNumber, e.getMessage(), e);
        }
    }
    
    /**
     * 동호수 승인 알림톡 발송
     */
    public void sendUnitApprovalMessage(String phoneNumber, String parkingLotName, 
                                        String dong, String ho, LocalDateTime approvedAt) {
        String url = notificationServiceUrl + "/api/alimtalk/send";
        
        Map<String, Object> request = new HashMap<>();
        request.put("templateId", "member_unit_approved");
        request.put("phoneNumber", phoneNumber);
        
        Map<String, String> params = new HashMap<>();
        params.put("주차장명", parkingLotName);
        params.put("동", dong);
        params.put("호", ho);
        params.put("승인일시", approvedAt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")));
        request.put("templateParams", params);
        
        try {
            restTemplate.postForEntity(url, request, Map.class);
            log.info("동호수 승인 알림톡 발송 성공: phoneNumber={}", phoneNumber);
        } catch (Exception e) {
            log.error("동호수 승인 알림톡 발송 실패: phoneNumber={}, error={}", 
                      phoneNumber, e.getMessage());
        }
    }
}

Step 3: 서비스에서 사용

@Service
@RequiredArgsConstructor
public class MemberService {
    
    private final AlimtalkClient alimtalkClient;
    private final MemberRepository memberRepository;
    
    @Transactional
    public void signUp(SignUpRequest request) {
        // 1. 회원 등록
        Member member = Member.builder()
            .email(request.getEmail())
            .phoneNumber(request.getPhoneNumber())
            .build();
        
        memberRepository.save(member);
        
        // 2. 알림톡 발송 (비동기 권장)
        try {
            alimtalkClient.sendWelcomeMessage(
                member.getPhoneNumber(), 
                member.getEmail()
            );
        } catch (Exception e) {
            // 알림톡 실패해도 회원가입은 성공 처리
            log.warn("알림톡 발송 실패했지만 회원가입은 완료: memberId={}", member.getId());
        }
    }
}

방법 2: WebClient 사용 (Reactive)

@Component
@RequiredArgsConstructor
public class AlimtalkWebClient {
    
    private final WebClient webClient;
    
    @Value("${notification-service.url:http://localhost}")
    private String notificationServiceUrl;
    
    public Mono<Map> sendAlimtalk(String templateId, String phoneNumber, 
                                   Map<String, String> templateParams) {
        
        Map<String, Object> request = new HashMap<>();
        request.put("templateId", templateId);
        request.put("phoneNumber", phoneNumber);
        request.put("templateParams", templateParams);
        
        return webClient.post()
            .uri(notificationServiceUrl + "/api/alimtalk/send")
            .header("X-Service-Name", "MemberService") // 서비스명 변경 필요
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(request)
            .retrieve()
            .bodyToMono(Map.class)
            .doOnSuccess(response -> 
                log.info("알림톡 발송 성공: response={}", response))
            .doOnError(error -> 
                log.error("알림톡 발송 실패: error={}", error.getMessage()));
    }
}

방법 3: FeignClient 사용

@FeignClient(name = "notification-service", url = "${notification-service.url:http://localhost}")
public interface NotificationServiceClient {
    
    @PostMapping("/api/alimtalk/send")
    Map<String, Object> sendAlimtalk(
        @RequestHeader("X-Service-Name") String serviceName,
        @RequestBody AlimtalkSendRequest request
    );
}

// 사용 예시
@Service
@RequiredArgsConstructor
public class SomeService {
    
    private final NotificationServiceClient notificationClient;
    
    public void sendAlimtalk() {
        AlimtalkSendRequest request = AlimtalkSendRequest.builder()
            .templateId("amano_hub_member_registered")
            .phoneNumber("010-1234-5678")
            .templateParams(Map.of("아이디명", "test@example.com"))
            .build();
        
        notificationClient.sendAlimtalk("MemberService", request);
    }
}

📋 사용 가능한 템플릿

1. 회원가입 완료 (amano_hub_member_registered)

필수 파라미터:

사용 예시:

Map<String, String> params = new HashMap<>();
params.put("아이디명", "hong@example.com");
params.put("가입날짜", "2025-11-16 14:30");

2. 동호수 승인 완료 (member_unit_approved)

필수 파라미터:

사용 예시:

Map<String, String> params = new HashMap<>();
params.put("주차장명", "종암우림카이저");
params.put("동", "101");
params.put("호", "1503");
params.put("승인일시", "2025-11-16 15:00");

3. 동호수 승인 거부 (member_unit_rejected)

필수 파라미터:

사용 예시:

Map<String, String> params = new HashMap<>();
params.put("주차장명", "종암우림카이저");
params.put("동", "101");
params.put("호", "1503");
params.put("거부사유", "등록 서류 불일치");

🔧 설정 (application.yml)

# NotificationService URL 설정
notification-service:
  url: http://localhost  # 로컬 개발 환경
  # url: http://notification-service:8080  # Docker 환경

📊 발송 이력 확인

1. 전체 로그 조회

curl -X GET "http://localhost/api/alimtalk/logs?page=0&size=20"

2. 특정 전화번호 이력 조회

curl -X GET "http://localhost/api/alimtalk/logs/phone/010-1234-5678"

3. 실패한 알림톡만 조회

curl -X GET "http://localhost/api/alimtalk/logs?sendStatus=FAILED"

4. 특정 서비스의 발송 이력

curl -X GET "http://localhost/api/alimtalk/logs?serviceName=MemberService"

⚠️ 주의사항

1. 에러 처리

알림톡 발송 실패는 핵심 비즈니스 로직에 영향을 주지 않도록 처리하세요.

// ❌ 나쁜 예: 알림톡 실패 시 회원가입 롤백
@Transactional
public void signUp(SignUpRequest request) {
    Member member = memberRepository.save(newMember);
    alimtalkClient.sendWelcomeMessage(...); // 예외 발생 시 롤백됨
}

// ✅ 좋은 예: 알림톡 실패해도 회원가입은 성공
@Transactional
public void signUp(SignUpRequest request) {
    Member member = memberRepository.save(newMember);
    
    try {
        alimtalkClient.sendWelcomeMessage(...);
    } catch (Exception e) {
        log.warn("알림톡 발송 실패 (회원가입은 완료): memberId={}", member.getId());
    }
}

2. 비동기 발송 권장

대량 발송이나 성능이 중요한 경우 비동기 처리를 권장합니다.

@Service
@RequiredArgsConstructor
public class AlimtalkAsyncClient {
    
    private final AlimtalkClient alimtalkClient;
    
    @Async
    public CompletableFuture<Void> sendAsync(String phoneNumber, String email) {
        return CompletableFuture.runAsync(() -> 
            alimtalkClient.sendWelcomeMessage(phoneNumber, email)
        );
    }
}

3. 재시도 로직 (선택사항)

실패 시 재시도를 원하면 Spring Retry를 사용하세요.

@Retryable(
    value = {HttpClientErrorException.class},
    maxAttempts = 3,
    backoff = @Backoff(delay = 2000)
)
public void sendAlimtalk(...) {
    // 발송 로직
}

4. 전화번호 형식


🆕 새 템플릿 추가 요청

새로운 알림톡 템플릿이 필요하면 다음 정보를 포함해서 요청하세요:

  1. 템플릿 ID: 영문 소문자 + 언더스코어 (예: password_reset)
  2. 템플릿 명칭: 한글 명칭 (예: "비밀번호 재설정")
  3. 메시지 내용: 실제 발송될 메시지
  4. 필수 파라미터: 치환될 파라미터 목록 (예: 인증번호,유효시간)
  5. 사용 시나리오: 언제 발송되는지 설명

예시:

템플릿 ID: password_reset
템플릿 명칭: 비밀번호 재설정
메시지 내용:
[아마노파킹]
비밀번호 재설정 요청

인증번호: #{인증번호}
유효시간: #{유효시간}

필수 파라미터: 인증번호, 유효시간
사용 시나리오: 사용자가 비밀번호 재설정을 요청할 때

🐛 문제 해결

문제 1: "템플릿을 찾을 수 없습니다"

원인: 템플릿 ID가 잘못되었거나 비활성화 상태

해결책:

# 활성화된 템플릿 확인
curl -X GET "http://localhost/api/alimtalk/templates?activeOnly=true"

문제 2: "필수 파라미터 누락"

원인: templateParams에 필수 파라미터가 없음

해결책: 템플릿의 requiredParams를 확인하고 모든 파라미터를 전달하세요.

// ❌ 나쁜 예: 필수 파라미터 누락
Map<String, String> params = new HashMap<>();
params.put("아이디명", "hong@example.com");
// 가입날짜 누락!

// ✅ 좋은 예: 모든 필수 파라미터 포함
Map<String, String> params = new HashMap<>();
params.put("아이디명", "hong@example.com");
params.put("가입날짜", "2025-11-16 14:30");

문제 3: 발송은 성공했는데 로그에 "발송 대기 중..."

원인: 이전 버전 코드 사용 중 (메시지 업데이트 안 됨)

해결책: NotificationService를 최신 버전으로 업데이트하세요.