๐ ์ค๋์ ์ด๋ค ํ๋ฃจ์์ง..
์ค๋์ ํ์๋ถ์ด ํ๋ฆฌํ ์ฌ๋ฆฌ๊ณ merge ๋ ์ฝ๋๋ฅผ pull ๋ฐ์์ ๋ด ์ฝ๋๋ฅผ ์ฎ๊ธฐ๋ ์์ ์ ํ๋ค.
์ง๊ธ๊น์ง๋ ๋ด๊ฐ ๊ฐ์ธ์ ์ผ๋ก ํ
์คํธ Controller๋ฅผ ๋ง๋ค๊ณ ๊ฑฐ๊ธฐ์ ์์
์ ์งํํ๊ณ ์์๋ค. ํ์ง๋ง ์ด์ ๋ ํ์์ ์ฝ๋์ ํฉ์ณ์ผ ํ๋ ๋ด ์ฝ๋๊ฐ ์ ๋์ํ ์ ์๋๋ก ์ฎ๊ธฐ๋ ์์
์ ํ๋ค!
์๋ฒฝํ๊ฒ ๋ค ์ฎ๊ธฐ๋ ค๊ณ ํ์ง๋ง ๊ฒฐ์ ํํธ์์ ๋์ด์ค๋ DTO๋ฅผ ์์ํ ์ ์์ด์ ๋ก์ง์ ๋ค ์์ฑํ์ง ๋ชปํ๊ณ ์ฐ์ ์ ํ๋ง ์ก์๋๊ณ ํ์์ ์์๋๋ก ๋๋จธ์ง ์ฝ๋๋ฅผ ์์ฑํ๋ค.
case INVENTORY_DECREASE -> {
// todo ๊ฒฐ์ ํํธ์์ ์ ๋ฌ๋๋ payload ๊ตฌ์กฐ ํ์ธ ํ DTO ์ ์
// log.info("๐ฅ Kafka ์ฌ๊ณ ๊ฐ์ ๋ฉ์์ง ์์ : {}", message);
// ์ฌ๊ณ ๊ฐ์ ๋ก์ง ์งํ
// productOptionCombinationService.decreaseProductOptionCombinationInventory(payload.orderId());
// log.info("๐ ์ฌ๊ณ ๊ฐ์ ์ฑ๊ณต! ๋ชจ๋ ์ํ์ ์ฌ๊ณ ์ฐจ๊ฐ์ด ์ ์์ ์ผ๋ก ์ฒ๋ฆฌ๋์์ต๋๋ค.");
// todo ๋ฐฐ์ก ์์ฑ ๋ฉ์์ง ๋ฐํ
// kafkaTemplate.send(deliveryTopic, request);
// log.info("\uD83D\uDCE4 Kafka ๋ฐฐ์ก ์์ฑ ๋ฉ์์ง ์ ์ก: {}", request);
}
@Slf4j
@Component
@RequiredArgsConstructor
public class DeliveryKafkaListener {
private final DeliveryService deliveryService;
@KafkaListener(topics = "${kafka.topic.delivery}", groupId = "${spring.kafka.consumer.delivery.group-id}")
public void listenDeliveryCreate(@Header(KafkaHeaders.RECEIVED_KEY) String key, String value) {
// ๋ฉ์์ง ์ ์ฒด๋ฅผ OperationWrapperDto๋ก ํ์ฑ
OperationWrapperDto wrapperDto = JsonHelper.fromJson(value, OperationWrapperDto.class);
// payload ํ๋ ์ถ์ถ
String payload = wrapperDto.payload();
// payload๋ฅผ CreateDeliveryRequest๋ก ๋ค์ ํ์ฑ
// todo CreateDeliveryRequest ๋์ WrapperRequest๋ก ๋ค์ ์์ฑ ํ์
CreateDeliveryRequest request = JsonHelper.fromJson(payload, CreateDeliveryRequest.class);
log.info("๐ฅ Kafka ๋ฐฐ์ก ์์ฑ ๋ฉ์์ง ์์ : {}", request);
deliveryService.createDelivery(request);
log.info("๐ ๋ฐฐ์ก ์์ฑ ์ฑ๊ณต! ๋ฐฐ์ก ์์ฑ์ด ์ ์์ ์ผ๋ก ์ฒ๋ฆฌ๋์์ต๋๋ค.");
}
}
๊ทธ๋ค์์ ํ๋ฆฌํ ํผ๋๋ฐฑ๋ฐ์ ๋ถ๋ถ์ ์์ ํ๋ค!
KafkaProducerConfig์์ ProducerFactory<String, KafkaMessage<InventoryDecreasePayload>>๋ก ์ฌ์ฉํ๊ณ ์์๋๋ฐ, ProducerFactory<String, Object>๋ก ์ฌ์ฉํ ์ ์์ด์ ํ๋๋ก ํต์ผํด ์ฃผ์๋ค.
๋ ์ค๋ณต์ผ๋ก ์ฌ์ฉ๋๋ ๋ถ๋ถ์ ๋ฉ์๋๋ก ์ถ์ถํ๋ค.
Map<String, Object> configProps = new HashMap<>();
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
์ด ๋ถ๋ถ์ด ๊ณ์ ๊ฒน์ณ์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๋ฉ์๋๋ก ์ถ์ถํด์ ๊ด๋ฆฌํด ์ค๋ ๊ด์ฐฎ์ ๊ฑฐ ๊ฐ๋ค๊ณ ํด์ ๋ฐ๋ก ๋ฉ์๋๋ก ์ถ์ถํ๋ค.
๊ทธ๋ฆฌ๊ณ ๋น๋๊ธฐ ์ฒ๋ฆฌ ํ๋ฆ์ ์ข ๋ ๊ณ ๋ฏผํ๋ ์๊ฐ์ ๊ฐ์ก๋ค.
์ง๊ธ์ Kafka๋ฅผ ์ฌ์ฉํ๊ณ ์์ง๋ง ๋ญ๊ฐ ๋๊ธฐ ๋๋์ด ์์ด์ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ๋ ๊ฒ์ด ๋ ํจ์จ์ ์ผ์ง ๊ณ ๋ฏผํด ๋ณด๊ณ , ๊ทธ๊ฒ๋ ๋จธ๋ฆฌ์์ ์ ์กํ์ง ์์์ Canva์์ ์ง๊ธ ๊ตฌ์กฐ๋ฅผ ๊ทธ๋ ค๊ฐ๋ฉด์ ์ ๋ฆฌํ๊ณ ํ๋ฆ์ ์ก์๊ฐ๋ค.
๊ทธ ๊ฒฐ๊ณผ ์ฌ๋ ์๋ฆผ ๋ฉ์์ง๋ฅผ ํตํด ์์ธ ์ํฉ์ ์๋ ค์ฃผ๋ฉด ์ข์ ๊ฒ ๊ฐ๋ค๋ ์์ด๋์ด๊ฐ ๋ ์ฌ๋ผ์ ์ฌ๋ ์๋ฆผ์ ๋ํด ์ฐพ์๋ณด๊ณ ํ๋ฃจ๋ฅผ ๋ง๋ฌด๋ฆฌํ๋ค.
๐ก ์๋กญ๊ฒ ์๊ฒ ๋ ๋ด์ฉ์ ๋ญ๊ฐ ์๋๋ผ..?!
โ ์นํ (Webhook)์ด๋?
์ด๋ฒคํธ ๊ธฐ๋ฐ ์๋ HTTP ์์ฒญ ์์คํ
์นํ
์ ๋ง ๊ทธ๋๋ก "์น(Web)์ ๊ฐ๊ณ ๋ฆฌ(Hook)๋ฅผ ๊ฑธ์ด๋๋ค"๋ ์๋ฏธ์ด๋ค.
์ฆ, ํน์ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ์ ๋, ์ค์ ํด ๋ URL๋ก ์๋์ผ๋ก HTTP ์์ฒญ์ ๋ณด๋ด๋ ๊ฒ์ด ์นํ
์ด๋ค.
โ ์ ์ฌ์ฉํ๋ ๊ฑธ๊น?
โ ๊ธฐ์กด ๋ฐฉ์
- ์ผ์ ์ฃผ๊ธฐ๋ง๋ค ์๋ฒ๊ฐ ๊ณ์ ๋ฌผ์ด๋ด์ผ ํด.
- ์: "์ ์ฃผ๋ฌธ ์๊ฒผ์ด?", "์ ๋ฐ์ดํธ ๋์ด?"๋ฅผ 5์ด๋ง๋ค ๊ณ์ ์์ฒญ
โ ์นํ ๋ฐฉ์
- ์ด๋ฒคํธ๊ฐ ์ค์ ๋ก ๋ฐ์ํ์ ๋๋ง ์์ฒญ์ ๋ณด๋
- ์๋ฒ ๋ถํ ๊ฐ์, ๋น ๋ฅธ ์๋ต, ์ค์๊ฐ ์ฒ๋ฆฌ์ ๋งค์ฐ ์ ๋ฆฌ
โ ์นํ ๋์ ๊ตฌ์กฐ
[์ด๋ค ์๋น์ค] --- ์ด๋ฒคํธ ๋ฐ์ ---> [๋์ ์๋ฒ๋ก HTTP POST ์์ฒญ]
์์: GitHub ์นํ
- GitHub ์ ์ฅ์์์ Push ๋ฐ์ → ๋ฑ๋กํ ์๋ฒ์ /github/webhook์ ์๋์ผ๋ก POST ์ ์ก
์์: Slack ์นํ
- ๋์ ์๋ฒ์์ ๋ฐฐ์ก ์คํจ ๋ฐ์ → Slack์ด ๋ฐ๊ธํ Webhook URL๋ก POST → ์๋ฆผ ์ ์ก
โ ์นํ vs API
๊ตฌ๋ถ | Webhook | API |
์๋ ๋ฐฉ์ | ์ด๋ฒคํธ ๊ธฐ๋ฐ ์๋ ์ ์ก | ํด๋ผ์ด์ธํธ๊ฐ ์์ฒญํด์ผ ์๋ต |
๋ฐ์ดํฐ ํ๋ฆ | ์๋ฒ โ ํด๋ผ์ด์ธํธ | ํด๋ผ์ด์ธํธ โ ์๋ฒ |
์์ | "๋ฐฐ์ก ์คํจ" ์๋ฆผ ์๋ ์ ์ก | ํ๋ก ํธ์์ /delivery API ํธ์ถ |
โ ์นํ ์ด ์ฌ์ฉํ๋ ๊ธฐ์
- ๋๋ถ๋ถ HTTP POST ์์ฒญ
- ๋ณธ๋ฌธ์ ์ผ๋ฐ์ ์ผ๋ก JSON
- ํค๋์ ์ธ์ฆ ํ ํฐ์ด๋ ์๊ทธ๋์ฒ๋ฅผ ํฌํจํด์ ๋ณด์ ์ ์ง
POST /webhook HTTP/1.1
Content-Type: application/json
{
"event": "DELIVERY_FAILED",
"orderId": 12345,
"reason": "์ฌ๊ณ ์์"
}
โ Slack Webhook ์์
Slack์ Incoming Webhook ๊ธฐ๋ฅ์ ์ ๊ณตํด.
- ์ฌ์ฉ์๊ฐ ๋ง๋ ๋ฉ์์ง๋ฅผ Slack ์ฑ๋๋ก ์๋ ์ ์ก ๊ฐ๋ฅ
- Slack์ด ๋ฐ๊ธํ๋ Webhook URL๋ก ๋ฉ์์ง๋ฅผ POST ํ๋ฉด ์ฑ๋์ ๋ธ
POST <https://hooks.slack.com/services/T000/B000/abcxyz>
Content-Type: application/json
{
"text": "โ ๋ฐฐ์ก ์คํจ: ์ฃผ๋ฌธ ๋ฒํธ 12345"
}
โ ์นํ ๊ตฌ์ฑ ์์
๊ตฌ์ฑ ์์ | ์ค๋ช |
์ด๋ฒคํธ | ํธ๋ฆฌ๊ฑฐ๊ฐ ๋๋ ํ์ (์: ์ฃผ๋ฌธ ์คํจ, GitHub push ๋ฑ) |
Webhook URL | ์ด๋ฒคํธ ๋ฐ์ ์ ํธ์ถํ ์ธ๋ถ ์ฃผ์ (๋ฐ๋ ์ชฝ ์๋ฒ ์ฃผ์) |
Payload | ํจ๊ป ์ ์กํ ๋ฐ์ดํฐ (JSON ๋ฑ) |
Header | ๋ณดํต Content-Type์ด๋ ์ธ์ฆ ํ ํฐ ํฌํจ |
โ ์นํ ์ ์ฌ์ฉํ๋ ๋ํ์ ์ธ ์๋น์ค๋ค
์๋น์ค | Webhook ์ฌ์ฉ ์ฌ๋ก |
GitHub | ์ฝ๋ Push, PR, Issue ์์ฑ ์ ์๋ฆผ ์ ์ก |
Slack | ์ธ๋ถ ์ฑ์ด Slack ์ฑ๋์ ๋ฉ์์ง ์ ์ก |
Stripe | ๊ฒฐ์ ์ฑ๊ณต/์คํจ ์ ๋์ ์๋ฒ๋ก ์๋ฆผ |
KakaoPay, Toss | ๊ฒฐ์ ์น์ธ/์ทจ์ ์ด๋ฒคํธ ์ ์ก |
Notion | ํ์ด์ง ๋ณ๊ฒฝ ์ด๋ฒคํธ ์ ์ก (๋ฒ ํ) |
โ ์ค๋ฌด์์์ ์ฌ์ฉ ์์ (Spring ๊ธฐ์ค)
@RestController
@RequestMapping("/webhook")
public class SlackWebhookReceiver {
@PostMapping
public ResponseEntity<Void> receiveWebhook(@RequestBody WebhookPayload payload) {
System.out.println("๐ฉ ๋ฐ์ ์นํ
: " + payload);
return ResponseEntity.ok().build();
}
}
๋๋ ์๋ฆผ์ ๋ณด๋ด๋ ์ญํ ์ด๋ผ๋ฉด:
restTemplate.postForEntity("<https://hooks.slack.com/>...", request, String.class);
โ ์ฃผ์ํ ์ (๋ณด์)
- Webhook URL์ ๋ ธ์ถ๋๋ฉด ๋๊ตฌ๋ ๋ฉ์์ง๋ฅผ ๋ณด๋ผ ์ ์์ผ๋ฏ๋ก ๋น๊ณต๊ฐ ์ ์งํด์ผ ํจ
- Slack์ด๋ GitHub ๋ฑ์ ๋ณดํต ์๋ช (signature)์ ํจ๊ป ๋ณด๋ด์ ์์ฒญ์ ์ง์๋ฅผ ๊ฒ์ฆํ ์ ์์
โ ํ ๋ฌธ์ฅ ์์ฝ
์นํ ์ "์ด๋ฒคํธ๊ฐ ๋ฐ์ํ์ ๋ ์๋์ผ๋ก ์๋ฆผ์ ์ธ๋ถ ์์คํ ์ ๋ณด๋ด์ฃผ๋ ๊ธฐ์ "์ด์ผ.
๐๏ธ ๋ด์ผ์ ๋ญ ํ์ง?!
โ๏ธ Slack ์๋ฆผ ๊ตฌํํ๊ธฐ
โ๏ธ TIL ์์ฑํ๊ธฐ