Kafka
- Spring ์์ Kafka๋ฅผ ์ฌ์ฉํ๋ ค ํ๋ค ๋ณด๋, ๊ด๋ จ ์ค์ ๋ค์ ํด์ฃผ์ด์ผ ํ๋ค. ํด๋น ์ค์ ๋ค์ ๋ํด์ ์ดํด๋ณด๋๋ก ํ๊ฒ ๋ค!
ConsumerConfig
package com.hanium.diarist.common.config;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.listener.ContainerProperties;
import org.springframework.kafka.support.serializer.ErrorHandlingDeserializer;
import org.springframework.kafka.support.serializer.JsonDeserializer;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableKafka
public class KafkaConsumerConfig {
@Value("${kafka.bootstrap-servers}")
private String bootstrapServers;
@Bean
public ConsumerFactory<String, Object> consumerFactory() {
Map<String, Object> config = new HashMap<>();
config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
config.put(ConsumerConfig.GROUP_ID_CONFIG, "diary");
config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
config.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, JsonDeserializer.class.getName());
config.put(JsonDeserializer.TRUSTED_PACKAGES, "*");
config.put(JsonDeserializer.VALUE_DEFAULT_TYPE, "java.util.HashMap");
return new DefaultKafkaConsumerFactory<>(config, new StringDeserializer(),
new ErrorHandlingDeserializer<>(new JsonDeserializer<>()));
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, Object> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, Object> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
return factory;
}
}
- Consumer ์ ๋ํ ์ค์ ์ map์ ํตํด ํ ์ ์์๋ค. ๊ทธ๋ฃนID, serializer ๋ฐฉ์, ๋ฑ๋ฑ์ ์ค์ ์ ConsumerFactory๋ฅผ ํตํด ํ์๋ค.
ProducerConfig
package com.hanium.diarist.common.config;
import com.hanium.diarist.domain.diary.exception.KafkaConnectException;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import org.springframework.kafka.support.ProducerListener;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
@Configuration
public class KafkaProducerConfig {
@Value("${kafka.bootstrap-servers}")
private String bootstrapServers;
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
return new DefaultKafkaProducerFactory<>(configProps);
}
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
KafkaTemplate<String, String> kafkaTemplate = new KafkaTemplate<>(producerFactory());
kafkaTemplate.setProducerListener(new ProducerListener<String, String>() {
@Override
public void onSuccess(ProducerRecord<String, String> producerRecord, RecordMetadata recordMetadata) {
ProducerListener.super.onSuccess(producerRecord, recordMetadata);
}
@Override
public void onError(ProducerRecord<String, String> producerRecord, RecordMetadata recordMetadata, Exception exception) {
if (exception instanceof TimeoutException) {
throw new KafkaConnectException();
}
}
});
return kafkaTemplate;
}
}
- Producer ์ ๋ํ ์ค์ ์ผ๋ก, bootstrap-server, ์ง๋ ฌํ ๋ฐฉ์๋ฑ์ ๋ํ ์ค์ ์ ํด์ฃผ์๋ค.
- ์ดํ KafkaTemplate๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ผ ํ ํ๋ฆฟ์ ์ค์ ํ์๋ค.
ProducerService
package com.hanium.diarist.domain.diary.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hanium.diarist.domain.diary.domain.Diary;
import com.hanium.diarist.domain.diary.dto.CreateDiaryRequest;
import com.hanium.diarist.domain.diary.exception.JsonProcessException;
import com.hanium.diarist.domain.diary.exception.KafkaConnectException;
import com.hanium.diarist.domain.diary.repository.DiaryRepository;
import com.hanium.diarist.domain.user.domain.User;
import com.hanium.diarist.domain.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.core.KafkaProducerException;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;
@Service
@Slf4j
@RequiredArgsConstructor
public class CreateDiaryProducerService {
private final KafkaTemplate<String,String> kafkaTemplate;
private final ObjectMapper objectMapper;
private final DiaryRepository diaryRepository;
private final UserRepository userRepository;
private final String CreateTopicName = "create-diary";
private final String reCreateTopicName = "re-create-diary";
public boolean sendCreateDiaryMessageWithAd(CreateDiaryRequest createDiaryRequest) {
LocalDate date = createDiaryRequest.getDiaryDate();// ์ผ๊ธฐ ์์ฑ ๋ ์ง ๊ฐ์ ธ์ด
User user = userRepository.findById(createDiaryRequest.getUserId()).orElseThrow();// ํด๋น ์ ์ ์ฐพ์
Optional<Diary> existingDiary = diaryRepository.findByUserAndDiaryDate(user, date);// ํด๋น ์ ์ ์ ์ผ๊ธฐ ์์ฑ ๋ ์ง์ ์ผ๊ธฐ๊ฐ ์๋์ง ํ์ธ
try{
String message = objectMapper.writeValueAsString(createDiaryRequest);
if (existingDiary.isPresent()|| !date.isEqual(LocalDate.now())) { // ๋น์ ๋ ์ง์ ์ผ๊ธฐ๊ฐ ์๊ฑฐ๋, ๊ณผ๊ฑฐ์ ์ผ๊ธฐ๋ฅผ ์์ฑํ ๊ฒฝ์ฐ
// ๊ด๊ณ ์์ฒญ ํ์
sendKafkaMessage(reCreateTopicName, message);
watchAd();
return true;
}else{// ๊ณผ๊ฑฐ ๋ ์ง ์ผ๊ธฐ ์์ฑ
sendKafkaMessage(CreateTopicName, message);
return false;
}
} catch (KafkaProducerException e){
throw new KafkaConnectException();
} catch (JsonProcessingException e) {
throw new JsonProcessException();
}
}
private void sendKafkaMessage(String topic,String message) {
CompletableFuture<SendResult<String, String>> future = kafkaTemplate.send(topic, message);
future.thenAccept(result -> {
// ์ฑ๊ณต ์ ๋ก์ง
log.info("Message sent successfully to topic {} with offset {}", result.getRecordMetadata().topic(), result.getRecordMetadata().offset());
}).exceptionally(ex -> {
// ์คํจ ์ ์์ธ ์ฒ๋ฆฌ
if (ex.getCause() instanceof TimeoutException) {
throw new KafkaConnectException();
}
return null;
});
}
@Async
public void watchAd() {
System.out.println("๊ด๊ณ ์์ฒญ ์ค");
}
}
- ๋ ์ง๋ฅผ ์์ฑํ๋ ์กฐ๊ฑด ( ์ค๋ ์ฒซ ์์ฑ, ์ค๋ ์ฌ ์์ฑ, ๊ณผ๊ฑฐ ์ผ๊ธฐ ์์ฑ) ์ ๋ฐ๋ผ ๋ก์ง์ด ๋ฌ๋ผ์ง๊ธฐ ๋๋ฌธ์ ํ ํฝ์ ๋๋์ด์ฃผ์๋ค.
- ์ผ๊ธฐ๋ฅผ ๋ณด๋ผ ๊ฒฝ์ฐ ์นดํ์นด ๋ธ๋ก์ปค์ ๋ค์ด๊ฐ๋ ๊ฒ์ ํ์ธํ ์ ์์๋ค!

ConsumerService
package com.hanium.diarist.domain.diary.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hanium.diarist.domain.diary.domain.Diary;
import com.hanium.diarist.domain.diary.dto.CreateDiaryResponse;
import com.hanium.diarist.domain.diary.exception.DiaryIdInvalidException;
import com.hanium.diarist.domain.diary.exception.DiaryNotFoundException;
import com.hanium.diarist.domain.diary.exception.JsonProcessException;
import com.hanium.diarist.domain.diary.exception.SseException;
import com.hanium.diarist.domain.diary.repository.DiaryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.HashMap;
import java.util.concurrent.CopyOnWriteArrayList;
@Service
@RequiredArgsConstructor
public class CreateDiaryConsumerService {
private final DiaryRepository diaryRepository;
private final ObjectMapper objectMapper;
private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>();// ํด๋ผ์ด์ธํธ์ sse ์ฐ๊ฒฐ์ ๊ด๋ฆฌํ๋ ๋ฆฌ์คํธ
// ํด๋ผ์ด์ธํธ๊ฐ sse ์ฐ๊ฒฐ์ ์์ฒญํ ๋ ์๋ก์ด sseEmitter๋ฅผ ์์ฑํด ๋ฐํํจ
public SseEmitter addEmitter() {
SseEmitter emitter = new SseEmitter(60000L);// ํ์์์ ์๊ฐ 60์ด
this.emitters.add(emitter);
emitter.onCompletion(() -> this.emitters.remove(emitter));// ์ฐ๊ฒฐ ์๋ฃ์ ๋ฆฌ์คํธ์์ ์ ๊ฑฐ
emitter.onTimeout(() -> this.emitters.remove(emitter));// ํ์์์์ ๋ฆฌ์คํธ์์ ์ ๊ฑฐ
emitter.onError(e -> this.emitters.remove(emitter));// ์๋ฌ์ ๋ฆฌ์คํธ์์ ์ ๊ฑฐ
return emitter;
}
// ๋ชจ๋ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฉ์ธ์ง๋ฅผ ์ ์กํจ.
private void sendEmitters(CreateDiaryResponse diaryResponse){
for (SseEmitter emitter : emitters) {
try {
String jsonResponse = objectMapper.writeValueAsString(diaryResponse);
//System.out.println("Sending to emitter: " + jsonResponse);
emitter.send(SseEmitter.event().data(jsonResponse));
}catch (JsonProcessingException e) {
throw new JsonProcessException();
} catch (IOException e) {
this.emitters.remove(emitter);
throw new SseException();
}
}
}
// kafka ์ create-diary-response ํ ํฝ์์ ๋ฉ์ธ์ง๋ฅผ ์์ ํด์ ์ฒ๋ฆฌํจ
@KafkaListener(topics = "create-diary-response", groupId = "diary")
public void consumeCreateDiaryResponse(HashMap<String,Integer> message) {
try {
long diaryId = Long.parseLong(String.valueOf(message.get("diaryId")));
Diary diary = diaryRepository.findByDiaryIdWithDetails(diaryId).orElseThrow(() -> new DiaryNotFoundException());
CreateDiaryResponse createDiaryResponse = new CreateDiaryResponse(diary.getEmotion().getEmotionName(), diary.getEmotion().getEmotionPicture(), diary.getContent(), diary.getArtist().getArtistName(), diary.getArtist().getArtistPicture(), diary.getDiaryDate(), diary.getImage().getImageUrl());
// System.out.println(createDiaryResponse);
sendEmitters(createDiaryResponse);
} catch (NumberFormatException e) {
throw new DiaryIdInvalidException();
}
}
}
- ์ปจ์๋จธ๋ฅผ ๊ตฌํํ ๋ ํ๊ฐ์ง ์๊ฐํด๋ณด์ผ ํ ์ผ์ด ์์๋ค.
graph LR
a[app] --> b[server]
b --> c([kafka])
c--> d[model]
d-->c
c-->b
- ๋ค์๊ณผ ๊ฐ์ด appโ ์๋ฒ๋ก ์์ฒญ, ์๋ฒ์์ ์นดํ์นด , ๋ชจ๋ธ์์ ์นดํ์นด์ ๋ฉ์ธ์ง๋ฅผ ๊ตฌ๋ ํด์ ์ฝ์ด์จ๋ค.
- ์ดํ ๋ชจ๋ธ์์ ์นดํ์นด๋ก ๋ฉ์ธ์งํ๋ฅผ ๋ณด๋ด๊ณ ์๋ฒ๊ฐ ํด๋น ๋ฉ์ธ์ง๋ฅผ ๊ตฌ๋ ์ ํ๋ค๋ ๊ฐ๋ ๊น์ง๋ ๊ฐ๋ฅํ์๋ค.
<aside> ๐ก ๊ทธ๋ ๋ค๋ฉด ์ด๋ป๊ฒ ์๋ฒ์์ app์ผ๋ก ์ผ๊ธฐ๊ฐ ์์ฑ ์๋ฃ๋์๋ค๋ ๊ฒ์ ์๋ ค์ค ์ ์์๊น?
</aside>
- ์ด ๋ถ๋ถ์ ๋ํด์ ์๊ฐํด๋ณผ ํ์๊ฐ ์์๋ค.
โ ์๊ฐํ ์ ์๋ ๋ฐฉ๋ฒ์ ๋ช๊ฐ์ง๊ฐ ์์๋ค
1. ์น์์ผ (WebSocket)
- ์ฅ์ :
- ์๋ฐฉํฅ ํต์ : ํด๋ผ์ด์ธํธ์ ์๋ฒ๊ฐ ์ง์์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์ ์ ์์ต๋๋ค.
- ์ ์ง์ฐ: ์ค์๊ฐ ํต์ ์ ์ ํฉํ๋ฉฐ, ์ฆ๊ฐ์ ์ธ ์ํธ์์ฉ์ด ํ์ํ ๋ ์ ์ฉํฉ๋๋ค.
- ํ์ค ํ๋กํ ์ฝ๋ก ๋๋ฆฌ ์ง์๋ฉ๋๋ค.
- ๋จ์ :
- ์ฐ๊ฒฐ ์ ์ง ๊ด๋ฆฌ: ์๋ฒ๋ ๊ฐ ํด๋ผ์ด์ธํธ์ ์ง์์ ์ผ๋ก ์ฐ๊ฒฐ์ ์ ์งํด์ผ ํ๋ฉฐ, ์ด๋ ๋ฆฌ์์ค๋ฅผ ์๋นํ ์ฌ์ฉํ ์ ์์ต๋๋ค.
- ์ค์ผ์ผ๋ง ๋ฌธ์ : ๋ง์ ์์ ๋์ ์ฐ๊ฒฐ์ ๊ด๋ฆฌํ๊ธฐ ์ํด์๋ ์ถ๊ฐ ์ธํ๋ผ์คํธ๋ญ์ฒ์ ๋ณต์ก์ฑ์ด ์๊ตฌ๋ฉ๋๋ค.
2. ์๋ฒ-์ ์ก ์ด๋ฒคํธ (SSE)
- ์ฅ์ :
- ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก์ ๋จ๋ฐฉํฅ ํต์ : ์๋ฒ๊ฐ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฐ์ดํฐ๋ฅผ ํธ์ํ ์ ์์ต๋๋ค.
- ์ ๋ ดํ ์ ์ง ๋น์ฉ: ์น์์ผ์ ๋นํด ์๋ฒ ๋ถ๋ด์ด ์ ์ต๋๋ค.
- ์๋ ์ฌ์ฐ๊ฒฐ ๋ฐ ๊ฐ๋จํ ๋ฉ์์ง ์ด๋ฒคํธ ์ฒ๋ฆฌ.
- ๋จ์ :
- ๋จ๋ฐฉํฅ ํต์ ๋ง ๊ฐ๋ฅ: ํด๋ผ์ด์ธํธ์์ ์๋ฒ๋ก๋ ์ง์ ์ ์ธ ๋ฉ์์ง ์ ์ก์ด ๋ถ๊ฐ๋ฅํฉ๋๋ค.
- ๋ธ๋ผ์ฐ์ ํธํ์ฑ ๋ฌธ์ : ์ผ๋ถ ์ค๋๋ ๋ธ๋ผ์ฐ์ ์์๋ ์ง์๋์ง ์์ ์ ์์ต๋๋ค.
3. ํ์ด์ด๋ฒ ์ด์ค ํด๋ผ์ฐ๋ ๋ฉ์์ง (FCM)
- ์ฅ์ :
- ๊ด๋ฒ์ํ ๊ธฐ๊ธฐ ์ง์: ๋ชจ๋ฐ์ผ ๊ธฐ๊ธฐ์ ๋ฐ์คํฌํฑ์ ํฌํจํด ๋ค์ํ ํ๋ซํผ์์ ์๋ํฉ๋๋ค.
- ๋์ ํ์ฅ์ฑ ๋ฐ ์ ๋ขฐ์ฑ: ๊ตฌ๊ธ์ ์ธํ๋ผ๋ฅผ ํ์ฉํ์ฌ ๋๊ท๋ชจ ๋ฉ์์ง๋ฅผ ํจ๊ณผ์ ์ผ๋ก ์ ์กํ ์ ์์ต๋๋ค.
- ์คํ๋ผ์ธ ๋ฉ์์ง ์ฒ๋ฆฌ: ๊ธฐ๊ธฐ๊ฐ ์คํ๋ผ์ธ์ผ ๋ ๋ฉ์์ง๋ฅผ ์ ์ฅํ๋ค๊ฐ ๊ธฐ๊ธฐ๊ฐ ์จ๋ผ์ธ ์ํ๊ฐ ๋๋ฉด ์ ์กํฉ๋๋ค.
- ๋จ์ :
- ์ธ๋ถ ์๋น์ค ์์กด์ฑ: FCM์ ๊ตฌ๊ธ์ ์๋น์ค์ด๋ฏ๋ก ์ด๋ฅผ ํตํ ๋ฉ์์ง ์ ์ก์ ๊ตฌ๊ธ์ ์ ์ฑ ๊ณผ ์๋น์ค ์ํ์ ํฌ๊ฒ ์์กดํฉ๋๋ค.
- ์ถ๊ฐ ๋น์ฉ ๋ฐ์ ๊ฐ๋ฅ์ฑ: ๋ฌด๋ฃ ์ฌ์ฉ๋์ ์ด๊ณผํ ๊ฒฝ์ฐ ๋น์ฉ์ด ๋ฐ์ํ ์ ์์ต๋๋ค.
์ต์ ์ ์ ํ
- ์น์์ผ์ ์๋ฐฉํฅ ํต์ ์ด ํ์ํ๊ฑฐ๋ ์ฌ์ฉ์์ ์ค์๊ฐ ์ํธ์์ฉ์ด ์ค์ํ ๊ฒฝ์ฐ ๊ฐ์ฅ ์ ํฉํฉ๋๋ค.
- SSE๋ ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ์ฃผ๊ธฐ์ ์ด๊ฑฐ๋ ์ง์์ ์ธ ๋ฐ์ดํฐ ์คํธ๋ฆผ์ด ํ์ํ ๋ ์ ์ฉํ๋ฉฐ, ๋ฆฌ์์ค ์ฌ์ฉ์ด ๋ํฉ๋๋ค.
- FCM์ ๋ค์ํ ๊ธฐ๊ธฐ์ ํ๋ซํผ ๊ฐ์ ๋ฉ์์ง ์ ์ก์ด ์ค์ํ๊ณ , ์ธ๋ถ ์๋น์ค์ ์์กดํ๋ ๊ฒ์ด ๋ฌธ์ ๊ฐ ๋์ง ์๋ ๊ฒฝ์ฐ ์ ํฉํฉ๋๋ค.
์นดํ์นด์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ํด๋ผ์ด์ธํธ์๊ฒ ์ ์กํ๋ ๊ฒฝ์ฐ, ์๋ฒ์ ํด๋ผ์ด์ธํธ ๊ฐ์ ํต์ ๋ฐฉ์, ์ง์ํด์ผ ํ๋ ํด๋ผ์ด์ธํธ ์ ํ, ํ์ํ ์ธํ๋ผ ๋ฑ์ ๊ณ ๋ คํด ๊ฐ์ฅ ์ ํฉํ ๊ธฐ์ ์ ์ ํํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค. ์๋ฅผ ๋ค์ด, ๋์ ์ค์๊ฐ ๋ฐ์์ฑ๊ณผ ์๋ฐฉํฅ ํต์ ์ด ํ์ํ ๊ฒฝ์ฐ ์น์์ผ์, ๋น๊ต์ ๊ฐ๋จํ ์๋ฆผ์ด๋ ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ ์ ์ก์๋ SSE๋ FCM์ ๊ณ ๋ คํด๋ณผ ์ ์์ต๋๋ค.
- ๋ค์์ ์ด์ ๋ฅผ ๊ณ ๋ คํด SSE๋ก ๊ตฌํํ๊ธฐ๋ก ํ์๋ค.
- ํด๋ผ์ด์ธํธ๊ฐ SSE๋ก ์ฐ๊ฒฐ์ ์์ฒญํ๋ค. ( ํ์์์์๊ฐ 1๋ถ )
- ํด๋น 1๋ถ ๋ด์ ์นดํ์นด์ ์ปจ์๋จธ๋ก ์ผ๊ธฐ ์์ฑ์ด ์๋ฃ๋์๋ค๋ ๋ฉ์ธ์ง๋ฅผ ์ฝ์ผ๋ฉด ํด๋น ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์จ๋ค,.
- ์ผ๊ธฐ์ ๋ฐ์ดํฐ๋ฅผ SSE๋ก ํด๋ผ์ด์ธํธ์๊ฒ ์ ์กํ๋ค.
- ๋ค์๊ณผ ๊ฐ์ ๋ก์ง์ ์์๋ก ์๋ฒ์์ ํด๋ผ์ด์ธํธ๊น์ง ๋ฐ์ดํฐ๋ฅผ ์ ์ก ํ ์ ์์๋ค.

'๐ฅํ๋ก์ ํธ๐ฅ' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Trouble Shooting - ๊ตฌ๊ธ ๋ก๊ทธ์ธ, Swagger ๐ซ (1) | 2024.08.05 |
---|---|
~ 7/29 ํ์ด์ ํ๋ก์ ํธ ๊ณต๋ชจ์ sprint 2๐ฅ (1) | 2024.08.05 |
ํ์ด์ ICT ๊ณต๋ชจ์ ๊ฐ๋ฐ ์ผ์ง ( 5/30 ) (0) | 2024.05.30 |
Kafka
- Spring ์์ Kafka๋ฅผ ์ฌ์ฉํ๋ ค ํ๋ค ๋ณด๋, ๊ด๋ จ ์ค์ ๋ค์ ํด์ฃผ์ด์ผ ํ๋ค. ํด๋น ์ค์ ๋ค์ ๋ํด์ ์ดํด๋ณด๋๋ก ํ๊ฒ ๋ค!
ConsumerConfig
package com.hanium.diarist.common.config;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.listener.ContainerProperties;
import org.springframework.kafka.support.serializer.ErrorHandlingDeserializer;
import org.springframework.kafka.support.serializer.JsonDeserializer;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableKafka
public class KafkaConsumerConfig {
@Value("${kafka.bootstrap-servers}")
private String bootstrapServers;
@Bean
public ConsumerFactory<String, Object> consumerFactory() {
Map<String, Object> config = new HashMap<>();
config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
config.put(ConsumerConfig.GROUP_ID_CONFIG, "diary");
config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
config.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, JsonDeserializer.class.getName());
config.put(JsonDeserializer.TRUSTED_PACKAGES, "*");
config.put(JsonDeserializer.VALUE_DEFAULT_TYPE, "java.util.HashMap");
return new DefaultKafkaConsumerFactory<>(config, new StringDeserializer(),
new ErrorHandlingDeserializer<>(new JsonDeserializer<>()));
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, Object> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, Object> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
return factory;
}
}
- Consumer ์ ๋ํ ์ค์ ์ map์ ํตํด ํ ์ ์์๋ค. ๊ทธ๋ฃนID, serializer ๋ฐฉ์, ๋ฑ๋ฑ์ ์ค์ ์ ConsumerFactory๋ฅผ ํตํด ํ์๋ค.
ProducerConfig
package com.hanium.diarist.common.config;
import com.hanium.diarist.domain.diary.exception.KafkaConnectException;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import org.springframework.kafka.support.ProducerListener;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
@Configuration
public class KafkaProducerConfig {
@Value("${kafka.bootstrap-servers}")
private String bootstrapServers;
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
return new DefaultKafkaProducerFactory<>(configProps);
}
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
KafkaTemplate<String, String> kafkaTemplate = new KafkaTemplate<>(producerFactory());
kafkaTemplate.setProducerListener(new ProducerListener<String, String>() {
@Override
public void onSuccess(ProducerRecord<String, String> producerRecord, RecordMetadata recordMetadata) {
ProducerListener.super.onSuccess(producerRecord, recordMetadata);
}
@Override
public void onError(ProducerRecord<String, String> producerRecord, RecordMetadata recordMetadata, Exception exception) {
if (exception instanceof TimeoutException) {
throw new KafkaConnectException();
}
}
});
return kafkaTemplate;
}
}
- Producer ์ ๋ํ ์ค์ ์ผ๋ก, bootstrap-server, ์ง๋ ฌํ ๋ฐฉ์๋ฑ์ ๋ํ ์ค์ ์ ํด์ฃผ์๋ค.
- ์ดํ KafkaTemplate๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ผ ํ ํ๋ฆฟ์ ์ค์ ํ์๋ค.
ProducerService
package com.hanium.diarist.domain.diary.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hanium.diarist.domain.diary.domain.Diary;
import com.hanium.diarist.domain.diary.dto.CreateDiaryRequest;
import com.hanium.diarist.domain.diary.exception.JsonProcessException;
import com.hanium.diarist.domain.diary.exception.KafkaConnectException;
import com.hanium.diarist.domain.diary.repository.DiaryRepository;
import com.hanium.diarist.domain.user.domain.User;
import com.hanium.diarist.domain.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.core.KafkaProducerException;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;
@Service
@Slf4j
@RequiredArgsConstructor
public class CreateDiaryProducerService {
private final KafkaTemplate<String,String> kafkaTemplate;
private final ObjectMapper objectMapper;
private final DiaryRepository diaryRepository;
private final UserRepository userRepository;
private final String CreateTopicName = "create-diary";
private final String reCreateTopicName = "re-create-diary";
public boolean sendCreateDiaryMessageWithAd(CreateDiaryRequest createDiaryRequest) {
LocalDate date = createDiaryRequest.getDiaryDate();// ์ผ๊ธฐ ์์ฑ ๋ ์ง ๊ฐ์ ธ์ด
User user = userRepository.findById(createDiaryRequest.getUserId()).orElseThrow();// ํด๋น ์ ์ ์ฐพ์
Optional<Diary> existingDiary = diaryRepository.findByUserAndDiaryDate(user, date);// ํด๋น ์ ์ ์ ์ผ๊ธฐ ์์ฑ ๋ ์ง์ ์ผ๊ธฐ๊ฐ ์๋์ง ํ์ธ
try{
String message = objectMapper.writeValueAsString(createDiaryRequest);
if (existingDiary.isPresent()|| !date.isEqual(LocalDate.now())) { // ๋น์ ๋ ์ง์ ์ผ๊ธฐ๊ฐ ์๊ฑฐ๋, ๊ณผ๊ฑฐ์ ์ผ๊ธฐ๋ฅผ ์์ฑํ ๊ฒฝ์ฐ
// ๊ด๊ณ ์์ฒญ ํ์
sendKafkaMessage(reCreateTopicName, message);
watchAd();
return true;
}else{// ๊ณผ๊ฑฐ ๋ ์ง ์ผ๊ธฐ ์์ฑ
sendKafkaMessage(CreateTopicName, message);
return false;
}
} catch (KafkaProducerException e){
throw new KafkaConnectException();
} catch (JsonProcessingException e) {
throw new JsonProcessException();
}
}
private void sendKafkaMessage(String topic,String message) {
CompletableFuture<SendResult<String, String>> future = kafkaTemplate.send(topic, message);
future.thenAccept(result -> {
// ์ฑ๊ณต ์ ๋ก์ง
log.info("Message sent successfully to topic {} with offset {}", result.getRecordMetadata().topic(), result.getRecordMetadata().offset());
}).exceptionally(ex -> {
// ์คํจ ์ ์์ธ ์ฒ๋ฆฌ
if (ex.getCause() instanceof TimeoutException) {
throw new KafkaConnectException();
}
return null;
});
}
@Async
public void watchAd() {
System.out.println("๊ด๊ณ ์์ฒญ ์ค");
}
}
- ๋ ์ง๋ฅผ ์์ฑํ๋ ์กฐ๊ฑด ( ์ค๋ ์ฒซ ์์ฑ, ์ค๋ ์ฌ ์์ฑ, ๊ณผ๊ฑฐ ์ผ๊ธฐ ์์ฑ) ์ ๋ฐ๋ผ ๋ก์ง์ด ๋ฌ๋ผ์ง๊ธฐ ๋๋ฌธ์ ํ ํฝ์ ๋๋์ด์ฃผ์๋ค.
- ์ผ๊ธฐ๋ฅผ ๋ณด๋ผ ๊ฒฝ์ฐ ์นดํ์นด ๋ธ๋ก์ปค์ ๋ค์ด๊ฐ๋ ๊ฒ์ ํ์ธํ ์ ์์๋ค!

ConsumerService
package com.hanium.diarist.domain.diary.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hanium.diarist.domain.diary.domain.Diary;
import com.hanium.diarist.domain.diary.dto.CreateDiaryResponse;
import com.hanium.diarist.domain.diary.exception.DiaryIdInvalidException;
import com.hanium.diarist.domain.diary.exception.DiaryNotFoundException;
import com.hanium.diarist.domain.diary.exception.JsonProcessException;
import com.hanium.diarist.domain.diary.exception.SseException;
import com.hanium.diarist.domain.diary.repository.DiaryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.HashMap;
import java.util.concurrent.CopyOnWriteArrayList;
@Service
@RequiredArgsConstructor
public class CreateDiaryConsumerService {
private final DiaryRepository diaryRepository;
private final ObjectMapper objectMapper;
private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>();// ํด๋ผ์ด์ธํธ์ sse ์ฐ๊ฒฐ์ ๊ด๋ฆฌํ๋ ๋ฆฌ์คํธ
// ํด๋ผ์ด์ธํธ๊ฐ sse ์ฐ๊ฒฐ์ ์์ฒญํ ๋ ์๋ก์ด sseEmitter๋ฅผ ์์ฑํด ๋ฐํํจ
public SseEmitter addEmitter() {
SseEmitter emitter = new SseEmitter(60000L);// ํ์์์ ์๊ฐ 60์ด
this.emitters.add(emitter);
emitter.onCompletion(() -> this.emitters.remove(emitter));// ์ฐ๊ฒฐ ์๋ฃ์ ๋ฆฌ์คํธ์์ ์ ๊ฑฐ
emitter.onTimeout(() -> this.emitters.remove(emitter));// ํ์์์์ ๋ฆฌ์คํธ์์ ์ ๊ฑฐ
emitter.onError(e -> this.emitters.remove(emitter));// ์๋ฌ์ ๋ฆฌ์คํธ์์ ์ ๊ฑฐ
return emitter;
}
// ๋ชจ๋ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฉ์ธ์ง๋ฅผ ์ ์กํจ.
private void sendEmitters(CreateDiaryResponse diaryResponse){
for (SseEmitter emitter : emitters) {
try {
String jsonResponse = objectMapper.writeValueAsString(diaryResponse);
//System.out.println("Sending to emitter: " + jsonResponse);
emitter.send(SseEmitter.event().data(jsonResponse));
}catch (JsonProcessingException e) {
throw new JsonProcessException();
} catch (IOException e) {
this.emitters.remove(emitter);
throw new SseException();
}
}
}
// kafka ์ create-diary-response ํ ํฝ์์ ๋ฉ์ธ์ง๋ฅผ ์์ ํด์ ์ฒ๋ฆฌํจ
@KafkaListener(topics = "create-diary-response", groupId = "diary")
public void consumeCreateDiaryResponse(HashMap<String,Integer> message) {
try {
long diaryId = Long.parseLong(String.valueOf(message.get("diaryId")));
Diary diary = diaryRepository.findByDiaryIdWithDetails(diaryId).orElseThrow(() -> new DiaryNotFoundException());
CreateDiaryResponse createDiaryResponse = new CreateDiaryResponse(diary.getEmotion().getEmotionName(), diary.getEmotion().getEmotionPicture(), diary.getContent(), diary.getArtist().getArtistName(), diary.getArtist().getArtistPicture(), diary.getDiaryDate(), diary.getImage().getImageUrl());
// System.out.println(createDiaryResponse);
sendEmitters(createDiaryResponse);
} catch (NumberFormatException e) {
throw new DiaryIdInvalidException();
}
}
}
- ์ปจ์๋จธ๋ฅผ ๊ตฌํํ ๋ ํ๊ฐ์ง ์๊ฐํด๋ณด์ผ ํ ์ผ์ด ์์๋ค.
graph LR
a[app] --> b[server]
b --> c([kafka])
c--> d[model]
d-->c
c-->b
- ๋ค์๊ณผ ๊ฐ์ด appโ ์๋ฒ๋ก ์์ฒญ, ์๋ฒ์์ ์นดํ์นด , ๋ชจ๋ธ์์ ์นดํ์นด์ ๋ฉ์ธ์ง๋ฅผ ๊ตฌ๋ ํด์ ์ฝ์ด์จ๋ค.
- ์ดํ ๋ชจ๋ธ์์ ์นดํ์นด๋ก ๋ฉ์ธ์งํ๋ฅผ ๋ณด๋ด๊ณ ์๋ฒ๊ฐ ํด๋น ๋ฉ์ธ์ง๋ฅผ ๊ตฌ๋ ์ ํ๋ค๋ ๊ฐ๋ ๊น์ง๋ ๊ฐ๋ฅํ์๋ค.
<aside> ๐ก ๊ทธ๋ ๋ค๋ฉด ์ด๋ป๊ฒ ์๋ฒ์์ app์ผ๋ก ์ผ๊ธฐ๊ฐ ์์ฑ ์๋ฃ๋์๋ค๋ ๊ฒ์ ์๋ ค์ค ์ ์์๊น?
</aside>
- ์ด ๋ถ๋ถ์ ๋ํด์ ์๊ฐํด๋ณผ ํ์๊ฐ ์์๋ค.
โ ์๊ฐํ ์ ์๋ ๋ฐฉ๋ฒ์ ๋ช๊ฐ์ง๊ฐ ์์๋ค
1. ์น์์ผ (WebSocket)
- ์ฅ์ :
- ์๋ฐฉํฅ ํต์ : ํด๋ผ์ด์ธํธ์ ์๋ฒ๊ฐ ์ง์์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์ ์ ์์ต๋๋ค.
- ์ ์ง์ฐ: ์ค์๊ฐ ํต์ ์ ์ ํฉํ๋ฉฐ, ์ฆ๊ฐ์ ์ธ ์ํธ์์ฉ์ด ํ์ํ ๋ ์ ์ฉํฉ๋๋ค.
- ํ์ค ํ๋กํ ์ฝ๋ก ๋๋ฆฌ ์ง์๋ฉ๋๋ค.
- ๋จ์ :
- ์ฐ๊ฒฐ ์ ์ง ๊ด๋ฆฌ: ์๋ฒ๋ ๊ฐ ํด๋ผ์ด์ธํธ์ ์ง์์ ์ผ๋ก ์ฐ๊ฒฐ์ ์ ์งํด์ผ ํ๋ฉฐ, ์ด๋ ๋ฆฌ์์ค๋ฅผ ์๋นํ ์ฌ์ฉํ ์ ์์ต๋๋ค.
- ์ค์ผ์ผ๋ง ๋ฌธ์ : ๋ง์ ์์ ๋์ ์ฐ๊ฒฐ์ ๊ด๋ฆฌํ๊ธฐ ์ํด์๋ ์ถ๊ฐ ์ธํ๋ผ์คํธ๋ญ์ฒ์ ๋ณต์ก์ฑ์ด ์๊ตฌ๋ฉ๋๋ค.
2. ์๋ฒ-์ ์ก ์ด๋ฒคํธ (SSE)
- ์ฅ์ :
- ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก์ ๋จ๋ฐฉํฅ ํต์ : ์๋ฒ๊ฐ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฐ์ดํฐ๋ฅผ ํธ์ํ ์ ์์ต๋๋ค.
- ์ ๋ ดํ ์ ์ง ๋น์ฉ: ์น์์ผ์ ๋นํด ์๋ฒ ๋ถ๋ด์ด ์ ์ต๋๋ค.
- ์๋ ์ฌ์ฐ๊ฒฐ ๋ฐ ๊ฐ๋จํ ๋ฉ์์ง ์ด๋ฒคํธ ์ฒ๋ฆฌ.
- ๋จ์ :
- ๋จ๋ฐฉํฅ ํต์ ๋ง ๊ฐ๋ฅ: ํด๋ผ์ด์ธํธ์์ ์๋ฒ๋ก๋ ์ง์ ์ ์ธ ๋ฉ์์ง ์ ์ก์ด ๋ถ๊ฐ๋ฅํฉ๋๋ค.
- ๋ธ๋ผ์ฐ์ ํธํ์ฑ ๋ฌธ์ : ์ผ๋ถ ์ค๋๋ ๋ธ๋ผ์ฐ์ ์์๋ ์ง์๋์ง ์์ ์ ์์ต๋๋ค.
3. ํ์ด์ด๋ฒ ์ด์ค ํด๋ผ์ฐ๋ ๋ฉ์์ง (FCM)
- ์ฅ์ :
- ๊ด๋ฒ์ํ ๊ธฐ๊ธฐ ์ง์: ๋ชจ๋ฐ์ผ ๊ธฐ๊ธฐ์ ๋ฐ์คํฌํฑ์ ํฌํจํด ๋ค์ํ ํ๋ซํผ์์ ์๋ํฉ๋๋ค.
- ๋์ ํ์ฅ์ฑ ๋ฐ ์ ๋ขฐ์ฑ: ๊ตฌ๊ธ์ ์ธํ๋ผ๋ฅผ ํ์ฉํ์ฌ ๋๊ท๋ชจ ๋ฉ์์ง๋ฅผ ํจ๊ณผ์ ์ผ๋ก ์ ์กํ ์ ์์ต๋๋ค.
- ์คํ๋ผ์ธ ๋ฉ์์ง ์ฒ๋ฆฌ: ๊ธฐ๊ธฐ๊ฐ ์คํ๋ผ์ธ์ผ ๋ ๋ฉ์์ง๋ฅผ ์ ์ฅํ๋ค๊ฐ ๊ธฐ๊ธฐ๊ฐ ์จ๋ผ์ธ ์ํ๊ฐ ๋๋ฉด ์ ์กํฉ๋๋ค.
- ๋จ์ :
- ์ธ๋ถ ์๋น์ค ์์กด์ฑ: FCM์ ๊ตฌ๊ธ์ ์๋น์ค์ด๋ฏ๋ก ์ด๋ฅผ ํตํ ๋ฉ์์ง ์ ์ก์ ๊ตฌ๊ธ์ ์ ์ฑ ๊ณผ ์๋น์ค ์ํ์ ํฌ๊ฒ ์์กดํฉ๋๋ค.
- ์ถ๊ฐ ๋น์ฉ ๋ฐ์ ๊ฐ๋ฅ์ฑ: ๋ฌด๋ฃ ์ฌ์ฉ๋์ ์ด๊ณผํ ๊ฒฝ์ฐ ๋น์ฉ์ด ๋ฐ์ํ ์ ์์ต๋๋ค.
์ต์ ์ ์ ํ
- ์น์์ผ์ ์๋ฐฉํฅ ํต์ ์ด ํ์ํ๊ฑฐ๋ ์ฌ์ฉ์์ ์ค์๊ฐ ์ํธ์์ฉ์ด ์ค์ํ ๊ฒฝ์ฐ ๊ฐ์ฅ ์ ํฉํฉ๋๋ค.
- SSE๋ ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ์ฃผ๊ธฐ์ ์ด๊ฑฐ๋ ์ง์์ ์ธ ๋ฐ์ดํฐ ์คํธ๋ฆผ์ด ํ์ํ ๋ ์ ์ฉํ๋ฉฐ, ๋ฆฌ์์ค ์ฌ์ฉ์ด ๋ํฉ๋๋ค.
- FCM์ ๋ค์ํ ๊ธฐ๊ธฐ์ ํ๋ซํผ ๊ฐ์ ๋ฉ์์ง ์ ์ก์ด ์ค์ํ๊ณ , ์ธ๋ถ ์๋น์ค์ ์์กดํ๋ ๊ฒ์ด ๋ฌธ์ ๊ฐ ๋์ง ์๋ ๊ฒฝ์ฐ ์ ํฉํฉ๋๋ค.
์นดํ์นด์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ํด๋ผ์ด์ธํธ์๊ฒ ์ ์กํ๋ ๊ฒฝ์ฐ, ์๋ฒ์ ํด๋ผ์ด์ธํธ ๊ฐ์ ํต์ ๋ฐฉ์, ์ง์ํด์ผ ํ๋ ํด๋ผ์ด์ธํธ ์ ํ, ํ์ํ ์ธํ๋ผ ๋ฑ์ ๊ณ ๋ คํด ๊ฐ์ฅ ์ ํฉํ ๊ธฐ์ ์ ์ ํํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค. ์๋ฅผ ๋ค์ด, ๋์ ์ค์๊ฐ ๋ฐ์์ฑ๊ณผ ์๋ฐฉํฅ ํต์ ์ด ํ์ํ ๊ฒฝ์ฐ ์น์์ผ์, ๋น๊ต์ ๊ฐ๋จํ ์๋ฆผ์ด๋ ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ ์ ์ก์๋ SSE๋ FCM์ ๊ณ ๋ คํด๋ณผ ์ ์์ต๋๋ค.
- ๋ค์์ ์ด์ ๋ฅผ ๊ณ ๋ คํด SSE๋ก ๊ตฌํํ๊ธฐ๋ก ํ์๋ค.
- ํด๋ผ์ด์ธํธ๊ฐ SSE๋ก ์ฐ๊ฒฐ์ ์์ฒญํ๋ค. ( ํ์์์์๊ฐ 1๋ถ )
- ํด๋น 1๋ถ ๋ด์ ์นดํ์นด์ ์ปจ์๋จธ๋ก ์ผ๊ธฐ ์์ฑ์ด ์๋ฃ๋์๋ค๋ ๋ฉ์ธ์ง๋ฅผ ์ฝ์ผ๋ฉด ํด๋น ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์จ๋ค,.
- ์ผ๊ธฐ์ ๋ฐ์ดํฐ๋ฅผ SSE๋ก ํด๋ผ์ด์ธํธ์๊ฒ ์ ์กํ๋ค.
- ๋ค์๊ณผ ๊ฐ์ ๋ก์ง์ ์์๋ก ์๋ฒ์์ ํด๋ผ์ด์ธํธ๊น์ง ๋ฐ์ดํฐ๋ฅผ ์ ์ก ํ ์ ์์๋ค.

'๐ฅํ๋ก์ ํธ๐ฅ' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Trouble Shooting - ๊ตฌ๊ธ ๋ก๊ทธ์ธ, Swagger ๐ซ (1) | 2024.08.05 |
---|---|
~ 7/29 ํ์ด์ ํ๋ก์ ํธ ๊ณต๋ชจ์ sprint 2๐ฅ (1) | 2024.08.05 |
ํ์ด์ ICT ๊ณต๋ชจ์ ๊ฐ๋ฐ ์ผ์ง ( 5/30 ) (0) | 2024.05.30 |