091
[KINO][JAVA/Spring Boot] 좌석 선정 및 예약: WebSocket 응용(1) 본문
[KINO][JAVA/Spring Boot] 좌석 선정 및 예약: WebSocket 응용(1)
공구일 2026. 3. 24. 10:23*이 글은 프로젝트 KINO CINEMA의 개발 내용을 정리해놓은 글입니다.*
0. 설계
- 초반에 예약부분을 고려하지 못하고 3명의 팀원끼리 티켓팅 부분, 좌석 선점, 결제를 메인으로 풀스텍으로 짠 뒤, 전체 흐름을 합치는 방식을 선택했었습니다. 합산하는 과정에서 좌석 선점 -> 결제로 넘어갈 때 페이지는 필요없지만 서버에서 예약 로직이 필수적이라고 판단됐고 그러면서 초반에 짰던 것과는 다른 차이가 생겼습니다. 그 차이가 바로 원래는 좌석을 선택하는 동시에 좌석 상태가 AVAILABLE에서 HELD로 변하는 양방향 형식이었다면, 예약 로직이 들어오고 난 뒤로는 예약하기 버튼을 눌러서 결제 페이지로 넘어가는 과정에서 HELD로 바뀌고 이후에 선점된 결과만 가볍게 브로드캐스팅하는 형식이 되었습니다.
-> 그러기 때문에 이전에 짜놨던 WebSocketController는 현재 웹에서 사용되고 있지 않지만, 실시간성을 공부할 때 도움이 됐기 때문에 작성해뒀습니다. ReservationCommandService 내에서 단방향 PUSH로 브로드캐스팅 하는 것까지 설명할 예정입니다.
- 영화관 사이트인 KINO에서 다수의 사용자가 동시에 같은 좌석을 선택하는 (1)동시성 문제를 방지하기 위해 데이터베이스에 비관적 락을 적용하여 데이터 정합성을 보장하였으며, 선정된 좌석 정보를 다른 사람 화면에 즉각적으로 반영하는 (2)실시간 상태 동기화를 위해 WebSocket(STOMP)을 도입하였습니다.
좌석 지정 페이지(좌선 선택 후 버튼 클릭 시) -> 예약 생성(페이지없음): 이 과정에서 웹소켓 단방향 PUSH 발생
1. WebSocketConfig & WebSocketController(사용X)
- WebSocketConfig은 프론트엔드와 소통할 창구를 여는 곳으로, 이 설정파일에서는 WebSocket 연결과 메세지 흐름을 정의합니다.
package com.cinema.kino.config;
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 registerStompEndpoints(StompEndpointRegistry registry) {
//1. 티켓팅 모달의 엔드포인트
registry.addEndpoint("/ws-kino")
.setAllowedOriginPatterns("*")
.withSockJS();
//2. 좌석지정 페이지의 엔드포인트
registry.addEndpoint("/ws-seat")
.setAllowedOriginPatterns("*")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic"); // 구독(서버->클라이언트)
registry.setApplicationDestinationPrefixes("/app"); // 전송(클라이언트->서버)
}
}
-> @Configuration: Spring 설정 클래스(Bean 등록 + 구조정의)임을 알려주는 어노테이션입니다.
-> @EnableWebSocketMessageBroker: Spring에 WebSocket + STOMP 기능 사용을 키는 어노테이션입니다.
• 내부에서 WebSocketHandler를 생성하고, STOMP 처리기를 생성하며, MessageBroker를 생성하고 인아웃바운드 Channel을 생성합니다.
-> implements WebSocketMessageBrokerConfigurer: WebSocketMessageBrokerConfigurer는 Spring의 WebSocket(STOMP) 동작을 커스터마이징하기 위한 확장 인터페이스이며, registerStompEndpoints()와 configureMessageBroker()를 포함한 다양한 설정 메서드를 제공한다. 필요한 메서드만 선택적으로 override하여 WebSocket 연결 경로와 메시지 라우팅 구조를 정의할 수 있다.
• registerStompEndpoints(StompEndpointRegistry registry): StompEndpointRegistry은 WebSocket 연결 endpoint를 연결하는 객체로, .addEndpoint(...)를 통해 클라이언트가 WebSocket 연결할 URL을 지정해줍니다. .setAllowOriginPatterns(...)은 CORS 허용되는 것을 나타내는 메소드로, 현재 위의 코드에서 모든 도메인을 허용하고 있습니다. .withSockJS()는 WebSocket을 지원하지 않는 브라우저에서 WebSocket 연결 실패 시 HTTP fallback을 하기 위해 작성된 메소드로, 연결 실패시 XHR streaming / polling으로 대체됩니다.
• configureMessageBroker(MessageBrokerRegistry registry): MessageBrokerRegistry은 메세지 라우팅 규칙 설정 객체로, .enableSimpleBroker(...)을 통해 구독용 prefix를 지정해줍니다. 이 메서드는 내부 메모리 기반 메시지 브로커로, 구독 리스트를 저장하고, 메세지가 오면 반복문으로 전송됩니다. .setApplicationDestinationPrefixes(...)는 클라이언트에서 서버로 메세지를 보내는 prefix를 지정해줍니다. prefix는 메세지 흐름을 결정하는 규칙으로, 위에서 지정한 "/app"은 Controller로 보내 서버로 전송하는 것이고, "/topic"은 Broker로 보내서 브로드캐스팅을 수행합니다.
- WebSocketController는 클라이언트의 WebSocket 요청을 받아서 좌석 상태를 변경하고 모든 사용자에게 실시간으로 뿌립니다.
package com.cinema.kino.controller;
//import문 생략
@Slf4j
@Controller
@RequiredArgsConstructor
public class SeatWebSocketController {
private final SeatCommandService seatCommandService;
private final SimpMessagingTemplate messagingTemplate;
@MessageMapping("/seat/hold")
public void holdSeat(@Payload SeatSelectRequestDTO request) {
log.info("[WS] 좌석 선점 요청 도착 - 상영ID: {}, 좌석수: {}",
request.getScreeningId(),
request.getTickets() != null ? request.getTickets().size() : 0);
try {
List<SeatStatusResponseDTO> updatedSeats = seatCommandService.holdSeats(request);
messagingTemplate.convertAndSend(
"/topic/screening/" + request.getScreeningId(), updatedSeats
);
log.info("[WS] 좌석 선점 브로드캐스팅 완료 - 상영ID: {}", request.getScreeningId());
} catch (IllegalStateException e) {
// "이미 선택된 좌석입니다" 등의 비즈니스 예외 처리
log.warn("[WS] 좌석 선점 실패 (비즈니스 예외): {}", e.getMessage());
messagingTemplate.convertAndSend(
"/topic/screening/" + request.getScreeningId() + "/error",
e.getMessage()
);
} catch (Exception e) {
// 기타 서버 에러 처리
log.error("[WS] 좌석 선점 중 서버 에러 발생", e);
}
}
}
-> @Slf4j: log.info(), logo.error()와 같은 로그를 사용하기 위한 인터페이스로, log 객체를 자동으로 만들어주는 Lombok입니다.
*Facade(퍼사드)는 복잡한 내부 시스템을 하나의 간단한 인터페이스로 감싸는 패턴으로, 위의 롬복 어노테이션을 사용하면 logback, log4j 등이 내부에서 사용됩니다.
-> @Controller: WebSocket 메세지를 처리하는 컨트롤러로, REST의 @RestController를 여기서 사용하지 않은 이유는 HTTP요청을 JSON으로 반환하는 컨트롤러 메서드이기 때문에 HTTP 응답 자체가 없는, 메세지를 주고 받는 구조인 웹소켓에서는 적합하지 않기때문에 연결유지를 위해 사용됩니다.
-> @RequiredArgsConstructor: final 필드 자동 생성자 주입입니다. 아래 선언된 final 필드를 참고해서 만들어줍니다.
• SeatCommandService는 좌석 선점 및 상태 변경 전용 서비스입니다.
• SimpMessagingTemplate는 서버에서 클라이언트로 메세지를 보내는 객체로, .convertAndSend(...)로 브로드캐스팅을 합니다.
-> @MessageMapping(...)...holdSeat(@Payload ...): @MessageMapping은 WebSocket 전용 @RequestMapping입니다. 위의 설정으로 지정했던 prefix가 붙어서 /app/seat/hold 상태로 매핑됩니다. @Payload는 JSON을 DTO로 반환하는, REST API의 @RequestBody와 완전히 똑같은 역할을 수행합니다.
• .convertAndSend(...)는 서버에서 클라이언트에게 메세지를 보내는 함수로, 객체에서 메세지로 변환해서 특정 경로로 전송합니다.
Q. 물리적 통신망(TCP 소켓)이 같은데 왜 프로토콜에 따라 @Payload와 @RequestBody를 분리해둔건가요?
A. 우선적으로 두 어노테이션은 패키지부터 다릅니다. @RequsetBody는 Spring Web MVC에 소속으로, HTTP 통신을 처리하기 위해 만들어졌고, @Payload는 Spring Messaging 소속으로 HTTP라는 개념 자체를 모르기 때문에 명령어, 헤더, 페이로드로 이루어진 Message 객체만을 취급합니다. 즉, @Payload는 웹소켓뿐만 아니라 메시징 시스템을 아우리는 범용 알맹이 추출도구로 기능적인 면(JSON->DTO)에서는 비슷하지만, 처리하는 과정에서의 차이가 발생하기 때문입니다.
'Programming Language > Java' 카테고리의 다른 글
| [KINO][JAVA/Spring Boot] 좌석 선정 및 예약: WebSocket 응용(3) (0) | 2026.03.29 |
|---|---|
| [KINO][JAVA/Spring Boot] 좌석 선정 및 예약: WebSocket 응용(2) (0) | 2026.03.28 |
| [JAVA/Spring Boot] STOMP: WebSocket 개념 (0) | 2026.03.23 |
| [JAVA/Spring Boot] Spring Framework: IoC, Bean, DI (0) | 2026.03.16 |
| [JAVA/Spring Boot] Spring MVC (0) | 2026.03.16 |