์น์์ผ์ TCP ์์ ์๋ ์๊ณ ๊ฐ๋ฒผ์ด ๊ณ์ธต์ ๋๋ค. ๋ฐ๋ผ์ ๋ฉ์์ง๋ฅผ ํฌํจํ๊ธฐ ์ํด "ํ์ ํ๋กํ ์ฝ"์ ์ฌ์ฉํ๋ ๋ฐ ์ ํฉํฉ๋๋ค. ์ด ํฌ์คํธ์์๋ Spring๊ณผ ํจ๊ป STOMP ๋ฉ์์ง์ ์ฌ์ฉํ์ฌ ๋ํํ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ญ๋๋ค. STOMP๋ ํ์ ์์ค WebSocket ์์์ ์๋ํ๋ ํ์ ํ๋กํ ์ฝ์ ๋๋ค.
spring.io์ ํฌ์คํธ๋ฅผ ์ฐธ๊ณ ํ์ฌ ์์ฑํ์์ต๋๋ค.
๋ง๋ค ๊ฒ
์ฌ์ฉ์ ์ด๋ฆ์ด ํฌํจ๋ ๋ฉ์์ง๋ฅผ ์๋ฝํ๋ ์๋ฒ๋ฅผ ๊ตฌ์ถํฉ๋๋ค. ์ด์ ๋ํ ์๋ต์ผ๋ก ์๋ฒ๋ ํด๋ผ์ด์ธํธ๊ฐ ๊ฐ์ ํ ๋๊ธฐ์ด์ ์ธ์ฌ๋ง์ ํธ์ํฉ๋๋ค.
์๊ตฌ ํ๊ฒฝ
Java 17 or later
Gradle 7.5+ or Maven 3.5+
๋น๋ ํ๊ฒฝ
$ ./gradlew -v
------------------------------------------------------------
Gradle 7.6
------------------------------------------------------------
Build time: 2022-11-25 13:35:10 UTC
Revision: daece9dbc5b79370cc8e4fd6fe4b2cd400e150a8
Kotlin: 1.7.10
Groovy: 3.0.13
Ant: Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM: 17.0.6 (Eclipse Adoptium 17.0.6+10)
OS: Mac OS X 13.0 aarch64
Gradle Dependency
build.gradle.kts์ ๋ค์์ ์ถ๊ฐ
implementation("org.springframework.boot:spring-boot-starter-websocket")
implementation("org.webjars:webjars-locator-core")
implementation("org.webjars:sockjs-client:1.0.2")
implementation("org.webjars:stomp-websocket:2.3.3")
implementation("org.webjars:bootstrap:3.3.7")
implementation("org.webjars:jquery:3.1.1-1")
STOMP ๋ฉ์์ง ์๋น์ค ํ์ ๊ตฌ์ฑ ์์
๋ฆฌ์์ค ํํ ํด๋์ค ์์ฑ
์ด ์๋น์ค๋ ๋ณธ๋ฌธ์ด JSON ๊ฐ์ฒด์ธ STOMP ๋ฉ์์ง์ ์ด๋ฆ์ด ํฌํจ๋ ๋ฉ์์ง๋ฅผ ๋ฐ์ต๋๋ค.
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class HelloMessage {
private String name;
}
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class Greeting {
private String content;
}
๋ฉ์์ง ์ฒ๋ฆฌ ์ปจํธ๋กค๋ฌ ์์ฑ
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.util.HtmlUtils;
@Controller
public class GreetingController {
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(1000); // simulated delay 1s.
return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
}
}
STOMP ๋ฉ์์ง์ ์ํ ์คํ๋ง ์ค์
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// ๊ฐ๋จํ ๋ฉ๋ชจ๋ฆฌ ๊ธฐ๋ฐ ๋ฉ์์ง ๋ธ๋ก์ปค๊ฐ /topic ์ ๋์ฌ๊ฐ ๋ถ์ ๋์์์ ์ธ์ฌ๋ง ๋ฉ์์ง๋ฅผ ํด๋ผ์ด์ธํธ๋ก ๋ค์ ์ ๋ฌํ ์ ์๋๋ก ์ค์
config.enableSimpleBroker("/topic");
// @MessageMapping์ด ๋ฌ๋ฆฐ ๋ฉ์๋์ ๋ฐ์ธ๋ฉ๋ ๋ฉ์์ง์ ๋ํด /app ์ ๋์ฌ ์ง์
// ์ด ์ ๋์ฌ๋ ๋ชจ๋ ๋ฉ์์ง ๋งคํ์ ์ ์ํ๋๋ฐ ์ฌ์ฉ๋๋ค
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// /gs-guide-websocket๋ผ๋ ์๋ํฌ์ธํธ๋ฅผ ๋ฑ๋กํ์ฌ ์น์์ผ์ ์ฌ์ฉํ ์ ์๋ ๊ฒฝ์ฐ
// ๋์ฒด ์ ์ก์ ํ ์ ์๋๋ก SockJS ํด๋ฐฑ ์ต์
์ ํ์ฑํ
// ์ฌ์ฉ ๊ฐ๋ฅํ ์ต์์ ์ ์ก(์น์์ผ, xhr-streaming, xhr-polling ๋ฑ)
registry.addEndpoint("/gs-guide-websocket").withSockJS();
}
}
ํด๋ผ์ด์ธํธ
๋ฉ์์ง๋ฅผ ์ฃผ๊ณ ๋ฐ์ ์๋ฐ์คํฌ๋ฆฝํธ ํด๋ผ์ด์ธํธ
index.html๊ณผ main.css๋ ์ด ํฌ์คํธ์ ๊ด์ฌ์ฌ๊ฐ ์๋๋ฏ๋ก ๊ณต์ ๊นํ ๋ ํฌ์์ Copy&Paste ํ์ต๋๋ค.
index.html
์ ์ ๊น ๋ณด๋ฉด, ์น์์ผ์ ํตํด STOMP๋ฅผ ํตํด ์๋ฒ์ ํต์ ํ๋ ๋ฐ ์ฌ์ฉ๋๋ SockJS ๋ฐ STOMP ์๋ฐ์คํฌ๋ฆฝํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๊ฐ์ ธ์ต๋๋ค. ๋ํ ํด๋ผ์ด์ธํธ ์ ํ๋ฆฌ์ผ์ด์
์ ๋ก์ง์ด ํฌํจ๋ app.js๋ ๊ฐ์ ธ์ต๋๋ค.
app.js
var stompClient = null;
function setConnected(connected) {
$("#connect").prop("disabled", connected);
$("#disconnect").prop("disabled", !connected);
if (connected) {
$("#conversation").show();
} else {
$("#conversation").hide();
}
$("#greetings").html("");
}
/**
* SockJS์ stomp.js๋ฅผ ์ฌ์ฉํ์ฌ SockJS ์๋ฒ๊ฐ ์ฐ๊ฒฐ์ ๊ธฐ๋ค๋ฆฌ๋ /gs-guide-websocket์ ๋ํ ์ฐ๊ฒฐ์ ์ฝ๋๋ค.
* ์ฐ๊ฒฐ์ ์ฑ๊ณตํ๋ฉด ํด๋ผ์ด์ธํธ๋ ์๋ฒ๊ฐ ์ธ์ฌ๋ง ๋ฉ์์ง๋ฅผ ๊ฒ์ํ๋ /topic/greetings ๋์์ ๊ฐ์
ํฉ๋๋ค.
* ํด๋น ๋์์์ ์ธ์ฌ๋ง์ด ์์ ๋๋ฉด DOM์ ๋จ๋ฝ ์์๋ฅผ ์ถ๊ฐํ์ฌ ์ธ์ฌ๋ง ๋ฉ์์ง๋ฅผ ํ์ํฉ๋๋ค.
*/
function connect() {
var socket = new SockJS('/gs-guide-websocket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings', function (greeting) {
showGreeting(JSON.parse(greeting.body).content);
});
});
}
function disconnect() {
if (stompClient !== null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
/**
* ์ฌ์ฉ์๊ฐ ์
๋ ฅํ ์ด๋ฆ์ ๊ฒ์ํ๊ณ STOMP ํด๋ผ์ด์ธํธ๋ฅผ ์ฌ์ฉํ์ฌ /app/hello ๋์(GreetingController.greeting()์ด ์์ ํ๋ ๊ณณ)์ผ๋ก ๋ณด๋
๋๋ค.
*/
function sendName() {
stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
}
function showGreeting(message) {
$("#greetings").append("<tr><td>" + message + "</td></tr>");
}
$(function () {
$("form").on('submit', function (e) {
e.preventDefault();
});
$("#connect").click(function () {
connect();
});
$("#disconnect").click(function () {
disconnect();
});
$("#send").click(function () {
sendName();
});
});
์คํ
application ํด๋์ค
์ด ์ ํ๋ฆฌ์ผ์ด์ ์ ์คํํ๋๋ฐ ์ฌ์ฉํฉ๋๋ค. Spring Boot๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ํด๋์ค๋ฅผ ์๋์ผ๋ก ์์ฑํ์ ๊ฒ ์ ๋๋ค.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
์คํ ๊ฐ๋ฅํ JAR ๋น๋
// ์คํ
$ ./gradlew bootrun
// ๋น๋ ํ JAR ์คํ
$ ./gradlew build
$ java -jar {ํจํค์ง ์ด๋ฆ}-{๋ฒ์ }.jar
์คํ ๊ฒฐ๊ณผ
์๋น์ค๊ฐ ์คํ๋๋ฉด ๋ธ๋ผ์ฐ์ ์์ http://localhost:8080 ์ ์ ์ํ์ฌ Connect ๋ฒํผ์ ํด๋ฆญํฉ๋๋ค.
์ฐ๊ฒฐ์ด ์ด๋ฆฌ๋ฉด ์ด๋ฆ์ ์ ๋ ฅํ๋ผ๋ ๋ฉ์์ง๊ฐ ํ์๋ฉ๋๋ค. ์ด๋ฆ์ ์ ๋ ฅํ๊ณ ๋ณด๋ด๊ธฐ๋ฅผ ํด๋ฆญํฉ๋๋ค. ์ฌ์ฉ์์ ์ด๋ฆ์ด STOMP๋ฅผ ํตํด ์๋ฒ์ JSON ๋ฉ์์ง๋ก ์ ์ก๋ฉ๋๋ค. 1์ด์ ์๋ฎฌ๋ ์ด์ ์ง์ฐ ํ ์๋ฒ๊ฐ ํ์ด์ง์ ํ์๋ "Hello" ์ธ์ฌ๋ง๊ณผ ํจ๊ป ๋ฉ์์ง๋ฅผ ๋ค์ ๋ณด๋ ๋๋ค. ์ด๋ ๋ค๋ฅธ ์ด๋ฆ์ ๋ณด๋ผ ์๋ ์๊ณ ์ฐ๊ฒฐ ๋๊ธฐ ๋ฒํผ์ ํด๋ฆญํ์ฌ ์ฐ๊ฒฐ์ ๋ซ์ ์๋ ์์ต๋๋ค.
command line ; ctrl + c ๋ก ์ข ๋ฃ.
> Task :bootRun
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.0.2)
~~~์๋ต
2023-02-11T18:31:57.151+09:00 INFO 13732 --- [ main] o.s.b.a.w.s.WelcomePageHandlerMapping : Adding welcome page: class path resource [static/index.html]
~~~
2023-02-11T18:31:57.996+09:00 INFO 13732 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2023-02-11T18:31:57.997+09:00 INFO 13732 --- [ main] o.s.m.s.b.SimpleBrokerMessageHandler : Starting...
2023-02-11T18:31:57.997+09:00 INFO 13732 --- [ main] o.s.m.s.b.SimpleBrokerMessageHandler : BrokerAvailabilityEvent[available=true, SimpleBrokerMessageHandler [org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry@d082916]]
2023-02-11T18:31:57.997+09:00 INFO 13732 --- [ main] o.s.m.s.b.SimpleBrokerMessageHandler : Started.
2023-02-11T18:31:58.010+09:00 INFO 13732 --- [ main] io.sookmyung.type.Application : Started Application in 2.713 seconds (process running for 2.901)
2023-02-11T18:32:29.089+09:00 INFO 13732 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-02-11T18:32:29.090+09:00 INFO 13732 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2023-02-11T18:32:29.092+09:00 INFO 13732 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 2 ms
2023-02-11T18:32:43.601+09:00 INFO 13732 --- [nboundChannel-7] i.s.type.example.GreetingController : HelloMessage(name=Jennie)
2023-02-11T18:32:46.048+09:00 INFO 13732 --- [boundChannel-10] i.s.type.example.GreetingController : HelloMessage(name=Jennie)
2023-02-11T18:32:52.323+09:00 INFO 13732 --- [boundChannel-13] i.s.type.example.GreetingController : HelloMessage(name=Bennie)
2023-02-11T18:32:57.039+09:00 INFO 13732 --- [MessageBroker-4] o.s.w.s.c.WebSocketMessageBrokerStats : WebSocketSession[1 current WS(1)-HttpStream(0)-HttpPoll(0), 1 total, 0 closed abnormally (0 connect failure, 0 send limit, 0 transport error)], stompSubProtocol[processed CONNECT(1)-CONNECTED(1)-DISCONNECT(0)], stompBrokerRelay[null], inboundChannel[pool size = 15, active threads = 0, queued tasks = 0, completed tasks = 15], outboundChannel[pool size = 4, active threads = 0, queued tasks = 0, completed tasks = 4], sockJsScheduler[pool size = 8, active threads = 1, queued tasks = 2, completed tasks = 5]
<==========---> 80% EXECUTING [1m 47s]
> :bootRun