Quarkus WebSockets Next
1. 소개
이 글에서는 Quarkus 프레임워크의 quarkus-websockets-next 확장에 대해 살펴보겠습니다. 이 확장은 애플리케이션 내에서 WebSockets를 지원하기 위한 새로운 실험적인 확장입니다.
Quarkus WebSockets Next는 이전 Quarkus WebSockets 확장을 대체하기 위해 설계된 새로운 확장입니다. 이 확장은 이전 확장보다 사용하기 쉽고 효율적입니다.
그러나 Quarkus의 일반적인 사례와 달리, Jakarta WebSockets API를 지원하지 않으며, 대신 WebSockets 작업을 위한 보다 간소화되고 현대적인 API를 제공합니다. 이는 자체 주석이 달린 클래스와 메소드를 사용하여 기능의 유연성을 높이며 JSON 지원과 같은 내장 기능도 제공합니다.
동시에, Quarkus WebSockets Next는 여전히 표준 Quarkus 코어 위에서 구성됩니다. 이는 우리가 기대하는 성능과 확장성을 모두 얻는 동시에 Quarkus가 제공하는 개발 경험의 이점을 누릴 수 있음을 의미합니다.
2. 의존성
새로운 프로젝트를 시작하는 경우, Maven을 사용하여 websockets-next 확장이 이미 설치된 구조를 만들 수 있습니다:
$ mvn io.quarkus.platform:quarkus-maven-plugin:3.16.4:create \
-DprojectGroupId=com.baeldung.quarkus \
-DprojectArtifactId=quarkus-websockets-next \
-Dextensions='websockets-next'
확장 기능이 여전히 실험적이므로, 이를 위해 io.quarkus.platform:quarkus-maven-plugin을 사용해야 합니다.
기존 프로젝트에서 작업하는 경우, 간단히 pom.xml 파일에 적절한 의존성을 추가할 수 있습니다:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets-next</artifactId>
</dependency>
3. 서버 엔드포인트
애플리케이션이 준비되고 websockets-next 확장이 설치되면 WebSockets를 사용하기 시작할 수 있습니다.
Quarkus에서는 @WebSocket 주석이 붙은 새 클래스를 생성함으로써 서버 엔드포인트를 만듭니다:
@WebSocket(path = "/echo")
public class EchoWebsocket {
// WebSocket 코드 여기에.
}
이렇게 하면 지정된 경로에서 듣는 엔드포인트가 생성됩니다. Quarkus의 특성상 필요에 따라 이 안에 경로 매개변수를 사용할 수 있으며, 고정 경로도 사용할 수 있습니다.
3.1. 메시지 콜백
WebSocket 엔드포인트가 유용하려면 메시지를 처리할 수 있어야 합니다.
WebSocket 연결은 텍스트와 바이너리 두 가지 유형의 메시지를 지원합니다. 서버 엔드포인트에서 @OnTextMessage 또는 @OnBinaryMessage로 주석 달린 메소드를 사용하여 이러한 메시지를 처리할 수 있습니다:
@OnTextMessage
public String onMessage(String message) {
return message;
}
이 경우 메시지 페이로드를 메소드 매개변수로 받아 메소드의 반환값을 클라이언트에 보냅니다. 따라서 이 예시는 에코 서버에 필요한 모든 것입니다. 수신된 메시지를 그대로 되돌려 보냅니다.
필요한 경우 바이너리 페이로드를 수신할 수도 있습니다. 이는 @OnBinaryMessage 주석을 사용하는 것으로 수행됩니다:
@OnBinaryMessage
public Buffer onMessage(Buffer message) {
return message;
}
여기서는 io.vertx.core.buffer.Buffer 인스턴스를 수신하고 반환합니다. 이 인스턴스에는 수신된 메시지의 원시 바이트가 포함됩니다.
3.2. 메소드 매개변수 및 반환값
핸들러가 수신하는 원시 페이로드 외에도 Quarkus가 콜백 메시지에 전달할 수 있는 여러 다른 항목이 있습니다.
모든 메시지 핸들러는 수신하는 메시지의 페이로드를 나타내는 정확히 하나의 매개변수를 가져야 합니다. 이 매개변수의 정확한 유형이 우리가 그것에 접근하는 방법을 결정합니다.
앞서 살펴본 것처럼, Buffer 또는 byte[]를 사용하면 수신 메시지의 정확한 바이트를 제공받습니다. String을 사용하면 이러한 바이트는 먼저 문자열로 디코딩됩니다.
더욱 풍부한 객체를 사용할 수도 있습니다. JsonObject 또는 JsonArray를 사용하면 수신 메시지가 JSON으로 처리되고 적절하게 디코딩됩니다. 또는 Quarkus가 지원하지 않는 다른 유형을 사용할 경우, Quarkus는 해당 유형으로 메시지를 JSON으로 역직렬화하려고 시도합니다:
@OnTextMessage
public Message onTextMessage(Message message) {
return message;
}
record Message(String message) {}
이러한 동일한 유형을 반환값으로도 사용할 수 있으며, 이 경우 Quarkus는 클라이언트로 메시지를 보내는 데 있어 예상대로 메시지를 직렬화합니다. 또한, 메시지 핸들러는 응답으로 아무것도 송신하지 않음을 나타내기 위해 void 반환을 가질 수 있습니다.
이 외에도 수용할 수 있는 다른 메소드 매개변수가 있습니다.
주석이 없는 String 매개변수는 메시지 페이로드로 제공됩니다. 그러나 @PathParam으로 주석이 붙은 String 매개변수를 사용하여 수신 URL의 이 매개변수 값을 받을 수도 있습니다:
@WebSocket(path = "/chat/:user")
public class ChatWebsocket {
@OnTextMessage(broadcast = true)
public String onTextMessage(String message, @PathParam("user") String user) {
return user + ": " + message;
}
}
WebSocketConnection 유형의 매개변수도 받을 수 있으며, 이는 클라이언트와 서버 간의 정확한 연결을 나타냅니다:
@OnTextMessage
public Map<String, String> onTextMessage(String message, WebSocketConnection connection) {
return Map.of(
"message", message,
"connection", connection.toString()
);
}
이것을 사용하면 클라이언트와 서버 간의 네트워크 연결 세부정보에 접근할 수 있습니다.
우리는 또한 이를 사용하여 연결을 더 직접적으로 상호작용할 수 있습니다. 메시지를 보내거나 강제로 연결을 종료할 수 있습니다:
@OnTextMessage
public void onTextMessage(String message, WebSocketConnection connection) {
if ("close".equals(message)) {
connection.sendTextAndAwait("Goodbye");
connection.closeAndAwait();
}
}
3.3. OnOpen 및 OnClose 콜백
메시지를 수신하기 위한 핸들러 외에도, @OnOpen을 사용하여 새로운 연결이 처음 열릴 때와 @OnClose를 사용하여 연결이 종료될 때 핸들러를 등록할 수 있습니다:
@OnOpen
public void onOpen() {
LOG.info("연결 열림");
}
@OnClose
public void onClose() {
LOG.info("연결 종료");
}
이 콜백 핸들러는 메시지 페이로드를 수신할 수 없지만 이전에 설명한 대로 다른 메소드 매개변수를 수신할 수 있습니다.
또한 @OnOpen 핸들러에는 반환값이 있어 클라이언트로 직렬화 및 전송될 수 있습니다. 이는 클라이언트가 먼저 무언가를 보내는 것을 기다리지 않고 연결 즉시 메시지를 보내는 데 유용합니다. 이를 수행하는 것은 메시지 핸들러에서 반환값에 대한 모든 규칙을 따릅니다:
@OnOpen
public String onOpen(WebSocketConnection connection) {
return "안녕하세요, " + connection.id();
}
3.4. 연결 액세스
현재 연결을 콜백 핸들러로 주입할 수 있음을 이미 보았습니다. 그러나 이는 연결 세부정보에 접근하는 유일한 방법이 아닙니다.
Quarkus는 @Inject를 사용하여 WebSocketConnection 객체를 CDI 세션 범위의 빈으로 주입할 수 있게 합니다. 이를 통해 시스템 내의 다른 빈에서 현재 연결에 접근할 수 있습니다:
@ApplicationScoped
public class CdiConnectionService {
@Inject
WebSocketConnection connection;
}
하지만 이는 WebSocket 핸들러의 컨텍스트 내에서 호출될 때만 작동합니다. 정규 HTTP 호출을 포함해 다른 컨텍스트에서 이를 접근하려 하면 jakarta.enterprise.context.ContextNotActiveException이 발생합니다.
우리는 또한 OpenConnections 유형의 객체를 주입하여 현재 열려 있는 모든 WebSocket 연결에 접근할 수 있습니다:
@Inject
OpenConnections connections;
그런 다음 모든 현재 열린 연결을 조회할 뿐만 아니라 이들을 통신하기 위해 메시지를 보낼 수도 있습니다:
public void sendToAll(String message) {
connections.forEach(connection -> connection.sendTextAndAwait(message));
}
단일 WebSocketConnection을 주입하는 것과 달리, 이는 다른 컨텍스트에서도 제대로 작동합니다. 이를 통해 필요에 따라 다른 컨텍스트에서 WebSocket 연결에 접근할 수 있습니다.
3.5. 오류 처리
경우에 따라 WebSocket 콜백을 처리할 때 문제가 발생할 수 있습니다. Quarkus는 @OnError로 주석이 달린 메소드를 작성하여 이러한 메소드에서 발생하는 모든 예외를 처리할 수 있도록 합니다:
@OnError
public String onError(RuntimeException e) {
return e.toString();
}
이들은 수신할 수 있는 매개변수와 반환값에 대한 다른 콜백 핸들러와 동일한 규칙을 따릅니다. 또한, 처리할 예외를 나타내는 매개변수를 반드시 가져야 합니다.
이러한 오류 처리기를 필요한 만큼 작성할 수 있으며, 서로 다른 예외 클래스에 대해 각각 작성할 수 있습니다. 만약 하나가 다른 것의 하위 클래스인 경우, 가장 구체적인 것이 호출됩니다:
@OnError
public String onIoException(IOException e) {
// IOException과 모든 하위 클래스를 처리합니다.
}
@OnError
public String onException(Exception e) {
// Exception과 모든 하위 클래스는 IOException을 제외하고 처리합니다.
}
4. 클라이언트 API
Quarkus는 서버 엔드포인트를 작성할 수 있을 뿐만 아니라 다른 서버와 통신할 수 있는 WebSocket 클라이언트를 작성할 수 있게 합니다.
4.1. 기본 커넥터
WebSocket 클라이언트를 작성하는 가장 기본적인 방법은 BasicWebSocketConnector를 사용하는 것입니다. 이를 통해 연결을 열고 원시 메시지를 보내고 받을 수 있습니다.
먼저 BasicWebSocketConnector를 코드에 주입해야 합니다:
@Inject
BasicWebSocketConnector connector;
그런 다음 이를 사용하여 원격 서비스에 연결할 수 있습니다:
WebSocketClientConnection connection = connector
.baseUri(serverUrl)
.executionModel(BasicWebSocketConnector.ExecutionModel.NON_BLOCKING)
.onTextMessage((c, m) -> {
// 수신 메시지를 처리합니다.
})
.connectAndAwait();
이 연결의 일환으로, 서버에서 수신된 모든 메시지를 처리하기 위한 람다를 등록합니다. 이는 WebSockets의 비동기 완전 이중 구조 특성 때문에 필요합니다. 우리는 이를 일반 HTTP 연결처럼 요청 및 응답 쌍으로 처리할 수는 없습니다.
연결을 열었으면, 이를 사용하여 서버에 메시지를 보낼 수도 있습니다:
connection.sendTextAndAwait("안녕하세요, 세계!");
이는 콜백 핸들러 내에서도 외부에서도 수행할 수 있습니다. 그러나 연결은 스레드 안전하지 않으므로, 여러 스레드에서 동시에 쓰지 않도록 주의해야 합니다.
onTextMessage 콜백 외에도 onOpen() 및 onClose()를 포함한 다른 생명 주기 이벤트에 대한 콜백을 등록할 수도 있습니다.
4.2. 풍부한 클라이언트 빈
기본 커넥터는 간단한 연결에 충분히 잘 작동하지만, 때때로 그보다 더 유연한 것이 필요합니다. Quarkus는 또한 서버 엔드포인트를 작성한 것과 유사하게 훨씬 더 풍부한 클라이언트를 작성할 수 있게 해줍니다.
이를 위해서는 @WebSocketClient로 주석이 붙은 새 클래스를 작성해야 합니다:
@WebSocketClient(path = "/json")
class RichWebsocketClient {
// 클라이언트 코드 여기에.
}
이 클래스 내에서 서버 엔드포인트와 동일한 방식으로 메소드를 @OnTextMessage, @OnOpen 등과 같은 주석을 사용하여 작성합니다:
@OnTextMessage
void onMessage(String message, WebSocketClientConnection connection) {
// 메시지 처리 여기에
}
이는 메소드 매개변수와 반환값에 관해서 서버 엔드포인트의 모든 동일한 규칙을 따르며, 연결 세부정보에 접근하려면 WebSocketClientConnection 대신 WebSocketConnection을 사용해야 합니다.
클라이언트 클래스를 작성한 후에는 주입된 WebSocketConnector
@Inject
WebSocketConnector<RichWebsocketClient> connector;
이를 사용하면 연결을 이전과 유사하게 생성할 수 있으며, 클라이언트 인스턴스가 모든 처리를 담당하므로 콜백을 제공할 필요가 없습니다:
WebSocketClientConnection connection = connector
.baseUri(serverUrl)
.connectAndAwait();
이 때 우리는 이전과 동일하게 사용할 수 있는 WebSocketClientConnection을 가지게 됩니다.
클라이언트 인스턴스에 접근해야 할 경우, 해당 클래스에 대한 Instance
@Inject
Instance<RichWebsocketClient> clients;
그러나 우리는 이 클라이언트 인스턴스가 올바른 컨텍스트 내에서만 사용 가능할 것이며, 모든 것이 비동기식이기 때문에 특정 이벤트가 이미 발생했는지 확인해야 한다는 점을 기억해야 합니다.
5. 결론
이 글에서는 websockets-next 확장을 사용한 Quarkus의 WebSockets에 대한 간단한 소개를 살펴보았습니다. 우리는 이를 사용하여 서버 및 클라이언트 구성 요소를 작성하는 방법을 보았습니다. 여기에서는 이 라이브러리의 기본 개념만 논의했지만, 훨씬 더 많은 고급 시나리오를 처리할 수 있습니다.
이 글의 모든 예제는 GitHub에서 확인할 수 있습니다.