Listener

Record Listener

ํŠน์ง•

  • ๊ฐœ๋ณ„ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ: ๊ฐ ๋ฉ”์‹œ์ง€(๋ ˆ์ฝ”๋“œ)๋ฅผ ํ•˜๋‚˜์”ฉ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

  • ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„: ๊ธฐ๋ณธ์ ์ธ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ๊ฐ€์žฅ ์ผ๋ฐ˜์ ์ธ ๋ฆฌ์Šค๋„ˆ์ž…๋‹ˆ๋‹ค.

  • ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ: ๋ฉ”์‹œ์ง€๋ฅผ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ˆ˜์‹ ํ•˜๊ณ  ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๊ตฌํ˜„ ๋ฐฉ๋ฒ•

  • @KafkaListener ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฆฌ์Šค๋„ˆ ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

  • ๋ฉ”์„œ๋“œ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฉ”์‹œ์ง€์˜ ๊ฐ’ ๋˜๋Š” ConsumerRecord๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฝ”๋“œ ์ƒ˜ํ”Œ

import org.springframework.kafka.annotation.KafkaListener
import org.springframework.stereotype.Component

@Component
class RecordListener {

    @KafkaListener(topics = ["my-topic"], groupId = "my-group")
    fun listen(message: String) {
        println("Received message: $message")
        // ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ๋กœ์ง
    }
}

์˜ต์…˜

  • topics: ๋ฆฌ์Šค๋‹ํ•  ํ† ํ”ฝ์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.

  • groupId: ์ปจ์Šˆ๋จธ ๊ทธ๋ฃน ID๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

  • concurrency: ๋™์‹œ์— ์‹คํ–‰๋  ์Šค๋ ˆ๋“œ ์ˆ˜๋ฅผ ์„ค์ •ํ•˜์—ฌ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

  • errorHandler: ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ ์‚ฌ์šฉํ•  ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.


Batch Listener

ํŠน์ง•

  • ์—ฌ๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ผ๊ด„ ์ฒ˜๋ฆฌ: ํ•œ ๋ฒˆ์— ์—ฌ๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฆฌ์ŠคํŠธ๋กœ ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

  • ์„ฑ๋Šฅ ํ–ฅ์ƒ: ๋Œ€๋Ÿ‰์˜ ๋ฉ”์‹œ์ง€๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ๋ฐฐ์น˜ ์ž‘์—…์— ์ ํ•ฉ: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ผ๊ด„ ์ž…๋ ฅ ๋“ฑ ๋ฐฐ์น˜ ์ž‘์—…์— ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

๊ตฌํ˜„ ๋ฐฉ๋ฒ•

  • ConcurrentKafkaListenerContainerFactory์˜ isBatchListener๋ฅผ true๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

  • ๋ฆฌ์Šค๋„ˆ ๋ฉ”์„œ๋“œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ List<ConsumerRecord<K, V>> ๋˜๋Š” List<V>๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.

์ฝ”๋“œ ์ƒ˜ํ”Œ

์„ค์ • ํด๋ž˜์Šค

import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.kafka.common.serialization.StringDeserializer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory
import org.springframework.kafka.core.DefaultKafkaConsumerFactory

@Configuration
class KafkaConsumerConfig {

    @Bean
    fun consumerFactory(): DefaultKafkaConsumerFactory<String, String> {
        val props = mapOf(
            ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to "localhost:9092",
            ConsumerConfig.GROUP_ID_CONFIG to "my-group",
            ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java,
            ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java
        )
        return DefaultKafkaConsumerFactory(props)
    }

    @Bean
    fun batchFactory(): ConcurrentKafkaListenerContainerFactory<String, String> {
        val factory = ConcurrentKafkaListenerContainerFactory<String, String>()
        factory.consumerFactory = consumerFactory()
        factory.isBatchListener = true
        return factory
    }
}

๋ฐฐ์น˜ ๋ฆฌ์Šค๋„ˆ ๊ตฌํ˜„

import org.springframework.kafka.annotation.KafkaListener
import org.springframework.stereotype.Component

@Component
class BatchListener {

    @KafkaListener(
        topics = ["my-topic"],
        groupId = "my-group",
        containerFactory = "batchFactory"
    )
    fun listen(messages: List<String>) {
        println("Received batch messages: $messages")
        // ๋ฐฐ์น˜ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ๋กœ์ง
    }
}

์˜ต์…˜

  • batchSize: ํ•œ ๋ฒˆ์— ๊ฐ€์ ธ์˜ฌ ๋ฉ”์‹œ์ง€ ์ˆ˜๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

  • pollTimeout: ๋ฉ”์‹œ์ง€๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ ๋Œ€๊ธฐํ•  ์ตœ๋Œ€ ์‹œ๊ฐ„์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.

  • concurrency: ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋  ์ปจ์Šˆ๋จธ ์Šค๋ ˆ๋“œ ์ˆ˜๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.


Acknowledgment Mode Listener

ํŠน์ง•

  • ์ˆ˜๋™ ์˜คํ”„์…‹ ์ปค๋ฐ‹: ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๊ฐ€ ์™„๋ฃŒ๋œ ํ›„ ์ˆ˜๋™์œผ๋กœ ์˜คํ”„์…‹์„ ์ปค๋ฐ‹ํ•ฉ๋‹ˆ๋‹ค.

  • ์ •ํ™•ํ•œ ์ฒ˜๋ฆฌ ๋ณด์žฅ: ๋ฉ”์‹œ์ง€์˜ ์ค‘๋ณต ์ฒ˜๋ฆฌ๋‚˜ ์†์‹ค์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŠธ๋žœ์žญ์…˜๊ณผ ์—ฐ๊ณ„ํ•˜์—ฌ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๊ตฌํ˜„ ๋ฐฉ๋ฒ•

  • ๋ฆฌ์Šค๋„ˆ ๋ฉ”์„œ๋“œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ Acknowledgment ๊ฐ์ฒด๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค.

  • ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๊ฐ€ ์™„๋ฃŒ๋œ ํ›„ acknowledge() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์˜คํ”„์…‹์„ ์ปค๋ฐ‹ํ•ฉ๋‹ˆ๋‹ค.

์ฝ”๋“œ ์ƒ˜ํ”Œ

๋ฆฌ์Šค๋„ˆ ๊ตฌํ˜„

import org.springframework.kafka.annotation.KafkaListener
import org.springframework.kafka.support.Acknowledgment
import org.springframework.stereotype.Component

@Component
class AcknowledgmentListener {

    @KafkaListener(
        topics = ["my-topic"],
        groupId = "my-group",
        containerFactory = "kafkaManualAckListenerContainerFactory"
    )
    fun listen(message: String, ack: Acknowledgment) {
        try {
            println("Processing message: $message")
            // ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ๋กœ์ง
            ack.acknowledge() // ์˜คํ”„์…‹ ์ปค๋ฐ‹
        } catch (e: Exception) {
            println("Error processing message: ${e.message}")
            // ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋กœ์ง
        }
    }
}

์„ค์ • ํด๋ž˜์Šค

import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.kafka.common.serialization.StringDeserializer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory
import org.springframework.kafka.core.DefaultKafkaConsumerFactory
import org.springframework.kafka.listener.ContainerProperties

@Configuration
class KafkaConsumerConfig {

    @Bean
    fun consumerFactory(): DefaultKafkaConsumerFactory<String, String> {
        val props = mapOf(
            ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to "localhost:9092",
            ConsumerConfig.GROUP_ID_CONFIG to "my-group",
            ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java,
            ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java
        )
        return DefaultKafkaConsumerFactory(props)
    }

    @Bean
    fun kafkaManualAckListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory<String, String> {
        val factory = ConcurrentKafkaListenerContainerFactory<String, String>()
        factory.consumerFactory = consumerFactory()
        factory.containerProperties.ackMode = ContainerProperties.AckMode.MANUAL
        return factory
    }
}

์˜ต์…˜

  • ackMode: ์ˆ˜๋™ ์ปค๋ฐ‹ ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค (MANUAL, MANUAL_IMMEDIATE ๋“ฑ).

  • syncCommits: ์ปค๋ฐ‹์„ ๋™๊ธฐํ™”ํ• ์ง€ ์—ฌ๋ถ€๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

  • errorHandler: ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.


Consumer Aware Listener

ํŠน์ง•

  • Consumer ๊ฐ์ฒด ์ ‘๊ทผ: ๋ฆฌ์Šค๋„ˆ ๋ฉ”์„œ๋“œ์—์„œ Consumer ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„ ์ปจ์Šˆ๋จธ์— ๋Œ€ํ•œ ์ œ์–ด๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

  • ๋™์  ์ œ์–ด: ๋Ÿฐํƒ€์ž„์— ์ปจ์Šˆ๋จธ ์„ค์ • ๋ณ€๊ฒฝ์ด๋‚˜ ํŒŒํ‹ฐ์…˜ ํ• ๋‹น ๋“ฑ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

  • ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ํ™œ์šฉ: ๋‚ฎ์€ ์ˆ˜์ค€์˜ Kafka API๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ตฌํ˜„ ๋ฐฉ๋ฒ•

  • ๋ฆฌ์Šค๋„ˆ ๋ฉ”์„œ๋“œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ Consumer ๊ฐ์ฒด๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค.

  • Consumer API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ปจ์Šˆ๋จธ๋ฅผ ์ œ์–ดํ•ฉ๋‹ˆ๋‹ค.

์ฝ”๋“œ ์ƒ˜ํ”Œ

import org.apache.kafka.clients.consumer.Consumer
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.springframework.kafka.annotation.KafkaListener
import org.springframework.stereotype.Component

@Component
class ConsumerAwareListener {

    @KafkaListener(topics = ["my-topic"], groupId = "my-group")
    fun listen(record: ConsumerRecord<String, String>, consumer: Consumer<String, String>) {
        println("Received message: ${record.value()} from partition: ${record.partition()}")
        // Consumer ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•œ ์ถ”๊ฐ€ ์ œ์–ด ๋กœ์ง
    }
}

์˜ต์…˜

  • pollTimeout: ๋ฉ”์‹œ์ง€๋ฅผ ํด๋งํ•  ๋•Œ ์‚ฌ์šฉํ•  ํƒ€์ž„์•„์›ƒ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

  • listenerType: ๋ฆฌ์Šค๋„ˆ์˜ ํƒ€์ž…์„ ์ง€์ •ํ•˜์—ฌ ์›ํ•˜๋Š” ํ˜•ํƒœ์˜ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • concurrency: ์ปจ์Šˆ๋จธ์˜ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์Šค๋ ˆ๋“œ ์ˆ˜๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.


Message Listener Container

ํŠน์ง•

  • ์ปค์Šคํ…€ ๋ฆฌ์Šค๋„ˆ ๊ตฌํ˜„: ์–ด๋…ธํ…Œ์ด์…˜ ์—†์ด ์ง์ ‘ ๋ฆฌ์Šค๋„ˆ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

  • ๊ณ ๊ธ‰ ์„ค์ • ์ง€์›: ์„ธ๋ถ€์ ์ธ ์„ค์ •๊ณผ ์ œ์–ด๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

  • ์œ ์—ฐ์„ฑ: ๋ณต์žกํ•œ ์š”๊ตฌ ์‚ฌํ•ญ์ด๋‚˜ ํŠน์ˆ˜ํ•œ ์ผ€์ด์Šค์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

๊ตฌํ˜„ ๋ฐฉ๋ฒ•

  • MessageListenerContainer๋ฅผ ์ง์ ‘ ์ƒ์„ฑํ•˜๊ณ  ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

  • ContainerProperties๋ฅผ ํ†ตํ•ด ํ•„์š”ํ•œ ์„ค์ •์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.

์ฝ”๋“œ ์ƒ˜ํ”Œ

import org.apache.kafka.clients.consumer.ConsumerRecord
import org.springframework.kafka.core.ConsumerFactory
import org.springframework.kafka.listener.MessageListener
import org.springframework.kafka.listener.KafkaMessageListenerContainer
import org.springframework.kafka.listener.ContainerProperties
import org.springframework.stereotype.Component

@Component
class CustomListenerContainer(
    consumerFactory: ConsumerFactory<String, String>
) {

    init {
        val containerProps = ContainerProperties("my-topic")
        containerProps.messageListener = MessageListener<String, String> { record: ConsumerRecord<String, String> ->
            println("Received message: ${record.value()}")
            // ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ๋กœ์ง
        }
        val container = KafkaMessageListenerContainer(consumerFactory, containerProps)
        container.start()
    }
}

์˜ต์…˜

  • ContainerProperties: ์˜คํ”„์…‹ ๊ด€๋ฆฌ, ์—๋Ÿฌ ์ฒ˜๋ฆฌ, ์Šค๋ ˆ๋“œ ์ˆ˜ ๋“ฑ ๋‹ค์–‘ํ•œ ์„ค์ •์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

  • setAutoStartup: ์ปจํ…Œ์ด๋„ˆ์˜ ์ž๋™ ์‹œ์ž‘ ์—ฌ๋ถ€๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

  • setConcurrency: ์ปจ์Šˆ๋จธ ์Šค๋ ˆ๋“œ ์ˆ˜๋ฅผ ์ง€์ •ํ•˜์—ฌ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.


์ถ”๊ฐ€ ์˜ต์…˜ ๋ฐ ๊ณ ๋ ค ์‚ฌํ•ญ

  • ์—๋Ÿฌ ์ฒ˜๋ฆฌ (Error Handling)

    • errorHandler ๋˜๋Š” seekToCurrentErrorHandler๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ ์žฌ์‹œ๋„ ๋กœ์ง์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

    • ๋ฐ๋“œ ๋ ˆํ„ฐ ํ(DLQ)๋ฅผ ์„ค์ •ํ•˜์—ฌ ์ฒ˜๋ฆฌ ์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณ„๋„์˜ ํ† ํ”ฝ์— ์ €์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ๋ฐ ์„ฑ๋Šฅ ํŠœ๋‹

    • concurrency: ๋ฆฌ์Šค๋„ˆ ์ปจํ…Œ์ด๋„ˆ ํŒฉํ† ๋ฆฌ์—์„œ ์„ค์ •ํ•˜์—ฌ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ์Šค๋ ˆ๋“œ ์ˆ˜๋ฅผ ๋Š˜๋ฆฝ๋‹ˆ๋‹ค.

    • ํŒŒํ‹ฐ์…˜ ์ˆ˜ ์ฆ๊ฐ€: ํ† ํ”ฝ์˜ ํŒŒํ‹ฐ์…˜ ์ˆ˜๋ฅผ ๋Š˜๋ ค ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

  • ๋ณด์•ˆ ์„ค์ •

    • SSL/TLS ์•”ํ˜ธํ™”: ssl.keystore.location ๋“ฑ์˜ ์„ค์ •์„ ํ†ตํ•ด ํ†ต์‹ ์„ ์•”ํ˜ธํ™”ํ•ฉ๋‹ˆ๋‹ค.

    • ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ: SASL ๋˜๋Š” OAuth๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ธ์ฆ๊ณผ ๊ถŒํ•œ ๊ด€๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

  • ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ

    • ChainedKafkaTransactionManager๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Kafka์™€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฐ„์˜ ํŠธ๋žœ์žญ์…˜์„ ํ†ตํ•ฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋กœ๊น…

    • ์Šคํ”„๋ง ์•ก์ถ”์—์ดํ„ฐ(Actuator)์™€ ํ†ตํ•ฉํ•˜์—ฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ƒํƒœ๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค.

    • Kafka์˜ JMX ๋ฉ”ํŠธ๋ฆญ์„ ํ™œ์šฉํ•˜์—ฌ ์„ฑ๋Šฅ ๋ฐ ์ƒํƒœ๋ฅผ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.

Last updated

Was this helpful?