[TIL] ํ๋ก์ ํธ 23์ผ์ฐจ.. ์นด์นด์คํก ์๋ฆผ ๊ตฌํํ๊ธฐ!!
๐ ์ค๋์ ์ด๋ค ํ๋ฃจ์์ง..
๊ฐ์ฅ ๋จผ์ ๋ฐฐ์ก ์์ฑ ์คํจ ์ ์ฌ๊ณ ๋ณต์์ ์ํ ๋ก์ง์ ๊ตฌํํ๋ค.
์ฃผ๋ฌธ ์์ฑ ํ ํฝ์ ๋ค์ด๊ฐ ์๋ ์ฌ๊ณ ๊ฐ์ ๋ก์ง๊ณผ ํจ๊ป ๋ฐ๋ก ์ฌ๊ณ ๊ด๋ จ ํ ํฝ์ ์์ฑํด์ ๋ฉ์์ง ๋ถ๊ธฐ ์ฒ๋ฆฌ๊ฐ ๋ ์ ์๋๋ก ๋ก์ง์ ๊ตฌํํ๋ ค๊ณ ํ๋ค.
// ์ฌ๊ณ ๊ด๋ จ Kafka Listener
@KafkaListener(topics = "${kafka.topic.product}", groupId = "${spring.kafka.consumer.product.inventory.group-id}")
public void listenInventory(@Header(KafkaHeaders.RECEIVED_KEY) String key, String value) {
try {
OperationWrapperDto wrapperDto = JsonHelper.fromJson(value, OperationWrapperDto.class);
OperationType type = wrapperDto.operationType();
switch (type) {
case ORDER_PAYMENT_INVENTORY_DECREASE -> routeInventoryDecreaseMessage(key, value);
case DELIVERY_FAIL_INVENTORY_RESTORE -> routeInventoryIncreaseMessage(key, value);
default -> log.error("โ ์ง์ํ์ง ์๋ ๋ฉ์์ง ํ์
์์ : {}", type);
}
} catch (ConflictException e) {
log.warn("โ ๏ธ ์ฌ๊ณ ๋ถ์กฑ์ผ๋ก ๋ฉ์์ง ์ฒ๋ฆฌ ์คํจ: {}", e.getMessage());
} catch (NotFoundException e) {
log.warn("โ ๏ธ ํ์ ๋ฐ์ดํฐ ๋๋ฝ์ผ๋ก ๋ฉ์์ง ์ฒ๋ฆฌ ์คํจ ({}): {}", e.getErrorCode(), e.getMessage());
} catch (NumberFormatException e) {
log.warn("โ ๏ธ ์๋ชป๋ orderId ํ์์
๋๋ค. key: {}, message: {}", key, e.getMessage());
} catch (Exception e) {
log.error("โ ์๊ธฐ์น ๋ชปํ ์ค๋ฅ๋ก ๋ฉ์์ง ์ฒ๋ฆฌ ์คํจ: {}", e.getMessage());
}
}
- INVENTORY_DECRERASE: ์ฌ๊ณ ๊ฐ์
- INVENTORY_INCREASE: ์ฌ๊ณ ๋ณต์
DeliverySerive์์ ์์ธ ๋ฐ์ ์ KafkaTemplate์ ํ์ฉํด์ ๋ฉ์์ง๋ฅผ ๋ฐํํ ์ ์๋๋ก ํ๋ฉด ๋ ๊ฒ ๊ฐ์๋ค.
๊ทธ๋ผ Listener๋ ์์ฒญํ ๋ฉ์์ง๋ฅผ ๋ฐ์์ ์ฌ๊ณ ๋ณต์ ๋ก์ง์ ์ํํ๋ฉด ๋๋ค.
kafkaTemplate.send(productTopic, request.orderId().toString(), request);
private void routeInventoryIncreaseMessage(String key, String value) {
// todo ๋ฐฐ์ก ์คํจ ์ ์ฌ๊ณ ๋ณต์ ๋ก์ง ์์ฑ
log.info("๐ฅ Kafka ์ฌ๊ณ ๋ณต์ ๋ฉ์์ง ์์ : key={}, value={}", key, value);
// ์ฌ๊ณ ๋ณต์ ๋ก์ง
try {
Long orderId = Long.valueOf(key);
productOptionCombinationService.increaseProductOptionCombinationInventory(orderId);
} catch (NumberFormatException e) {
log.error("โ orderId ํ์ฑ ์คํจ. ์๋ชป๋ key ํ์: {}", key);
}
}
kafkaTemplate์์ key๋ก orderId๋ฅผ ๋๊ฒจ์ฃผ๊ณ ์๊ธฐ ๋๋ฌธ์, ์ฌ๊ณ ๋ณต์ ๋ก์ง์์๋ key๋ฅผ ํ์ฉํด์ ๋ฉ์๋๋ฅผ ์ํํ ์ ์๋ค.
ORDER_PAYMENT_INVENTORY_DECREASE,
DELIVERY_FAIL_INVENTORY_RESTORE
๊ทธ๋ฆฌ๊ณ ๋ค์๊ณผ ๊ฐ์ด ๋ช ํํ๊ฒ ํ์ธํ ์ ์๋ ๋ฉ์์ง ํ์ ์ผ๋ก ์์ ํ๋ค.
String payload = JsonHelper.toJson(request);
OperationWrapperDto wrapper = OperationWrapperDto.from(OperationType.DELIVERY_FAIL_INVENTORY_RESTORE, payload);
String wrappedMessage = JsonHelper.toJson(wrapper);
kafkaTemplate.send(productTopic, request.orderId().toString(), wrappedMessage);
๊ทธ๋ฆฌ๊ณ ๋ค์๊ณผ ๊ฐ์ด CreateDeliveryRequest๋ฅผ ๋ฐ๋ก ๋ณด๋ด๋ ๊ฒ์ด ์๋๋ผ WrapperDto๋ก ๋ณํํด์ ๋ฉ์์ง ํ์ ์ด ํฌํจ๋ Dto๋ก ๋ณด๋ด์ค ์ ์๋๋ก ์ฒ๋ฆฌ ๊ณผ์ ์ด ํ์ํด์ ์ถ๊ฐ์ ์ผ๋ก ์์ฑํ๋ค.
์ฌ๊ณ ๋ณต์ ๋ก์ง ๊ตฌํ์ด ์์ฑ๋๊ณ ์นด์นด์คํก ์๋ฆผ ๋ฉ์์ง ์ ์ก์ผ๋ก ๋์ด๊ฐ๋ค.
KakaoMessageBuilder
@Slf4j
@Component
public class KakaoMessageBuilder {
public String buildTextTemplateObject(UserNotification userNotification) {
try {
ObjectMapper objectMapper = new ObjectMapper();
// ํ
์คํธ ๋ธ๋ก ๊ตฌ์ฑ
String text = """
[์ํ ์ถ๊ณ ์๋ด]
์ฃผ๋ฌธํ์ ์ํ์ด ๋ฐ์ก๋์์ต๋๋ค.
๋ฐฐ์ก ์กฐํ๊น์ง ํ์ผ ๊ธฐ์ค 1~2์ผ ์ ๋ ์์๋ ์ ์์ต๋๋ค.
์ํ ์๋ น๊น์ง ์กฐ๊ธ๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์!
โ ์ฃผ๋ฌธ ์ ๋ณด
์ฃผ๋ฌธ๋ฒํธ: %s
โ ๋ฐฐ์ก ์ ๋ณด
ํ๋ฐฐ์ฌ: %s
์ก์ฅ๋ฒํธ: %s
โ ์ฐธ๊ณ ์ฌํญ
- ์ ํด์ง ์ถ๊ณ ์ง(%s)์์ ์ถ๊ณ ๋ฉ๋๋ค.
""".formatted(
userNotification.orderId(),
userNotification.courierName(),
userNotification.trackingNumber(),
userNotification.senderName()
);
ObjectNode root = objectMapper.createObjectNode();
root.put("object_type", "text");
root.put("text", text);
ObjectNode link = objectMapper.createObjectNode();
link.put("web_url", userNotification.trackingUrlTemplate());
link.put("mobile_web_url", userNotification.trackingUrlTemplate());
root.set("link", link);
root.put("button_title", "์ฃผ๋ฌธ ์กฐํ");
return objectMapper.writeValueAsString(root);
} catch (Exception e) {
log.error("โ ์นด์นด์ค ํ
์คํธ ํ
ํ๋ฆฟ ์์ฑ ์คํจ: {}", e.getMessage());
return null;
}
}
}
KakaoMessageService
@Slf4j
@Service
@RequiredArgsConstructor
public class KakaoMessageService {
private final KakaoMessageBuilder kakaoMessageBuilder;
private final WebClient webClient;
@Value("${kakao.token}")
private String token;
@Value("${kakao.url}")
private String url;
public void sendMessage(UserNotification userNotification) {
String templateObject = kakaoMessageBuilder.buildTextTemplateObject(userNotification);
if (templateObject == null || templateObject.isBlank()) {
log.warn("โ ๏ธ ํ
ํ๋ฆฟ ์์ฑ ์คํจ๋ก ๋ฉ์์ง ์ ์ก์ด ์ค๋จ๋์์ต๋๋ค.");
return;
}
webClient.post()
.uri(url)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.body(BodyInserters.fromFormData("template_object", templateObject))
.retrieve()
.bodyToMono(String.class)
.doOnNext(response -> log.info("โ
์นด์นด์ค ๋ฉ์์ง ์ ์ก ์ฑ๊ณต: {}", response))
.subscribe(
response -> log.info("โ
์นด์นด์ค ์๋ต ์ฑ๊ณต: {}", response),
error -> {
if (error instanceof WebClientResponseException e) {
log.warn("โ ์นด์นด์ค API ์๋ต ์คํจ: {} - {}", e.getStatusCode(), e.getResponseBodyAsString());
} else {
log.error("โ ์นด์นด์ค ๋ฉ์์ง ์ ์ก ์ค ์ ์ ์๋ ์ค๋ฅ ๋ฐ์", error);
}
}
);
}
}
์ด๋ ๊ฒ ๋ชจ๋ ์ค์ ์ด ๋๋ ํ ๋ฐฐ์ก์ ์ถ๊ณ ํ๊ฒ ๋๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์นด์นด์คํก ์๋ฆผ ๋ฉ์์ง๊ฐ ๋์ฐฉํ๋ค!
๐ก ์๋กญ๊ฒ ์๊ฒ ๋ ๋ด์ฉ์ ๋ญ๊ฐ ์๋๋ผ..?!
โ 1. ์นด์นด์ค ๊ฐ๋ฐ์ ์ฝ์ ์ ์
๐ https://developers.kakao.com
- ์นด์นด์ค ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ
- ์ค๋ฅธ์ชฝ ์๋จ ๋ฉ๋ด์์ ๋ด ์ ํ๋ฆฌ์ผ์ด์ → ์ ํ๋ฆฌ์ผ์ด์ ์ถ๊ฐ
โ 2. ์ ํ๋ฆฌ์ผ์ด์ ์์ฑ
- ์ฑ ์ด๋ฆ: ์์ ๋กญ๊ฒ (TurtleMart Test ๋ฑ)
- ์ฌ์ ์๋ช : ์๋ต ๊ฐ๋ฅ
์ฑ์ด ์์ฑ๋๋ฉด,
→ REST API ํค๋ฅผ ๋ณต์ฌํด๋๋ค! (Access Token ๋ฐ๊ธํ ๋ ์ฌ์ฉ๋๋ค.)
โ 3. ํ๋ซํผ ์ค์
๐ ์ข์ธก ๋ฉ๋ด → ํ๋ซํผ > Web ํ๋ซํผ ๋ฑ๋ก
- ์ฌ์ดํธ ๋๋ฉ์ธ ์ ๋ ฅ:
- <http://localhost:8080>
โ 4. ์นด์นด์ค ๋ก๊ทธ์ธ ์ค์
๐ ์ข์ธก ๋ฉ๋ด → ์ ํ ์ค์ > ์นด์นด์ค ๋ก๊ทธ์ธ
- "์นด์นด์ค ๋ก๊ทธ์ธ ํ์ฑํ": โ ON
- Redirect URI ๋ฑ๋ก:
- <http://localhost:8080/oauth/kakao/callback>
์ด URI๋ ๋์ค์ Access Token ๋ฐ์ ๋ ์ฌ์ฉํ๋ ์ฃผ์๋ค!
โ 5. ๋์ ํญ๋ชฉ ์ค์
๐ ์ข์ธก ๋ฉ๋ด → ์นด์นด์ค ๋ก๊ทธ์ธ > ๋์ํญ๋ชฉ ์ค์
- ์นด์นด์คํก ๋ฉ์์ง ์ ์ก(talk_message) ํญ๋ชฉ์ ์ฐพ๋๋ค.
- "ํ์ ๋์" ๋๋ "์ ํ ๋์"๋ก ON
โ ์ด๊ฑธ ์ ์ผ๋ฉด ๋ฉ์์ง ์ ์ก์ด ๊ฑฐ์ ๋๋ค!
โ 6. ์ฑ ํค ํ์ธ
๐ ์ข์ธก ๋ฉ๋ด → ์ฑ ํค
- REST API ํค
- JavaScript ํค
→ ๋ ๋ค ๋ณต์ฌํด๋ฌ๋ ์ข๋ค.
โ 7. ํ ์คํธ์ฉ Access Token ๋ฐ๊ธ (์๋)
์ด๊ฑด Postman์ด๋ ๋ธ๋ผ์ฐ์ ๋ก ์๋ ๋ฐ๊ธํ ์ ์๋ค.
๐ 1๋จ๊ณ: ์ธ๊ฐ ์ฝ๋ ์์ฒญ (๋ธ๋ผ์ฐ์ ์์)
<https://kauth.kakao.com/oauth/authorize>?
response_type=code&
client_id={REST_API_KEY}&
redirect_uri=http://localhost:8080/oauth/kakao/callback&
scope=talk_message
→ ์ฌ์ฉ์ ๋์ํ๊ณ ๋๋ฉด? code=xxxxx ๋ถ์ URL๋ก ๋ฆฌ๋ค์ด๋ ํธ ๋๋ค.
→ ์ฌ๊ธฐ์ code ๊ฐ์ ๋ณต์ฌํ๋ค!
๐ 2๋จ๊ณ: Access Token ์์ฒญ (Postman ๋๋ CURL)
POST <https://kauth.kakao.com/oauth/token>
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&client_id={REST_API_KEY}
&redirect_uri=http://localhost:8080/oauth/kakao/callback
&code={์์์ ๋ฐ์ ์ธ๊ฐ์ฝ๋}
์๋ต:
{
"access_token": "โญ ์ฌ๊ธฐ์ ์๋ ๊ฐ์ yml์ ๋ฃ์ผ๋ฉด ๋จ โญ",
...
}
๐๏ธ ๋ด์ผ์ ๋ญ ํ์ง?!
โ๏ธ ์นด์นด์คํก ์๋ฆผ ๋ฉ์์ง ๋ง๋ฌด๋ฆฌํ๊ธฐ
โ๏ธ ๊ฒฐ์ -์ฌ๊ณ ๊ฐ์ ํ๋ฆ ์ด์ด์ฃผ๊ธฐ
โ๏ธ ๊ฐ๊ฒฉ ๋ณ๋ ๋ก์ง ์ค๊ณํ๊ธฐ
โ๏ธ TIL ์์ฑํ๊ธฐ