Spring Webflux/Netty/MongoDB๋กœ ์ฑ„ํŒ… ์„œ๋ฒ„ ๊ตฌํ˜„
Web/Java (Spring+JSP)

Spring Webflux/Netty/MongoDB๋กœ ์ฑ„ํŒ… ์„œ๋ฒ„ ๊ตฌํ˜„

  • ๐Ÿ“Œ Goal: Spring Webflux์™€ MongoDB๋ฅผ ํ™œ์šฉํ•ด ์–‘๋ฐฉํ–ฅ์œผ๋กœ ์†Œํ†ตํ•  ์ˆ˜ ์žˆ๋Š” ์ฑ„ํŒ… ์„œ๋ฒ„๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค

1. ํ™˜๊ฒฝ์„ค์ •

  • MongoDB
    • ์™ธ๋ถ€์—์„œ๋„ DB์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก cloud cluster๋ฅผ ํ•˜๋‚˜ ๊ตฌ์„ฑํ•˜๊ณ , Database์™€ collection์„ ์ƒ์„ฑํ•œ๋‹ค. RDBMS์—์„œ์˜ Schema์™€ Table์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค.

    • MongoDB์—์„œ connection string์„ ์ œ๊ณตํ•˜๋ฏ€๋กœ ๋ณต์‚ฌํ•ด๋‘๊ณ , ์ถ”ํ›„ yml ํŒŒ์ผ ์„ค์ • ์‹œ์— ์‚ฌ์šฉํ•œ๋‹ค.
    mongodb+srv://<username>:<password>@<cluster-address>/<database-name>?retryWrites=true&w=majority
    
  • Spring Boot
    • Reactive-web๊ณผ MongoDB๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก Gradle์„ ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ๋‹ค.
      • Reactive web์—์„œ๋Š” default ์„œ๋ฒ„๋กœ netty๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ์ถ”๊ฐ€์ ์œผ๋กœ ์„ค์ •ํ•  ๋ถ€๋ถ„์€ ์—†๋‹ค.
    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
    • applications.yml ํŒŒ์ผ์— connection string์„ ์ €์žฅํ•ด๋‘๊ณ  spring boot์—์„œ mongoDB๋กœ ๋ฐ”๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.
    spring:
    	data:
    	    mongodb:
    	      uri: <connection string>
    
    • ์ด๋ ‡๊ฒŒ ๊ตฌ์„ฑํ•ด๋‘” ์„ค์ •์ •๋ณด๋ฅผ ํ†ตํ•ด ์ถ”ํ›„์— spring boot์—์„œ mongoTemplate๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ CRUDํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

 

2. Netty๋ž€?

  • ์šฐ์„  Spring 5.X framework์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋‘๊ฐ€์ง€์˜ web stack์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์ž.
  • Spring Framework5๋Š” Servlet Stack, Reactive Stack์ด๋ผ๋Š” ๋‘ ๊ฐ€์ง€ ์›น ์Šคํƒ์„ ์ œ๊ณตํ•œ๋‹ค.
    • ํ•˜๋‚˜๋Š” ๋Œ€๋ถ€๋ถ„์˜ Java ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‚ฌ์šฉํ•˜๋Š” ์ฐจ๋‹จ I/O๊ฐ€ ์žˆ๋Š” ๊ณ ์ „์ ์ธ 'Servlet Stack' ์ด๋‹ค. Servlet Stack์€ Spring MVC ๋ฐ Spring Data๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์œผ๋ฉฐ, Tomcat, Jetty, Servlet Container์—์„œ ๋™์ž‘ํ•œ๋‹ค.
    • ๋‹ค๋ฅธ ํ•˜๋‚˜๋Š” ์ด๋ฒคํŠธ ๋ฃจํ”„, ๋น„์ฐจ๋‹จ ์‹คํ–‰ ๋ชจ๋ธ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜์—ฌ ๋” ์ ์€ ํ•˜๋“œ์›จ์–ด ๋ฆฌ์†Œ์Šค๋กœ ๋†’์€ ๋™์‹œ์„ฑ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” 'Reactive Stack' ์œผ๋กœ, Reactive Stack์€ Spring WebFlux์™€ Spring Data ๋ฐ˜์‘ํ˜• ์ €์žฅ์†Œ(Reactive Repositories)๋ฅผ ํ™œ์šฉํ•œ๋‹ค. Reactive Stack์€ Tomcat, Jetty ์™ธ์—๋„ Servlet 3.1+ Container์™€ Netty, Undertow ๊ฐ™์€ ๋…ผ๋ธ”๋กœํ‚น ์„œ๋ฒ„ ์œ„์—์„œ ๊ตฌ๋™๋œ๋‹ค.

  • ๊ธฐ๋ณธ์ ์œผ๋กœ Servlet ๊ธฐ๋ฐ˜์˜ ์Šคํ”„๋ง์€ ์‚ฌ์šฉ์ž๊ฐ€ request๋ฅผ ํ•  ๋•Œ๋งˆ๋‹ค ์Šค๋ ˆ๋“œ๊ฐ€ ๋งŒ๋“ค์–ด์ง„๋‹ค. 
    • ๋”ฐ๋ผ์„œ ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๊ฐ€ ๋™์‹œ์— ์š”์ฒญ์„ ํ•˜๊ฒŒ ๋˜๋ฉด ์Šค๋ ˆ๋“œ๊ฐ€ ๊ณ„์† ์ƒ์„ฑ๋˜๋ฉด์„œ ๊ฐœ์ˆ˜๊ฐ€ ๋Š˜์–ด๋‚˜๊ฒŒ ๋˜๊ณ , ์„œ๋ฒ„์— ๊ณผ๋ถ€ํ•˜๊ฐ€ ๊ฑธ๋ฆฌ๊ฒŒ ๋œ๋‹ค.
    • ๋”ฐ๋ผ์„œ Thread ๊ธฐ๋ฐ˜ ์„œ๋ฒ„๋“ค์€ ์„œ๋ฒ„ ํ•˜๋‚˜๊ฐ€ ์‹œ๊ฐ„์„ ์ชผ๊ฐœ์„œ(time slicing) ์™”๋‹ค๊ฐ”๋‹ค ํ•˜๋ฉฐ(context switching) ์—ฌ๋Ÿฌ๊ฐ€์ง€ ์ผ์„ ํ•˜๊ฒŒ ๋˜๋Š”๋ฐ, ์ด๊ฐ€ ์ž์ฃผ ๋ฐœ์ƒํ•˜๋ฉด ์›น ์„œ๋ฒ„๊ฐ€ ๋Š๋ ค์ง€๋Š” ๊ฒฝํ–ฅ์ด ์žˆ๋‹ค.
  • ํ•˜์ง€๋งŒ Netty๋Š” ๋น„๋™๊ธฐ ์„œ๋ฒ„์ด๋ฏ€๋กœ, ์ƒ๋‹นํžˆ ๋น ๋ฅด๋‹ค. 
    • ๋งŒ์•ฝ ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์„œ๋ฒ„์— ์š”์ฒญ์„ ํ•˜๊ณ  , ์ด ์„œ๋ฒ„๊ฐ€ DB๋กœ ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ ค ์‘๋‹ต์ด ๋Œ์•„์˜ค๊ธฐ๊นŒ์ง€ 3์ดˆ์ •๋„๊ฐ€ ๊ฑธ๋ฆฐ๋‹ค๊ณ  ๊ฐ€์ •ํ–ˆ์„๋•Œ, ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์˜ ์ƒˆ๋กœ์šด ์š”์ฒญ์ด ๋“ค์–ด์˜จ๋‹ค๋ฉด A์˜ ์‘๋‹ต์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋™์•ˆ B์˜ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•ด์ค€๋‹ค. 
    • ์ด๋ ‡๊ฒŒ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์‹œ๊ฐ„๋™์•ˆ ํ•  ์ˆ˜ ์žˆ๋Š” ์ผ์„ ์ฐพ์•„์„œ ๋น„๋™๊ธฐ์ ์œผ๋กœ task๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋œ๋‹ค. ์ฆ‰ ๋น„๋™๊ธฐ ์„œ๋ฒ„๋Š” ์‹ฑ๊ธ€ ์Šค๋ ˆ๋“œ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋Š” ๋Œ€์‹  idleํ•œ ์‹œ๊ฐ„์ด ์—†๊ณ , ๋งค์šฐ ๋นจ๋ผ์ง€๊ฒŒ ๋œ๋‹ค.
    • ํ•˜์ง€๋งŒ ์ด ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์—ญ์‹œ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•˜๋Š” ๋น„๋™๊ธฐ DB๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

 

3. Tailable - Repository & Model ๊ตฌํ˜„ 

@Data
@Document(collection="chat-app")
public class Chat {
    @Id
    private String id;
    private String msg;
    private String sender;
    private String receiver;
    private LocalDateTime createdAt;
}
  • Chat model์„ ๊ตฌํ˜„ํ•œ๋‹ค. MongoDB์—๋Š” document ํ˜•์‹์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฝ์ž…ํ•œ๋‹ค.
public interface ChatRepository extends ReactiveMongoRepository {
    @Tailable
    @Query("{sender:?0,receiver:?1}") //ํ•ด๋‹น ์ฟผ๋ฆฌ๊ฐ€ ๋™์ž‘ํ•˜๊ฒŒ ๋จ.
    Flux<Chat> mFindBySender(String sender, String receiver);
    //Flux- ๋ฐ์ดํ„ฐ์˜ ํ๋ฆ„,๋Š๊ธฐ์ง€ ์•Š๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ์ง€์†์ ์œผ๋กœ ๋ฐ›๊ฒ ๋‹ค๋Š” ์˜๋ฏธ
    
    @Tailable
    @Query("{ roomNum: ?0 }") //๋ฐฉ ๋‹จ์œ„๋กœ ์กฐํšŒํ•œ๋‹ค
    Flux<Chat> mFindByRoomNum(Integer roomNum);
}
  • client๊ฐ€ controller๋ฅผ ํ†ตํ•ด์„œ sender๊ฐ€ ๋‚˜์ธ ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ๋Š” Tailable Query๋ฅผ ์š”์ฒญํ–ˆ์„๋•Œ, ๋‹ค๋ฅธ ํด๋ผ์ด์–ธํŠธ๊ฐ€ sender๊ฐ€ ๋‚˜์ธ ๋ฐ์ดํ„ฐ๋ฅผ DB์— ์ƒˆ๋กœ ์‚ฝ์ž…ํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜์ž.
  • ์—ฌ๊ธฐ์— ์ปค์„œ๋ฅผ ๋‹ซ์ง€ ์•Š๊ณ  ๊ณ„์† ์œ ์ง€ํ•˜๊ณ  ์ตœ์‹  DB ๋‚ด์šฉ์„ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋˜๋Š” @Tailable ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๋ฉด Flux๋ฅผ ํ†ตํ•ด์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ๊ณ„์† ํ˜๋Ÿฌ๋“ค์–ด์˜ค๊ฒŒ ๋˜๋ฏ€๋กœ ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ๋ฐ์ดํ„ฐ๊นŒ์ง€ ๋ฐ˜ํ™˜์ด ๋œ๋‹ค.

 

4. SSE Protocol- Controller ๊ตฌํ˜„

  • SSE ํ”„๋กœํ† ์ฝœ์€ Server-Sent Events์˜ ์•ฝ์–ด๋กœ, 1ํšŒ์„ฑ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›์€ ๋’ค ์—ฐ๊ฒฐ์„ ์ข…๋ฃŒํ•˜๋Š” ์ผ๋ฐ˜์ ์ธ HTTP์™€๋Š” ๋‹ค๋ฅด๊ฒŒ ์ตœ์ดˆ ์—ฐ๊ฒฐ ์ดํ›„์— ์„œ๋ฒ„์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ง€์†์ ์œผ๋กœ, ์‹ค์‹œ๊ฐ„์œผ๋กœ streamingํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค. (์„œ๋ฒ„๊ฐ€ ์ผ๋ฐฉ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์†ก์‹ ํ•˜๋Š” ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ ) SSE๋Š” ๊ธฐ์กด HTTP ์„œ๋ฒ„์—์„œ HTTP API ๋งŒ์œผ๋กœ ๋™์ž‘๋˜๋ฉฐ, ๊ตฌํ˜„๋„ Websocket๋ณด๋‹ค ๊ฐ„๋‹จํ•˜๋‹ค. ๋˜ํ•œ Topic์„ ์ •ํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ๊ตฌ๋…ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์ฃผ๋กœ Server์—์„œ ์•Œ๋žŒ์„ ํ‘ธ์‹œํ•˜๊ฑฐ๋‚˜, SNS๋“ฑ์—์„œ ์ƒˆ๋กœ์šด ํ”ผ๋“œ๋‚˜ ๊ฒŒ์‹œ๋ฌผ์„ ๋ฐ›์•„์˜ฌ ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.

+) Websocket vs Server-Side-Event

https://surviveasdev.tistory.com/m/entry/%EC%9B%B9%EC%86%8C%EC%BC%93-%EA%B3%BC-SSEServer-Sent-Event-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B3%A0-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0

 

์›น์†Œ์ผ“ ๊ณผ SSE(Server-Sent-Event) ์ฐจ์ด์  ์•Œ์•„๋ณด๊ณ  ์‚ฌ์šฉํ•ด๋ณด๊ธฐ

์ตœ๊ทผ์— ์–ด๋–ค ์ด๋ฒคํŠธ๊ฐ€ ์ƒ๊ฒผ์„ ๋•Œ client side์— ui๋ฅผ ์—…๋ฐ์ดํŠธํ•ด์•ผ ๋˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด์•ผ ๋์—ˆ์Šต๋‹ˆ๋‹ค. ์ฒ˜์Œ์—๋Š” ์ด๋Ÿฐ ๊ฒฝ์šฐ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด socket ๋ฐ–์— ๋ชฐ๋ผ์„œ socket.io๋ฅผ ์‚ฌ์šฉํ•ด์„œ socket์œผ๋กœ ๋งŒ

surviveasdev.tistory.com

 

  • SSE๋Š” ๊ธฐ์กด์˜ ์›น ์„œ๋ฒ„ ํ†ต์‹  ๋ฐฉ์‹์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ๋”ฐ๋กœ ์„ค์ •ํ•  ๊ฒƒ์ด ์—†๋‹ค. ๋”ฐ๋ผ์„œ ์„œ๋ฒ„์ชฝ์—์„œ๋Š” SSE๋ฅผ ์‚ฌ์šฉํ•˜๊ฒ ๋‹ค๋Š” ํ—ค๋”๋งŒ์„ ์„ค์ •ํ•˜๋ฉด ๋˜๋ฉฐ, ์ด๋Š” MediaType์„ text_event_stream_value๋กœ ์ง€์ •ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.
  • Controller๋Š” Restfulํ•˜๊ฒŒ ๊ตฌํ˜„ํ•˜์˜€๋‹ค.
@RequiredArgsConstructor
@RestController //๋ฐ์ดํ„ฐ ๋ฆฌํ„ด ์„œ๋ฒ„
public class ChatController {
    private final ChatRepository chatRepository;
	
    @CrossOrigin
    @GetMapping(value="/sender/{sender}/receiver/{receiver}",produces= MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<Chat> getMsg(@PathVariable String sender,
                             @PathVariable String receiver){
        return chatRepository.mFindBySender(sender,receiver)
                .subscribeOn(Schedulers.boundedElastic());
    }

    @PostMapping("/chat")
    public Mono<Chat> setMsg(@RequestBody Chat chat){
        chat.setCreatedAt(LocalDateTime.now());
        return chatRepository.save(chat);
    }
    
    @CrossOrigin //๋‹จ์ฒด ์ฑ„ํŒ…๋ฐฉ ๊ตฌํ˜„
	@GetMapping(value = "/app/chats/chatrooms/{roomNum}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
	public Flux<Chat> findByRoomNum(@PathVariable Integer roomNum) {
		return chatRepository.mFindByRoomNum(roomNum)
				.subscribeOn(Schedulers.boundedElastic());
	}
}
  • /chat: ์ฑ„ํŒ…์„ ์ „์†กํ•˜๋ฉด ์ด๋ฅผ ์ž๋™์œผ๋กœ MongoDB์— ์‚ฝ์ž…ํ•œ๋‹ค.

  • /sender/{name}/receiver/{name}: ํ•ด๋‹น url๋กœ ์ ‘๊ทผํ•˜๋ฉด path variable์˜ sender/receiver์— ๋”ฐ๋ผ์„œ ์†ก์ˆ˜์‹ ์ž๊ฐ€ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„๋œ ๋ฉ”์„ธ์ง€๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๊ณ , ๋˜ํ•œ ์„œ๋ฒ„๋Š” ๋ฉˆ์ถ”์ง€ ์•Š๊ณ  ๊ณ„์† ์ƒˆ๋กœ ์‚ฝ์ž…๋˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๊ฒŒ ๋œ๋‹ค.

  • /chat/roomNum/{roomNum} : ํ•ด๋‹น url๋กœ ์ ‘๊ทผํ•˜๋ฉด ์ฑ„ํŒ…๋ฐฉ ์ „์ฒด๋ฅผ ์ˆ˜์‹ ์ž๋กœ ํ•˜์—ฌ ์ฑ„ํŒ…์ด ์ €์žฅ๋˜๋ฉฐ, ๋ˆ„๊ฐ€ ๋ณด๋ƒˆ๋Š”์ง€๋งŒ ์ •ํ™•ํžˆ ๋ช…์‹œํ•˜๋ฉด ๋œ๋‹ค. ์ฃผ๋กœ ๋‹จ์ฒด ์ฑ„ํŒ…๋ฐฉ์„ ๊ตฌํ˜„ํ•  ๋•Œ ์‚ฌ์šฉ๋œ๋‹ค.

 

5. ์ฑ„ํŒ…๋ฐฉ ๋ทฐ ๊ตฌํ˜„ 

  • Api ๊ตฌํ˜„ ๋งŒ์œผ๋กœ๋Š” ์ฑ„ํŒ…์ด ์ œ๋Œ€๋กœ ์ด๋ฃจ์–ด์ง€๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธ์ด ์–ด๋ ค์› ๊ธฐ ๋•Œ๋ฌธ์— ๋ฐฑ์—”๋“œ ํŒŒํŠธ์—์„œ ์ฑ„ํŒ…๋ฐฉ ๋ทฐ๊นŒ์ง€ ๊ตฌํ˜„ํ•˜๊ณ , api๋ฅผ ์—ฐ๋™ํ•˜๋ฉด์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜์˜€๋‹ค.
  • ์ฑ„ํŒ…๋ฐฉ index, ๋‚˜์˜ index, ์ด๋ฆ„, ์ƒ๋Œ€๋ฐฉ ์ด๋ฆ„ ์„ url parameter์— ๋‹ด์•„ requestํ•˜๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด์˜ค๋Š” api๋ฅผ ์ ์šฉํ•˜์—ฌ ์ด์ „ ์ฑ„ํŒ… ๋‚ด์—ญ์„ ์กฐํšŒํ•˜๊ณ , ๋‚˜์™€ ์ƒ๋Œ€๋ฐฉ์„ ๋ถ„๋ณ„ํ•˜๋ฉฐ, ๋ˆ„๊ฐ€ ์–ด๋–ค ์ฑ„ํŒ…๋ฐฉ์— ์ฑ„ํŒ…์„ ๋ณด๋ƒˆ๋Š”์ง€ ๋ช…์‹œํ•˜์—ฌ DB์— ์ฑ„ํŒ…์„ ์ €์žฅํ•œ๋‹ค.
  • ๋˜ํ•œ ์•„๋ž˜ ์ „์†ก์ฐฝ์—์„œ ์ฑ„ํŒ…์„ ๋ณด๋‚ด๋ฉด DB์— ์ฑ„ํŒ…์ด ์ €์žฅ๋˜๋Š”๋ฐ, ์ด๋•Œ ์ฑ„ํŒ…๋ฐฉ์„ ์กฐํšŒํ•˜๋Š” api(Server)์— ์ƒˆ๋กญ๊ฒŒ ๋ฐ์ดํ„ฐ๊ฐ€ ํ˜๋Ÿฌ๋“ค์–ด์™€ event๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ฑ„ํŒ…์ฐฝ์„ ์ดˆ๊ธฐํ™”ํ•˜์—ฌ ์ƒˆ๋กญ๊ฒŒ ๋ Œ๋”๋งํ•ด ์ƒˆ๋กœ์šด ์ฑ„ํŒ…๊นŒ์ง€ ๋ทฐ์— ์ถ”๊ฐ€ํ•˜์—ฌ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ฑ„ํŒ…์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€๋‹ค.
const searchParams=new URLSearchParams(location.search);


let userIdx=searchParams.get('user-idx') //์œ ์ € ์ธ๋ฑ์Šค 
let userName=searchParams.get('user-name'); //ํ˜„์žฌ ์œ ์ € ์ด๋ฆ„
let roomNum=searchParams.get('room-num') // ์ฑ„ํŒ…๋ฐฉ ๋ฒˆํ˜ธ 
let otherName=searchParams.get('other-name')// ์ƒ๋Œ€๋ฐฉ ์ด๋ฆ„
console.log(userIdx)

document.querySelector("#username").innerHTML = otherName;
//SSE ์—ฐ๊ฒฐ - DB์— ์ƒˆ๋กœ์šด ์ฑ„ํŒ… ๋‚ด์—ญ์ด ๋“ค์–ด์˜ฌ๋•Œ๋งˆ๋‹ค event ๋ฐœ์ƒ (์ดˆ๊ธฐํ™”)
let eventSite= "http://{aws ip ์ฃผ์†Œ}" //spring application์ด ๋Œ์•„๊ฐ€๋Š” ์ฃผ์†Œ ex) "http://localhost:9001"
const eventSource= new EventSource(`${eventSite}/chatrooms/${roomNum}`); 

 // ๊ณ„์† ๋ฐ์ดํ„ฐ๊ฐ€ ํ˜๋Ÿฌ๋“ค์–ด์˜ค๋ฏ€๋กœ postman์—์„œ ํ…Œ์ŠคํŠธ X, ๊ทธ๋ƒฅ url ๊ทธ๋Œ€๋กœ ์ ‘์†



eventSource.onmessage=(event)=>{
    const data=JSON.parse(event.data);
    //์ƒ๋Œ€๋ฐฉ์œผ๋กœ๋ถ€ํ„ฐ ๋ฉ”์„ธ์ง€๊ฐ€ ์ „์†ก๋˜๋Š” ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด DB์— ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜จ๋‹ค.
    if(data.sender_idx==userIdx){ //์ „์†ก์ž๊ฐ€ ๋‚ด๊ฐ€ ์•„๋‹ˆ๋ฉด ๋‹ค๋ฅธ์‚ฌ๋žŒ์ด๋ฏ€๋กœ ๋ฐ˜๋Œ€์ชฝ์— ๋ Œ๋”๋งํ•˜๋ฉด ๋จ
        //ํŒŒ๋ž€๋ฐ•์Šค (๋‚ด๊ฐ€ ๋ณด๋‚ธ ๋ฉ”์„ธ์ง€)
        initMyMessage(data);
    }
    else{
        //ํšŒ์ƒ‰๋ฐ•์Šค (์ƒ๋Œ€๋ฐฉ์ด ๋ณด๋‚ธ ๋ฉ”์„ธ์ง€)
        initYourMessage(data);
    }
    

}

function getSendMsgBox(data) {

    let md = data.createdAt.substring(5, 10)
	let tm = data.createdAt.substring(11, 16)
	convertTime = tm + " | " + md
    //๋‚ด๊ฐ€ ์ž…๋ ฅํ•œ ์ฑ„ํŒ… ๋ฐ•์Šค ์ƒ์„ฑํ•˜๊ธฐ
    return `
    <div class="sent_msg">
    <p>${data.msg}</p>
    <span class="time_date"> ${convertTime} / <b>${data.sender_name}</b> </span>
    </div>
    `;
}

function getReceivedMsgBox(data) {

    let md = data.createdAt.substring(5, 10)
	let tm = data.createdAt.substring(11, 16)
	convertTime = tm + " | " + md
    //์ƒ๋Œ€ํŽธ์—์„œ ๋ณด๋‚ธ ์ฑ„ํŒ… ๋ฐ•์Šค ์ƒ์„ฑํ•˜๊ธฐ
    return `
    <div class="received_withd_msg">
    <p>${data.msg}</p>
    <span class="time_date"> ${convertTime} / <b>${data.sender_name}</b></span>
    </div>
    `;
}

//์ตœ์ดˆ ์ฑ„ํŒ…๋ฐฉ ๋กœ๋“œ์‹œ ์ด์ „ DB ์ €์žฅ ๋‚ด์—ญ ๋ถˆ๋Ÿฌ์˜ค๋ฉฐ ์ดˆ๊ธฐํ™”

function initMyMessage(data){
    //๋‚ด๊ฐ€ ๋ณด๋‚ธ ๋ฉ”์„ธ์ง€ ์ดˆ๊ธฐํ™”
    let chatBox= document.querySelector("#chat-box");
   
   let chatOutGoingBox =document.createElement("div");
   chatOutGoingBox.className="outgoing_msg";
   
   chatOutGoingBox.innerHTML=getSendMsgBox(data);
    chatBox.append(chatOutGoingBox);
    document.documentElement.scrollTop = document.body.scrollHeight;
  
}

function initYourMessage(data){
    //์ƒ๋Œ€ ์ชฝ์œผ๋กœ๋ถ€ํ„ฐ ๋ฉ”์„ธ์ง€๊ฐ€ ์ „์†ก๋˜๋ฉด DB๋กœ๋ถ€ํ„ฐ ๋ถˆ๋Ÿฌ์˜ด
    let chatBox= document.querySelector("#chat-box");
   
   let chatReceivedBox =document.createElement("div");
   chatReceivedBox.className="received_msg";
  
   chatReceivedBox.innerHTML=getReceivedMsgBox(data);
    chatBox.append(chatReceivedBox);
    document.documentElement.scrollTop = document.body.scrollHeight;
}


//AJAX๋กœ ์ฑ„ํŒ… ๋ฉ”์„ธ์ง€ ์ „์†ก

async function newChat(){
    //DB์— insertํ•˜๋ฉด ์ž๋™์œผ๋กœ event๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด์„œ chatroom์˜ ๋‹ค๋ฅธ ์ƒ๋Œ€๋ฐฉ์—๊ฒŒ ๋ณด๋‚ด์ง (๋‹ค์ค‘ ์ฑ„ํŒ…๋„ ๊ฐ€๋Šฅ)
  
    let msgInput=document.querySelector("#chat-outgoing-msg");

   
   let date=new Date();
   let now= date.getHours()+":"+date.getMinutes()+" | "+date.getMonth()+"- "+date.getDate();

   let chat={
       sender_idx:userIdx,
       sender_name:userName,
       room_num:roomNum,
       msg: msgInput.value
   };

    fetch(`${eventSite}`,{
       method:"post",//http post ๋ฉ”์†Œ๋“œ (์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ writeํ• ๋•Œ ์‚ฌ์šฉ)
       body:JSON.stringify(chat),
       headers:{
           "Content-Type":"application/json; charset=utf-8"
       }

   });

    msgInput.value="";
}

//์ฑ„ํŒ… ์ „์†ก ๋ฒ„ํŠผ ๋ˆ„๋ฅผ ์‹œ
document.querySelector("#chat-send").addEventListener("click",()=>{
   newChat();
})


//enter ๋ˆ„๋ฅผ ์‹œ
document.querySelector("#chat-outgoing-msg").addEventListener("keydown",(e)=>{
   if(e.keyCode==13){
       newChat();
   }
})

 

- ์ „์ฒด ์ฝ”๋“œ๋Š” ํ•˜๋‹จ ๊นƒํ—ˆ๋ธŒ ์ฃผ์†Œ ์ฐธ๊ณ 

https://github.com/ashlovesliitea/silverlining-chatapp

 

GitHub - ashlovesliitea/silverlining-chatapp

Contribute to ashlovesliitea/silverlining-chatapp development by creating an account on GitHub.

github.com

 

 

๐Ÿ™ ์ฐธ๊ณ  ์ž๋ฃŒ

 

GitHub - codingspecialist/springboot-webflux-mongo-chatapp

Contribute to codingspecialist/springboot-webflux-mongo-chatapp development by creating an account on GitHub.

github.com

 

Spring Event์™€ SSE ๋กœ ๋ฆฌ์•กํ‹ฐ๋ธŒํ•˜๊ฒŒ ์ ‘๊ทผํ•˜๊ธฐ (EventListener, Server-Sent Events, ๋น„๋™๊ธฐ ์ปจํŠธ๋กค๋Ÿฌ, RxJava๋กœ

๋ณธ๊ฒฉ์ ์ธ ๋ฆฌ์•กํ‹ฐ๋ธŒ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์ „์—, ์˜ต์ €๋ฒ„ ํŒจํ„ด - ๋ฐœํ–‰๊ตฌ๋… ํŒจํ„ด์œผ๋กœ ๋งŒ๋“ค์–ด์ง„ Spring Event์™€ SSE๋ฅผ ํ†ตํ•ด ๋ฆฌ์•กํ‹ฐ๋ธŒ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค์–ด๋ณด์ž cf) ์˜ต์ €๋ฒ„ ํŒจํ„ด ํฌ์ŠคํŒ… : https://sjh836.tis

sjh836.tistory.com