์ง๋ ํฌ์คํ ์ ์ด์ด ์ฑํ ๋ง์ดํฌ๋ก ์๋น์ค ๊ฐ๋ฐ ๊ณผ์ ์ ํฌ์คํ ํ๋๋ก ํ๊ฒ ์ต๋๋ค. ์ฃผ์ ์ฝ๋๋ง ๋ค๋ฃฐ ์์ ์ด๋ผ ๋ค๋ฃจ์ง ์๋ ์ฝ๋๋ค์ ์๋ github๋ฅผ ํตํด ์ฐธ๊ณ ํ์๊ธธ ๋ฐ๋๋๋ค๐๐(msa-master ๋ธ๋์น)
+) Spring Cloud๋ฅผ ํตํ MSA๋ฅผ ๊ตฌ์ถํ์๋ ๋ถ์ด ์๋๋ผ๋ฉด ๋ชฉ์ฐจ 3๋ฒ๋ถํฐ ์งํํ์๋ฉด ๋ฉ๋๋ค!
๐ ์ฐธ๊ณ ์ฝ๋ : https://github.com/LminWoo99/PlantBackend/tree/msa-master
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
ext {
set('springCloudVersion', "2021.0.4")
}
server:
port: 0
spring:
application:
name: [์๋น์ค ๋ช
]
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}}
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://[๋๋ฉ์ธ]:8761/eureka
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@EnableJpaAuditing
@EnableWebMvc
public class PlantChatServiceApplication {
public static void main(String[] args) {
SpringApplication.run(PlantChatServiceApplication.class, args);
}
}
version: '3'
networks:
plant-network:
external: true
name: plant-network
services:
zookeeper:
image: wurstmeister/zookeeper
container_name: zookeeper
ports:
- "2181:2181"
networks:
- plant-network
kafka:
image: wurstmeister/kafka
container_name: kafka
ports:
- "9092:9092"
environment:
KAFKA_ADVERTISED_HOST_NAME: localhost
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
volumes:
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- zookeeper
networks:
- plant-network
mongo:
image: mongo:latest
container_name: mongo
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: [์ ์ name]
MONGO_INITDB_ROOT_PASSWORD: [์ ์ password]
networks:
- plant-network
docker-compose.yml ํ์ผ์ ์์ ์์ฑ๋ ์ฝ๋๋ฅผ ๋ถ์ฌ ๋ฃ์ด ์ฃผ์๊ณ ํ๋ก์ ํธ ์ต์๋จ ์์น์์ ์๋์ ๋ช ๋ น์ด๋ฅผ ์ ๋ ฅํด์ค๋๋ค.
docker-compose up -d
๐จ ์ฃผ์์ฌํญ!
MongoDB ๊ตฌ์ถ ์ username๊ณผ password๋ฅผ ์ค์ ํ์ง ์์ผ๋ฉด ์ฌ๊ฐํ ๋ณด์ ์ด์๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ์ต๊ทผ ๋ง์ ์ฌ๋ก์์ ๋ณด์์ด ์ทจ์ฝํ MongoDB ์ธ์คํด์ค๊ฐ ๋์ฌ์จ์ด ๊ณต๊ฒฉ์ ๋์์ด ๋์ด ๋ฐ์ดํฐ๊ฐ ์ํธํ๋๊ฑฐ๋ ์ญ์ ๋๋ ์ฌ๊ณ ๊ฐ ๋ฐ์ํ์ต๋๋ค. ์ด๋ฅผ ์๋ฐฉํ๊ธฐ ์ํด ๋ฐ๋์ ๊ฐ๋ ฅํ ์ธ์ฆ ์ ๋ณด๋ฅผ ์ค์ ํ๊ณ , ํ์ํ ๊ฒฝ์ฐ์๋ง ์ธ๋ถ์์ ์ ๊ทผ ๊ฐ๋ฅํ๋๋ก ๋คํธ์ํฌ ์ค์ ์ ํด์ผ ํฉ๋๋ค.
docker exec -it zookeeper bash
bash bin/zkServer.sh start /opt/zookeeper-3.4.13/conf/zoo.cfg -demon
docker exec -it kafka bash
kafka-server-start.sh -daemon
Kafka ์๋ฒ ์ค์ ๊น์ง ์๋ฃ ๋์์ต๋๋ค.
Kafka ์๋ฒ ์ค์ ์ด ์๋ฃ๋์์ต๋๋ค. ์ด์ Kafka์ ํต์ฌ ๊ฐ๋
์ธ Topic
์ ๋ํด ์์๋ณด๊ณ , ํ
์คํธ๋ฅผ ์งํํ๊ฒ ์ต๋๋ค.
Kafka์์ Topic
์ ๋ฉ์์ง๋ฅผ ๊ตฌ๋ถํ๋ ๋จ์์
๋๋ค. pub/sub
๋ชจ๋ธ์ ํตํด ๋ฉ์์ง์ ์์ฐ๊ณผ ์๋น๊ฐ ์ด๋ฃจ์ด์ง๋ฉฐ, ์ด๋ฅผ ์ํด์๋ Topic์ ๋จผ์ ์์ฑํด์ผ ํฉ๋๋ค.
๐ก์ฐธ๊ณ : ์ค์ ์ฑํ ๊ธฐ๋ฅ์ ์ฌ์ฉํ Topic์ Spring ์ ํ๋ฆฌ์ผ์ด์ ์์ ์๋์ผ๋ก ์์ฑํ ์์ ์ ๋๋ค.
Kafka๊ฐ ์ ์์ ์ผ๋ก ์๋ํ๋์ง ํ์ธํ๊ธฐ ์ํด ํ ์คํธ์ฉ Topic์ ์์ฑํด๋ณด๊ฒ ์ต๋๋ค.
kafka-topics.sh --create --zookeeper zookeeper:2181 --topic test-topic --partitions 1 --replication-factor 1
kafka-topics.sh --list --zookeeper zookeeper
์ด ๋ช
๋ น์ด๋ฅผ ์คํํ๋ฉด test-topic
์ด ๋ชฉ๋ก์ ํ์๋์ด์ผ ํฉ๋๋ค.
๐ Tip: ์ค์ ํ๋ก๋์ ํ๊ฒฝ์์๋ ๋ณด์๊ณผ ์ฑ๋ฅ์ ๊ณ ๋ คํ์ฌ Topic์ ํํฐ์ ์์ ๋ณต์ ํฉํฐ๋ฅผ ์ ์ ํ ์กฐ์ ํด์ผ ํฉ๋๋ค.
@EnableKafka
@Configuration
public class KafkaConsumerConfig {
@Value("${kafka.url}")
private String kafkaServerUrl;
@Bean
ConcurrentKafkaListenerContainerFactory<String, Message> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, Message> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
return factory;
}
@Bean
public ConsumerFactory<String, Message> consumerFactory() {
JsonDeserializer<ChatMessage> deserializer = new JsonDeserializer<>();
deserializer.addTrustedPackages("*");
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaServerUrl);
props.put(ConsumerConfig.GROUP_ID_CONFIG, "chat");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, deserializer);
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), deserializer);
}
}
@EnableKafka
@Configuration
public class KafkaProducerConfig {
@Value("${kafka.url}")
private String kafkaServerUrl;
@Bean
public ProducerFactory<String, Message> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigurations());
}
// Kafka Producer ๊ตฌ์ฑ์ ์ํ ์ค์ ๊ฐ๋ค์ ํฌํจํ ๋งต์ ๋ฐํํ๋ ๋ฉ์๋
@Bean
public Map<String, Object> producerConfigurations() {
return ImmutableMap.<String, Object>builder()
.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaServerUrl)
.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class)
.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class)
//๋ฉฑ๋ฑ์ฑ ํ๋ก๋์ ๋ช
์์ ์ค์
.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true)
.build();
}
}
mongodb:
client: mongodb://{ip์ฃผ์}:27017
name : {db ์ด๋ฆ}
์ค์ ํด๋์ค์ ์ฌ์ฉํ properties ํ์ผ์ ์์ฑ
@Data
@Component
@ConfigurationProperties(prefix = "mongo-db")
public class MongoDbProperties {
private String connectionString;
private String databaseName;
}
MongoConfig ํด๋์ค ์์ฑ
// MongoDB ์ค์ ํด๋์ค
@Configuration
@RequiredArgsConstructor
@EnableMongoRepositories(basePackages = "com.example.plantchatservice.repository.mongo")
public class MongoDbConfiguration {
private final MongoDbSettings mongoDbSettings;
// MongoDB ํด๋ผ์ด์ธํธ ๋น ์์ฑ
@Bean
public MongoClient createMongoClient() {
return MongoClients.create(mongoDbSettings.getConnectionString());
}
// MongoTemplate ๋น ์์ฑ
@Bean
public MongoTemplate createMongoTemplate() {
return new MongoTemplate(createMongoClient(), mongoDbSettings.getDatabaseName());
}
}
@Configuration
@RequiredArgsConstructor
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final StompHandler stompHandler;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat")
.setAllowedOriginPatterns("*")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/subscribe");
registry.setApplicationDestinationPrefixes("/publish");
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(stompHandler);
}
}
์น์์ผ ์ค์ ์ ๋ณด๋ ์ค์ํ๋ ๋ฉ์๋๋ณ๋ก ์์ธํ ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค!
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat")
.setAllowedOriginPatterns("*")
.withSockJS();
}
/chat
setAllowedOriginPatterns("*")
: ๋ชจ๋ ์ค๋ฆฌ์ง ํ์ฉ (๊ฐ๋ฐ ํ๊ฒฝ)withSockJS()
: SockJS ์ง์ ํ์ฑํ@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/subscribe");
registry.setApplicationDestinationPrefixes("/publish");
}
/subscribe
/subscribe/{chatNo}
(์ฑํ
๋ฐฉ ๋ฒํธ๋ก ๊ตฌ๋
)/publish
/publish/message
(๋ฉ์์ง ์ ์ก ์ปจํธ๋กค๋ฌ๋ก ๋ผ์ฐํ
)
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(stompHandler);
}
stompHandler
๋ฅผ ์ธํฐ์
ํฐ๋ก ๋ฑ๋ก/chat
์ ํตํด WebSocket ์ฐ๊ฒฐ/subscribe/{chatNo}
๋ก ํน์ ์ฑํ
๋ฐฉ ๊ตฌ๋
/publish/message
๋ก ๋ฉ์์ง ์ ์กStompHandler
๋ฅผ ํตํ ๋ฉ์์ง ์ฒ๋ฆฌ ๋ฐ ์ถ๊ฐ ๋ก์ง ๊ตฌํ ๊ฐ๋ฅ์ด ์ค์ ์ ํตํด ์ฑํ ๋ฐฉ๋ณ๋ก ํจ๊ณผ์ ์ธ ๋ฉ์์ง ๊ด๋ฆฌ ๋ฐ ์ ๋ฌ์ด ๊ฐ๋ฅํฉ๋๋ค.
@Document(collection="chatting")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Chatting {
@Id
private String id;
private Integer chatRoomNo;
private Integer senderNo;
private String senderName;
private String contentType;
private String content;
private LocalDateTime sendDate;
private long readCount;
@Builder
public Chatting(String id, Integer chatRoomNo, Integer senderNo, String senderName, String contentType, String content, LocalDateTime sendDate, long readCount) {
this.id = id;
this.chatRoomNo = chatRoomNo;
this.senderNo = senderNo;
this.senderName = senderName;
this.contentType = contentType;
this.content = content;
this.sendDate = sendDate;
this.readCount = readCount;
}
}
+) ์ถ๊ฐ๋ก msa๋ฅผ ๊ตฌ์ถํ์๋ ๋ถ๋ค์ ์น์์ผ์ ws ํ๋กํ ์ฝ์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ gateway yml์ ์๋ ์ฝ๋๋ฅผ ์ถ๊ฐํด์ค์ผ ํฉ๋๋ค!!
- id: plant-chat-service
uri: lb:ws://PLANT-CHAT-SERVICE
predicates:
- Path=/plant-chat-service/chat/**
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/plant-chat-service/(?<segment>.*), /$\{segment}
๋จผ์ ์ ๋ plant-service
(์ค๊ณ ๊ฑฐ๋ ๊ฒ์๊ธ ๋ง์ดํฌ๋ก ์๋น์ค)์ ์ค๊ณ ๊ฑฐ๋ ๊ฒ์๊ธ ๋ฐ์ดํฐ๊ฐ ์๊ธฐ ๋๋ฌธ์ FeignCleint
๋ฅผ ํตํด ํ์ํ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์์ต๋๋ค.
๊ทธ๋์ ํธ์ถํ ์๋ํฌ์ธํธ์ ๋ฉ์๋์ ๋ง๊ฒ ์๋์ ๊ฐ์ ํ์์ผ๋ก ์์ฑ์๋ฉด ๋ฉ๋๋ค!!
@FeignClient(name="plant-service")
public interface PlantServiceClient {
@GetMapping("{์๋ํฌ์ธํธ}")
ResponseEntity<ResponseTradeBoardDto> boardContent(@PathVariable(value = "id") Long id);
@GetMapping("{์๋ํฌ์ธํธ}")
ResponseEntity<MemberDto> findById(@RequestParam Long id);
@GetMapping("{์๋ํฌ์ธํธ}")
ResponseEntity<MemberDto> findByUsername(@RequestParam String username);
@GetMapping("{์๋ํฌ์ธํธ}")
ResponseEntity<MemberDto> getJoinMember(@RequestHeader("Authorization") String jwtToken);
}
@Slf4j
@Service
@RequiredArgsConstructor
public class ChatService {
private final ChatRepository chatRepository;
private final MongoChatRepository mongoChatRepository;
private final MessageSender sender;
private final AggregationSender aggregationSender;
private final MongoTemplate mongoTemplate;
private final ChatRoomService chatRoomService;
private final PlantServiceClient plantServiceClient;
private final CircuitBreakerFactory circuitBreakerFactory;
private final TokenHandler tokenHandler;
private final NotificationService notificationService;
/**
* ์ฑํ
๋ฐฉ ์์ฑ ๋ฉ์๋
* ๊ฑฐ๋ ๊ฒ์๊ธ์ ์ฌ๋ฆฌ์ง ์์ ์ฌ๋๋ง ํธ์ถํ๋ ๋ฉ์๋
* ๊ตฌ๋งค ํฌ๋ง์๋ง ์ฑํ
๋ฐฉ์ ์์ฑ ๊ฐ๋ฅ
* FeignCLient๋ฅผ ํตํด plant-service์์ ๊ฑฐ๋๊ฐ๋ฅ ์ฌ๋ถ ํ์ธ
* @param : MemberDto memberDto, ChatRequestDto requestDto
*/
@Transactional
public Chat makeChatRoom(Integer memberNo, ChatRequestDto requestDto) {
CircuitBreaker circuitBreaker = circuitBreakerFactory.create("circuitbreaker");
// ์ฑํ
์ ๊ฑธ๋ ค๊ณ ํ๋ ๊ฑฐ๋๊ธ์ด ๊ฑฐ๋ ๊ฐ๋ฅ ์ํ์ธ์ง ์กฐํํด๋ณธ๋ค.
ResponseEntity<ResponseTradeBoardDto> tradeBoardDto = circuitBreaker.run(() ->
plantServiceClient.boardContent(requestDto.getTradeBoardNo().longValue()),
throwable -> ResponseEntity.ok(null));
// ์กฐํํด์จ ๊ฑฐ๋๊ธ ์ํ๊ฐ ๊ฑฐ๋์๋ฃ ์ด๋ผ๋ฉด ๊ฑฐ๋๊ฐ ๋ถ๊ฐ๋ฅํ ์ํ์ด๋ค.
if (tradeBoardDto.getBody().getStatus().equals("๊ฑฐ๋์๋ฃ")) {
throw new IllegalStateException("ํ์ฌ ๊ฑฐ๋๊ฐ๋ฅ ์ํ๊ฐ ์๋๋๋ค.");
}
Integer tradeBoardNo = tradeBoardDto.getBody().getId().intValue();
//์ด๋ฏธ ํด๋น๊ธ ๊ธฐ์ค์ผ๋ก ์ฑํ
์ ์์ฒญํ ์ฌ๋๊ณผ ๋ฐ๋ ์ฌ๋์ด ์ผ์นํ ๊ฒฝ์ฐ ์ฒดํฌ
if (chatRepository.existChatRoomByBuyer(tradeBoardNo, requestDto.getCreateMember(), memberNo)) {
Chat existedChat = chatRepository.findByTradeBoardNoAndChatNo(tradeBoardNo, requestDto.getCreateMember());
return existedChat;
}
if (!chatRepository.existChatRoomByBuyer(tradeBoardNo, requestDto.getCreateMember(), memberNo)) {
Chat chat = Chat.builder()
.tradeBoardNo(requestDto.getTradeBoardNo())
.createMember(requestDto.getCreateMember())
.joinMember(memberNo)
.regDate(LocalDateTime.now())
.build();
Chat savedChat = chatRepository.save(chat);
return savedChat;
}
throw new IllegalArgumentException("์กด์ฌํ์ง ์๋ ๊ฒ์๊ธ ์
๋๋ค");
}
/**
* ์ฑํ
๋ฐฉ ๋ฆฌ์คํธ ์กฐํ ๋ฉ์๋
* FeignCLient๋ฅผ ํตํด plant-service์์ ์ ์ ์ ๋ณด ์กฐํํ ์ฑํ
๋ฐฉ ๋ง๋ ์ฌ๋์ธ์ง ํ์ธ
* mongodb์์ ์ฑํ
๋ฉ์ธ์ง ๋ณด๋ธ ์๊ฐ์ ๋ด๋ฆผ์ฐจ์์ผ๋ก ์ ๋ ฌํ ์ฒซ๋ฒ์งธ ๊ฐ ๋ง์ง๋ง ๋ฉ์ธ์ง๋ก ์ธํ
* @param : Integer memberNo, Integer tradeBoardNo
*/
public List<ChatRoomResponseDto> getChatList(Integer memberNo, Integer tradeBoardNo) {
List<ChatRoomResponseDto> chatRoomList = chatRepository.getChattingList(memberNo, tradeBoardNo);
//Participant ์ฑ์์ผ๋จ(username)
chatRoomList
.forEach(chatRoomDto -> {
//param์ผ๋ก ๋์ด์จ ๋ฉค๋ฒ๊ฐ ์ฑํ
๋ง๋ ๋ฉค๋ฒ์ผ ๊ฒฝ์ฐ => Participant์ ์ฐธ๊ฐํ ๋ฉค๋ฒ
// ResponseEntity<MemberDto> byId = plantServiceClient.findById(chatRoomDto.getCreateMember().longValue());
if (memberNo.equals(chatRoomDto.getCreateMember())) {
ResponseEntity<MemberDto> memberDtoResponse = plantServiceClient.findById(chatRoomDto.getJoinMember().longValue());
chatRoomDto.setParticipant(new ChatRoomResponseDto.Participant(memberDtoResponse.getBody().getUsername(), memberDtoResponse.getBody().getNickname()));
}
//param์ผ๋ก ๋์ด์จ ๋ฉค๋ฒ๊ฐ ์ฑํ
๋ฐฉ์ ์ฐธ๊ฐํ ๋ฉค๋ฒ์ผ ๊ฒฝ์ฐ => Participant์ ์ฑํ
๋ฐฉ ๋ง๋ ๋ฉค๋ฒ
if (!memberNo.equals(chatRoomDto.getCreateMember())){
ResponseEntity<MemberDto> memberDtoResponse = plantServiceClient.findById(chatRoomDto.getCreateMember().longValue());
chatRoomDto.setParticipant(new ChatRoomResponseDto.Participant(memberDtoResponse.getBody().getUsername(), memberDtoResponse.getBody().getNickname()));
}
// // ์ฑํ
๋ฐฉ๋ณ๋ก ์ฝ์ง ์์ ๋ฉ์์ง ๊ฐ์๋ฅผ ์
ํ
long unReadCount = countUnReadMessage(chatRoomDto.getChatNo(), memberNo);
chatRoomDto.setUnReadCount(unReadCount);
// ์ฑํ
๋ฐฉ๋ณ๋ก ๋ง์ง๋ง ์ฑํ
๋ด์ฉ๊ณผ ์๊ฐ์ ์
ํ
Page<Chatting> chatting =
mongoChatRepository.findByChatRoomNoOrderBySendDateDesc(chatRoomDto.getChatNo(), PageRequest.of(0, 1));
if (chatting.hasContent()) {
Chatting chat = chatting.getContent().get(0);
ChatRoomResponseDto.LatestMessage latestMessage = ChatRoomResponseDto.LatestMessage.builder()
.context(chat.getContent())
.sendAt(chat.getSendDate())
.build();
chatRoomDto.setLatestMessage(latestMessage);
}
});
return chatRoomList;
}
/**
* ์ฑํ
๋ฉ์ธ์ง ์กฐํ ๋ฉ์๋
* ์ฑํ
๋ฉ์ธ์ง ์กฐํ์ ํด๋น ๋ฉ์ธ์ง๋ฅผ ์ฝ์ ๊ฒ์ด๋ฏ๋ก ๋ฉ์ธ์ง ์ฝ์ ์ฒ๋ฆฌ๋ ์งํ
* @param : Integer chatRoomNo, Integer memberNo
*/
public ChattingHistoryResponseDto getChattingList(Integer chatRoomNo, Integer memberNo) {
CircuitBreaker circuitBreaker = circuitBreakerFactory.create("circuitbreaker");
// member id๋ก ์กฐํ
ResponseEntity<MemberDto> memberDto = circuitBreaker.run(() -> plantServiceClient.findById(memberNo.longValue()),
throwable -> ResponseEntity.ok(null));
updateCountAllZero(chatRoomNo, memberDto.getBody().getUsername());
List<ChatResponseDto> chattingList = mongoChatRepository.findByChatRoomNo(chatRoomNo)
.stream()
.map(chat -> new ChatResponseDto(chat, memberNo)
)
.collect(Collectors.toList());
return ChattingHistoryResponseDto.builder()
.chatList(chattingList)
.email(memberDto.getBody().getEmail())
.build();
}
/**
* ๋ฉ์ธ์ง ์ ์ก ๋ฉ์๋
* jwt ํ ํฐ์์ username ์ถ์ถ
* ์นดํ์นด ํ ํฝ์ผ๋ก ๋ฉ์ธ ์ ์ก
* @param : Message message, String accessToken
*/
public void sendMessage(Message message, String accessToken) {
// member id๋ก ์กฐํ
ResponseEntity<MemberDto> memberDto = plantServiceClient.findByUsername(tokenHandler.getUid(accessToken));
// ์ฑํ
๋ฐฉ์ ๋ชจ๋ ์ ์ ๊ฐ ์ฐธ์ฌ์ค์ธ์ง ํ์ธํ๋ค.
boolean isConnectedAll = chatRoomService.isAllConnected(message.getChatNo());
// 1:1 ์ฑํ
์ด๋ฏ๋ก 2๋ช
์ ์์ readCount 0, ํ๋ช
์ ์์ 1
Integer readCount = isConnectedAll ? 0 : 1;
// message ๊ฐ์ฒด์ ๋ณด๋ธ์๊ฐ, ๋ณด๋ธ์ฌ๋ memberNo, ๋๋ค์์ ์
ํ
ํด์ค๋ค.
message.setSendTimeAndSender(LocalDateTime.now(), memberDto.getBody().getId().intValue(), memberDto.getBody().getNickname(), readCount);
// ๋ณด๋ธ ์ฌ๋์ผ ๊ฒฝ์ฐ์๋ง ๋ฉ์์ง๋ฅผ ์ ์ฅ -> ์ค๋ณต ์ ์ฅ ๋ฐฉ์ง
if (message.getSenderEmail().equals(memberDto.getBody().getEmail())) {
// Message ๊ฐ์ฒด๋ฅผ ์ฑํ
์ํฐํฐ๋ก ๋ณํ
Chatting chatting = message.convertEntity();
// ์ฑํ
๋ด์ฉ์ ์ ์ฅ
Chatting savedChat = mongoChatRepository.save(chatting);
// ์ ์ฅ๋ ๊ณ ์ ID๋ฅผ ๋ฐํ
message.setId(savedChat.getId());
}
// ๋ฉ์์ง๋ฅผ ์ ์กํ๋ค.
sender.send(KafkaUtil.KAFKA_TOPIC, message);
}
/
/**
* ์ฐธ๊ฐ์ ์
์ฅ ์๋ฆผ ๋ฉ์๋
* @param : String email, Integer chatRoomNo
*/
public void updateMessage(String email, Integer chatRoomNo) {
Message message = Message.builder()
.contentType("notice")
.chatNo(chatRoomNo)
.content(email + " ๋์ด ๋์์ค์
จ์ต๋๋ค.")
.build();
sender.send(KafkaUtil.KAFKA_TOPIC, message);
}
/**
* ํ๋งค์๊ฐ ์ฐธ๊ฐํ ์ฑํ
๋ฐฉ์ด ์กด์ฌํ๋์ง ์ ๋ฌด ์ฒ๋ฆฌ ๋ฉ์๋
* ๋จ์ ์กฐํ์ฉ ๋ฉ์๋๋ผ readOnly = true
*
* @param : Integer tradeBoardNo, Integer memberNo
*/
@Transactional(readOnly = true)
public Boolean existChatRoomBySeller(Integer tradeBoardNo, Integer memberNo) {
return chatRepository.existChatRoomBySeller(tradeBoardNo, memberNo);
}
}
์ฝ๋์ ์ค์ํ ๋ด์ฉ์ ์ฃผ์ ๋ฌ์๋จ์ต๋๋ค!
์์์ ๋ง๋ ๋ง์ดํฌ๋ก ์๋น์ค๊ฐ์ ํต์ ์ ํ๊ธฐ ์ํด PlantServiceClient(FeignClient)
์์ ๋ฉ์๋๋ฅผ ํธ์ถํ๊ณ , ์ธ๋ถ api๊ฐ์ ์ฅ์ ๊ด๋ฆฌ๋ฅผ ํ๊ธฐ ์ํด CircuitBreaker
๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
KafkaUtil.KAFKA_TOPIC
์ ์ง์ ๋ ํ ํฝ์ผ๋ก message ๊ฐ์ฒด๋ฅผ ์ ์กํฉ๋๋ค.@Slf4j
@RestController
@RequiredArgsConstructor
public class ChatController {
private final ChatRoomService chatRoomService;
private final ChatService chatService;
@PostMapping("/chatroom")
public ResponseEntity<StatusResponseDto> createChatRoom(@RequestBody @Valid final ChatRequestDto requestDto, @RequestParam(required = false) Integer memberNo,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest().body(StatusResponseDto.addStatus(400));
}
// ์ฑํ
๋ฐฉ์ ๋ง๋ค์ด์ค๋ค.
Chat chat = chatService.makeChatRoom(memberNo, requestDto);
return ResponseEntity.ok(StatusResponseDto.addStatus(200, chat));
}
// ์ฑํ
๋ด์ญ ์กฐํ
@GetMapping("/chatroom/{roomNo}")
public ResponseEntity<ChattingHistoryResponseDto> chattingList(@PathVariable("roomNo") Integer roomNo, @RequestParam(required = false) Integer memberNo) {
ChattingHistoryResponseDto chattingList = chatService.getChattingList(roomNo, memberNo);
return ResponseEntity.ok().body(chattingList);
}
// ์ฑํ
๋ฐฉ ๋ฆฌ์คํธ ์กฐํ
@GetMapping("/chatroom")
public ResponseEntity<List<ChatRoomResponseDto>> chatRoomList(@RequestParam(value = "tradeBoardNo", required = false) Integer tradeBoardNo,
@RequestParam(value = "memberNo", required = false) Integer memberNo) {
List<ChatRoomResponseDto> chatList = chatService.getChatList(memberNo, tradeBoardNo);
return ResponseEntity.ok(chatList);
}
@MessageMapping("/message")
public void sendMessage(@Valid Message message, @Header("Authorization") final String accessToken) {
chatService.sendMessage(message, accessToken);
}
@MessageExceptionHandler
@SendTo("/error")
public String handleException(Exception e) {
return "WebSocket ๋ฉ์์ง ํธ๋ค๋ฌ์์ ์์ธ๊ฐ ๋ฐ์ํ์ต๋๋ค: " + e;
}
// ์ฑํ
๋ฐฉ ์ ์ ๋๊ธฐ
@PostMapping("/chatroom/{chatroomNo}")
public ResponseEntity<HttpStatus> disconnectChat(@PathVariable("chatroomNo") Integer chatroomNo,
@RequestParam(value = "nickname", required = false) String nickname) {
chatRoomService.disconnectChatRoom(chatroomNo, nickname);
return ResponseEntity.ok(HttpStatus.ACCEPTED);
}
// ํ๋งค์๊ฐ ์ฐธ๊ฐํ ์ฑํ
๋ฐฉ ์กด์ฌํ๋์ง ์กฐํ
@GetMapping("/chatroom/exist/seller")
public Boolean existChatRoomBySeller(@RequestParam Integer tradeBoardNo, @RequestParam Integer memberNo) {
return chatService.existChatRoomBySeller(tradeBoardNo, memberNo);
}
}
์ด๋ฒ ํฌ์คํ ์์ ์ฑํ ๊ธฐ๋ฅ ๊ตฌํ ๊ณผ์ ์ ํฌ์คํ ํด๋ดค์ต๋๋ค. ๋ค์ ํฌ์คํ ์์๋ SSE๋ฅผ ํ์ฉํ ์๋ฆผ ๊ธฐ๋ฅ์ ๊ดํด ํฌ์คํ ํ๋๋ก ํ๊ฒ ์ต๋๋ค!!
https://velog.io/@joonghyun/Springboot-MongoDB-Springboot%EC%99%80-MongoDB-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0
https://velog.io/@ch4570/Stomp-Kafka%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%B1%84%ED%8C%85-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0-with-Spring-Boot-2-Kafka-%EC%84%A4%EC%B9%98-MongoDB-Stomp-%EC%84%A4%EC%A0%95
๋ง๋งํ๋๋ฐ ๋๋ฌด ์ ๊ณต๋ถํ๊ตฌ ๊ฐ๋๋ค ใ ใ