1. 들어가기 전
스케줄링으로 정해진 시간에 강수량을 조회하고, 이에 따라 사용자들에게 강수 알림톡을 전송하는 기능을 개발하고자 한다. 정해진 시간에 강수량을 조회하는 기능은 이미 완성되었기에 특정 사용자에게 알림톡을 전송하는 기능만 구현하면 된다.
2. 친구톡과 알림톡
처음에 강수량에 따라 사용자들에게 메세지를 전송한다 라는 요구사항을 받았을 때 단순히 카카오톡 비즈 채널로 메세지를 전송하면 되겠다고 생각하였다. 어찌보면 반은 맞고, 반은 틀렸다고 할 수 있다.
왜냐하면 카카오톡으로 메세지를 전송하는 방식에는 2가지가 있는데 그게 친구톡 과 알림톡 이다.
2.1 친구톡
카카오 비즈니스 채널에는 친구톡과 알림톡이 있다.
먼저 친구톡은 수신자 입장에서는 일반적인 모먼트를 통해 발송된 메시지이다. 하지만 발송 과정에서 큰 특징이 있는데 카카오톡 채널을 맺은 이용자 대상으로만 메시지 발송이 가능하며, 광고성 메시지 전송이 가능하다는 것이다.
발송하기 위해서는 고객의 전화번호 정보를 입력하여 발송되는데, 모든 고객들에게 메시지가 발송되는 것이 아닌 해당 비즈니스 채널을 추가한 사용자 대상으로만 메시지가 발송된다는 것이다. 이는 카카오의 서비스 정책이므로 수정할 수 있는 사항이 아니다.
하지만 현재 개발하려는 메시지 전송의 큰 쟁점은 특정 경기를 신청하여 결제를 완료한 사용자들에게 강수량에 따라 우천 취소 여부를 무조건 전달하고자 하는 것이다. 따라서 경기를 신청하고, 결제를 완료하더라도 해당 사용자가 우리의 비즈니스 채널을 추가하지 않으면 강수톡을 보낼 수 없는 친구톡은 현재 요구사항과 적절하지 않다.
2.2 알림톡
알림톡은 카카오톡 채널을 추가하지 않은 이용자에게도 발송 가능한 정보성 메시지 이다. 사용자의 휴대폰 번호만 있다면 누구든지 항상 보낼 수 있기에 불법 스팸에 대한 법률이 좀 까다롭게 적용되는 편이며 광고성 메시지를 전송할 수 없다.
실제로 해당 서비스를 사용하기 위해서는 카카오와 파트너 계약이 맺어진 공식 딜러사를 사용해야한다. 우리는 그 중 NHN Cloud의 카카오톡 비즈니스 메시지 기능을 신청하였는데, 신청부터 본인인증에 대한 검수 단계도 2~3일 정도 걸렸다.
그 뿐만 아니라 사용하고자 하는 템플릿에 대해서도 검수가 진행되었는데, 카카오 비즈니스 가이드에 따르면 정보통신망법과 카카오 내부 기준에 따라 검수가 완료된 템플릿만 사용할 수 있는다고 공지되어 있다.
어찌됐든 텍스트 기반의 SMS 보다 CTA 버튼을 이용해 다양한 방식으로 활용할 수 있는 알림톡이 더 적절하다고 판단하여 알림톡을 선택하게 되었다.
3. 그래서 어떤 라이브러리를 사용할 것인가?
현재 NHN Cloud 를 통해 알림톡 API를 개발하고자한다. 외부 API를 사용하기 위해서는 RestTemplate, WebClient , OpenFeign 등 다양한 라이브러리가 있는데 나는 그 중에서 OpenFeign 를 이용하였다.
내가 생각하는 우선 가치는 성능 차이나 특정 상황에 맞는 라이브러리를 사용해야만 하는 경우가 아니라면 코드 가독성과 팀원들의 이해도가 가장 우선 순위이다.
2가지를 가장 우선 순위로 선정한 이유는 추후에 서술하겠다.
먼저 RestTemplate 는 Spring 3 시기에 첫 도입된 HTTP 통신을 지원하는 스프링 내장 객체이다. 그런데 Spring 3.0은 릴리즈 된지가 10년도 훨씬 지났다. 어찌보면 안정성이 있다는 말이지만 결국에는 오래된 기술이란 의미다. 한때 Deprecated 된다는 말이 있다가 maintenance mode 로 변경되기도 했었다. (Spring 6.1 에서 해당 언급도 삭제됨) 어찌됐든 스프링에서는 RestTemplate 를 권고하지는 않는다.
WebClient 는 비동기를 지원하지만 reactive 기반의 특성상 러닝커브가 완만하지 않다는 특징이 있다. 따라서 비동기가 반드시 필요한 경우가 아니라면 굳이 지금 당장 적용할 필요는 없다고 생각한다.
나의 우선 순위에 가장 적합한게 2가지 있었는데 RestClient 와 OpenFeign 이다.
RestClient 는 스프링 프레임워크가 적극적으로 밀고 있는 스프링 내장 객체로. 체이닝 방법으로 코드를 단순하고 깔끔하게 작성할 수 있다.
OpenFeign 은 인터페이스에 바로 Rest API를 매핑하는 선언적인 스타일로 사용법도 간편하고, 코드 가독성도 매우 좋다.
둘다 코드를 깔끔하게 작성하고, 가독성이 좋지만 OpenFeign 의 Rest API와 인터페이스의 결합으로 선언적인 스타일이 매우 마음에 들었다.
OpenFeign 을 선택한 이유
가장 결정적인 이유는 코드의 가독성과 간편한 사용법이다.
최근들어 느낀점이 프로젝트 규모가 커지면서 팀원들끼리 코드적인 부분에서 이해도가 다소 떨어지는 문제점이 있었다. 초기에 가독성을 많이 신경쓰지 않고, 모듈화 시키기보다 구현에만 신경을 썼던 부분들이 스택 마냥 쌓이다 보니 점점 팀원들이 짠 코드를 이해하기 어려워졌다.
결국 이러한 스파게티 코드들이 늘어나다 보니 해당 기능을 재사용하고자 할 때 모듈화가 되어있지 않아 비슷한 코드들을 다시 작성하고, 또 작성하고.. 계속된 반복이 많아졌다.
OpenFeign 을 사용한 client 코드를 살펴보면
@FeignClient(
name = "AlimTalkClient",
url = "<https://api-alimtalk.cloud.toast.com>",
configuration = {
FeignConfig.class,
AlimTalkHeaderConfig.class
}
)
public interface AlimTalkClient {
@PostMapping("/alimtalk/v2.3/appkeys/{appkey}/messages")
void send(
@PathVariable("appkey") @Value("KAKAO_BIZ_MESSAGE_APP_KEY") String appKey,
@RequestBody AlimTalkSendReq request
);
}
인터페이스에 Rest API만 매핑하여 간편하게 기능을 작성할 수 있다.
아무래도 앞서 설명한 문제들을 몸소 느끼다보니 최근에는 읽기 쉬운 코드, 이해하기 쉬운 코드들을 더 더욱 우선 가치로 두게 되었다. 이런 관점에서 OpenFeign의 방식이 다른 팀원들이 해당 코드를 보고 이해하기도 쉽고, 재사용하기도 쉽다고 판단하였다.
4. 카카오 알림톡 전송하기
본 목차는 NHN cloud의 KakaoTalk Bizmessage의 알림톡 API 가이드를 참고하였습니다.
- 헤더

요청을 보내기 위해서 헤더에는 X-Secret-Key 값이 필요한데, 이는 콘솔에서 확인할 수 있다.
우아한 feign 적용하기 를 참고하면 configuration을 따로 Bean으로 등록하여 헤더를 주입할 수 있는 방식을 안내하고 있다.

@Configuration
public class AlimTalkHeaderConfig {
@Value("${KAKAO_BIZ_MESSAGE_SECRET_KEY}")
private String secretKEy;
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> requestTemplate.header("X-Secret-Key", secretKEy);
}
}
나도 똑같이 클래스 단위에서 사용되는 메소드들이 항상 똑같은 헤더를 주입받을 수 있도록 설정하였다.
@FeignClient(
name = "AlimTalkClient",
url = "<https://api-alimtalk.cloud.toast.com>",
configuration = {
FeignConfig.class,
AlimTalkHeaderConfig.class
}
)
public interface AlimTalkClient {
// API 요청 로직 ..
}
Feign 을 적용할 인터페이스에 @FeignClien 를 설정하고 configuration에 AlimTalkHeaderConfig.class 를 지정해준다.
- body
{
"senderKey": String,
"templateCode": String,
"requestDate": String,
"senderGroupingKey": String,
"createUser": String,
"recipientList": [{
"recipientNo": String,
"templateParameter": {
String: String
},
"resendParameter": {
"isResend": boolean,
"resendType": String,
"resendTitle": String,
"resendContent": String,
"resendSendNo": String
},
"buttons": [
{
"ordering": Integer,
"chatExtra": String,
"chatEvent": String,
"relayId": String,
"oneClickId": String,
"productId": String,
"target": String
}
],
"quickReplies": [
"ordering": Integer,
"chatExtra": String,
"chatEvent": String,
"target": String
],
"recipientGroupingKey": String
}],
"messageOption": {
"price": Integer,
"currencyType": String
},
"statsId": String
}
requestBody에 들어갈 데이터들의 종류가 되게 많기 때문에 타입들에 대한 설명을 꼭 확인하자.
| 필드명 | 타입 | 피루여부 | 설명 |
| senderKey | String | O | 발신 키(40자) |
| templateCode | String | O | 등록한 발송 템플릿 코드(최대 20자) |
| recipientList | List | O | 수신자 리스트(최대 1000명) |
| - recipientNo | String | O | 수신번호(최대 15자) |
| - templateParameter | Object | X | 템플릿 파라미터(템플릿에 치환할 변수 포함 시, 필수) |
| -- key | String | X | 치환 키(#{key}) |
| -- value | String | X | 치환 키에 매핑되는 Value값 |
나는 해당 필드를 사용할 것 이다. 발송 실패에 대한 SMS 전송 여부도 지정할 수 있지만 일단 현재 요구사항에서는 해당되지 않으므로 따로 설정하지는 않았다.
알림톡을 전송할 템플릿은 사전에 작성한 뒤 반드시 검수를 받아야 한다. 그리고 검수가 완료되면 템플릿의 코드가 주어지는데 이를 이요하여 필요한 파라미터를 넣어 값을 치환하여 완전한 메시지를 전송해야한다.
public Object sendMessage(User user, Match match) {
Map<String, String> templateParameter = Map.of(
"name", user.getName(),
"matchName", match.getName(),
"matchTime", match.getStartAt(),
"field", field.getName(),
"appLinkId", match.getId()
);
AlimTalkSendReq.Recipients recipients = AlimTalkSendReq.Recipients.builder()
.recipientNo(user.getPhoneNumber())
.templateParameter(templateParameter)
.build();
AlimTalkSendReq request = AlimTalkSendReq.builder()
.senderKey(sendKey)
.recipientList(List.of(recipients))
.templateCode(TEMPLATE_CODE)
.build();
return alimTalkClient.send(appKey, request);
}
recipientList에 메세지를 전송받을 유저의 번호가 있어야한다. 그러므로 알림톡을 전송해야할 유저가 여러명이라면 유저들의 전화번호를 모두 입력하여야 한다.

성공적으로 알림톡이 전송된 것을 확인할 수 있다.
'Backend > Spring Boot' 카테고리의 다른 글
| ShedLock으로 분산 환경에서 스케줄러 중복 실행을 방지하자 (0) | 2026.04.21 |
|---|---|
| 이벤트 유실을 방지하기 위한 Outbox Pattern 도입기 (0) | 2026.04.14 |
| 분산 환경에서 MySQL Named Lock으로 분산 락을 구축하고, 동시성 제어하기 (0) | 2026.03.24 |
| Virtual Thread로 병렬 호출하여 Thread Pool 고갈 문제 해결하기 (2) | 2026.03.13 |
| RestClient는 어떻게 생성이 될까? (1) | 2026.01.31 |