๐Ÿ–ฅ๏ธ ๋ญ”๊ฐ€๋ญ”๊ฐ€ํ”„๋กœ์ ํŠธ/โœ๏ธ TIL

[TIL] ํ”„๋กœ์ ํŠธ 23์ผ์ฐจ.. ์นด์นด์˜คํ†ก ์•Œ๋ฆผ ๊ตฌํ˜„ํ•˜๊ธฐ!!

carrot0911 2025. 6. 10. 22:13

๐ŸŒž ์˜ค๋Š˜์€ ์–ด๋–ค ํ•˜๋ฃจ์˜€์ง€..

๊ฐ€์žฅ ๋จผ์ € ๋ฐฐ์†ก ์ƒ์„ฑ ์‹คํŒจ ์‹œ ์žฌ๊ณ  ๋ณต์›์„ ์œ„ํ•œ ๋กœ์ง์„ ๊ตฌํ˜„ํ–ˆ๋‹ค.
์ฃผ๋ฌธ ์ƒ์„ฑ ํ† ํ”ฝ์— ๋“ค์–ด๊ฐ€ ์žˆ๋˜ ์žฌ๊ณ  ๊ฐ์†Œ ๋กœ์ง๊ณผ ํ•จ๊ป˜ ๋”ฐ๋กœ ์žฌ๊ณ  ๊ด€๋ จ ํ† ํ”ฝ์„ ์ƒ์„ฑํ•ด์„œ ๋ฉ”์‹œ์ง€ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ๊ฐ€ ๋  ์ˆ˜ ์žˆ๋„๋ก ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๋ ค๊ณ  ํ–ˆ๋‹ค.

// ์žฌ๊ณ  ๊ด€๋ จ 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

  1. ์นด์นด์˜ค ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ
  2. ์˜ค๋ฅธ์ชฝ ์ƒ๋‹จ ๋ฉ”๋‰ด์—์„œ ๋‚ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ → ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ถ”๊ฐ€

โœ… 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. ๋™์˜ ํ•ญ๋ชฉ ์„ค์ •

๐Ÿ“ ์ขŒ์ธก ๋ฉ”๋‰ด → ์นด์นด์˜ค ๋กœ๊ทธ์ธ > ๋™์˜ํ•ญ๋ชฉ ์„ค์ •

  1. ์นด์นด์˜คํ†ก ๋ฉ”์‹œ์ง€ ์ „์†ก(talk_message) ํ•ญ๋ชฉ์„ ์ฐพ๋Š”๋‹ค.
  2. "ํ•„์ˆ˜ ๋™์˜" ๋˜๋Š” "์„ ํƒ ๋™์˜"๋กœ 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 ์ž‘์„ฑํ•˜๊ธฐ