접속자 대기열 시스템 #3 - 셋업
이번 포스팅에서는 waiting-web
과 waiting-api
두 개의 서버를 구축하는 방법을 소개합니다. 이 두 서버는 각각 예매 웹페이지와 대기열 처리를 담당하며, Spring Boot를 사용하여 간단하게 셋업할 수 있습니다.
1. 프로젝트 개요
waiting-web
: 대기열에서 성공적으로 빠져나온 사용자를 예매 웹페이지로 리디렉션하여 서비스합니다.waiting-api
: 대기 페이지를 제공하고 대기열 처리와 관련된 API 정보를 관리합니다.
이 두 서버는 분리된 서비스로 운영되어, 대기열 처리와 실제 웹페이지 서비스 로직을 독립적으로 관리할 수 있도록 합니다.
2. 프로젝트 셋업
두 개의 Spring Boot 프로젝트를 각각 설정하여 시작하겠습니다.
2.1. waiting-web
설정
waiting-web
은 사용자가 대기열에서 빠져나오면 예매 웹페이지로 리디렉션하는 역할을 합니다. 이 서버는 간단한 웹 애플리케이션으로 설정하며, Thymeleaf와 Spring MVC를 사용해 정적 페이지를 제공할 수 있도록 구성합니다.
2.1.1. build.gradle
설정
waiting-web
프로젝트의 Gradle 설정은 다음과 같습니다:
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
}
tasks.named('test') {
useJUnitPlatform()
}
2.1.2. 주요 의존성
spring-boot-starter-thymeleaf
: Thymeleaf 템플릿 엔진을 사용하여 정적 HTML 페이지를 제공합니다.spring-boot-starter-web
: Spring MVC를 이용해 웹 애플리케이션을 구축합니다.
이 설정을 통해 waiting-web
프로젝트는 간단한 웹페이지 서빙과 리디렉션 기능을 수행할 수 있습니다.
2.2. waiting-api
설정
waiting-api
는 대기열 처리와 관련된 백엔드 로직을 담당하며, Reactive Streams와 Redis를 사용하여 비동기 대기열 처리를 효율적으로 관리합니다.
2.2.1. build.gradle
설정
waiting-api
프로젝트의 Gradle 설정은 다음과 같이 구성합니다:
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
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'
testImplementation 'com.github.codemonstur:embedded-redis:1.0.0'
}
tasks.named('test') {
useJUnitPlatform()
}
2.2.2. 주요 의존성
spring-boot-starter-data-redis-reactive
: Redis를 사용하여 대기열 데이터를 비동기적으로 관리합니다.spring-boot-starter-webflux
: 비동기 웹 애플리케이션을 구축하기 위해 WebFlux를 사용합니다.spring-boot-starter-thymeleaf
: 필요 시 HTML 템플릿 엔진으로 대기 페이지를 제공합니다.io.projectreactor:reactor-test
: Reactive Streams 기반의 테스트를 지원합니다.com.github.codemonstur:embedded-redis
: 테스트 시 임베디드 Redis를 사용할 수 있게 해주는 라이브러리입니다.
3. html
resources > templates > index.html
3.1. waiting-web
resources > templates > index.html 파일을 만들고 아래 코드를 입력해줍니다.
디렉토리가 없다면 만들어주세요.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>영화 예매</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #1b1b1b;
margin: 0;
padding: 0;
color: #fff;
}
.container {
width: 90%;
max-width: 1200px;
margin: 20px auto;
background-color: #2e2e2e;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
.header img {
width: 150px;
}
.header h1 {
margin: 0;
font-size: 24px;
}
.steps {
display: flex;
justify-content: space-between;
margin: 20px 0;
}
.step {
flex: 1;
text-align: center;
padding: 10px;
background-color: #444;
margin-right: 5px;
border-radius: 5px;
font-size: 14px;
}
.step.active {
background-color: #d9534f;
color: #fff;
}
.clearfix::after {
content: "";
clear: both;
display: table;
}
.details-section {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.movie-poster {
width: 30%;
text-align: center;
}
.movie-poster img {
width: 100%;
border-radius: 8px;
}
.seat-selection, .summary-section {
width: 65%;
margin-left: 20px;
}
.seat-selection h2, .summary-section h2 {
font-size: 18px;
margin-bottom: 10px;
}
.seats {
display: grid;
grid-template-columns: repeat(16, 30px);
gap: 5px;
margin: 20px 0;
justify-content: center;
}
.seat {
width: 30px;
height: 30px;
background-color: #444;
border-radius: 5px;
cursor: pointer;
}
.seat.selected {
background-color: #d9534f;
}
.seat.occupied {
background-color: #777;
cursor: not-allowed;
}
.empty {
background-color: transparent;
pointer-events: none;
}
.summary-section {
margin-top: 20px;
background-color: #3c3c3c;
padding: 15px;
border-radius: 8px;
}
.summary p {
margin: 5px 0;
}
.summary .total {
font-size: 18px;
margin-top: 10px;
}
.date-time-info {
text-align: right;
font-size: 14px;
}
.date-time-info p {
margin: 5px 0;
}
.next-step {
margin-top: 20px;
text-align: center;
}
.next-button {
background-color: #d9534f;
color: #fff;
border: none;
padding: 10px 20px;
font-size: 16px;
border-radius: 5px;
cursor: pointer;
}
.next-button:hover {
background-color: #c9302c;
}
.cancel-button {
background-color: #444;
color: #fff;
border: none;
padding: 10px 20px;
font-size: 16px;
border-radius: 5px;
cursor: pointer;
margin-right: 10px;
}
@media (max-width: 768px) {
.details-section {
flex-direction: column;
}
.movie-poster, .seat-selection {
width: 100%;
margin: 0;
}
.seat-selection {
margin-top: 20px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<img src="https://resizing.flixster.com/wnZN6VrjWe4-xyFfhtXgmoRRNyM=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzFlNDNkZGQ3LTEwOWItNGFmZi1iYzNjLWRmYjJhYzZkNThhZS53ZWJw" alt="IMAGIX Cinema">
<h1>MORTAL ENGINES</h1>
</div>
<div class="steps">
<div class="step active">1. Choose your place</div>
<div class="step">2. Payment</div>
<div class="step">3. Ticket</div>
</div>
<div class="clearfix">
<div class="details-section">
<div class="movie-poster">
<img src="https://resizing.flixster.com/wnZN6VrjWe4-xyFfhtXgmoRRNyM=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzFlNDNkZGQ3LTEwOWItNGFmZi1iYzNjLWRmYjJhYzZkNThhZS53ZWJw" alt="Movie Poster">
</div>
<div class="seat-selection">
<h2>Choose your seats</h2>
<div class="seats">
<!-- Row 1 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 2 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat selected"></div><div class="seat selected"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 1 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 2 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat selected"></div><div class="seat selected"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 1 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 2 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat selected"></div><div class="seat selected"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 1 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 2 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat selected"></div><div class="seat selected"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 1 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 2 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat selected"></div><div class="seat selected"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 1 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 2 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat selected"></div><div class="seat selected"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 1 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 2 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat selected"></div><div class="seat selected"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 1 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 2 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat selected"></div><div class="seat selected"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 1 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 2 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat selected"></div><div class="seat selected"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 1 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 2 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat selected"></div><div class="seat selected"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 1 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- Row 2 -->
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<div class="seat"></div><div class="seat"></div><div class="seat"></div><div class="seat"></div>
<div class="seat selected"></div><div class="seat selected"></div><div class="seat"></div><div class="seat"></div>
<div class="empty"></div><div class="empty"></div><div class="empty"></div><div class="empty"></div>
<!-- More rows as needed -->
</div>
<div class="summary-section">
<h2>Summary</h2>
<p>6 row 7th seat - $15</p>
<p>6 row 8th seat - $15</p>
<hr>
<p class="total"><strong>Total: $30</strong></p>
</div>
<div class="next-step">
<button class="cancel-button">Cancel</button>
<button class="next-button">Next</button>
</div>
</div>
<div class="date-time-info">
<h2>Date & Time</h2>
<p>Date: 14 DEC</p>
<p>Time: 9:40 pm</p>
<p>Type: IMAX 3D</p>
</div>
</div>
</div>
</div>
<script>
document.querySelectorAll('.seat').forEach(seat => {
seat.addEventListener('click', () => {
if (!seat.classList.contains('occupied')) {
seat.classList.toggle('selected');
}
});
});
</script>
</body>
</html>
3.2. waiting-api
resources > templates > waiting-room.html 파일을 만들고 아래 코드를 입력해줍니다.
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>접속 대기 중</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.queue-container {
background-color: #fff;
width: 300px;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
text-align: center;
position: relative;
}
.queue-header {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
}
.queue-subheader {
font-size: 14px;
color: #666;
margin-bottom: 20px;
}
.queue-info {
background-color: #f9f9f9;
padding: 15px;
border-radius: 5px;
margin-bottom: 10px;
}
.queue-info p {
margin: 5px 0;
font-size: 14px;
}
.queue-count {
font-size: 24px;
color: #ff5722;
font-weight: bold;
}
.progress-bar {
background-color: #ddd;
border-radius: 5px;
height: 8px;
margin: 10px 0;
position: relative;
}
.progress-fill {
background-color: #ff5722;
height: 100%;
width: 50%;
border-radius: 5px;
}
.queue-warning {
font-size: 12px;
color: #999;
margin-top: 10px;
}
.close-button {
position: absolute;
top: 10px;
right: 10px;
font-size: 18px;
color: #999;
cursor: pointer;
}
</style>
</head>
<body>
<div class="queue-container">
<div class="close-button">×</div>
<div class="queue-header">YES24 티켓</div>
<div class="queue-subheader">접속 대기 중입니다.<br>순서가 오면 다음 단계로 넘어갑니다.</div>
<div class="queue-info">
<p>대기 인원 <span id="number" class="queue-count">0명 이상</span></p>
<div class="progress-bar">
<div class="progress-fill" style="width: 50%;"></div>
</div>
<p>예상 대기 시간: <span id="waiting-time">00분 00초</span></p>
</div>
<div class="queue-warning">
※ 대기 중 새로고침을 하거나 다시 접속하시면<br>대기 시간이 늘어날 수 있으니 유의해 주세요.
</div>
</div>
<script>
function fetchWaitingRank() {
const queue = `[[${queue}]]`;
const userId = `[[${userId}]]`;
const queryParam = new URLSearchParams({queue: queue, user_id: userId});
fetch('/api/v1/queue/rank?' + queryParam)
.then(response => response.json())
.then(data => {
const numberElement = document.querySelector('#number');
const timeElement = document.querySelector('#waiting-time');
if (data.rank < 0) {
fetch('/api/v1/queue/touch?' + queryParam)
.then(() => {
// 페이지 새로고침
window.location.reload();
})
.catch(error => console.error(error));
return;
}
// 대기 인원 업데이트
if (numberElement) {
numberElement.innerHTML = data.rank + '명 이상';
}
// 예상 대기 시간 업데이트
if (timeElement) {
timeElement.innerHTML = calculateEstimatedTime(data.rank);
}
})
.catch(error => console.error(error));
}
function calculateEstimatedTime(rank) {
// 대기열에서 10초마다 100명씩 제거
const removalRatePerSecond = 100 / 10;
const estimatedTimeInSeconds = rank / removalRatePerSecond;
const minutes = Math.floor(estimatedTimeInSeconds / 60);
const seconds = Math.floor(estimatedTimeInSeconds % 60);
return `${minutes}분 ${seconds < 10 ? '0' : ''}${seconds}초`;
}
setInterval(fetchWaitingRank, 3000);
</script>
</body>
</html>
4. 컨트롤러
4.1. waiting-web
@SpringBootApplication
@Controller
public class WaitingWebApplication {
RestTemplate restTemplate = new RestTemplate();
public static void main(String[] args) {
SpringApplication.run(WaitingWebApplication.class, args);
}
@GetMapping
public String index(){
return "index"
}
4.2. waiting-api
@Controller
public class WaitingRoomController{
@GetMapping("/waiting-room")
Mono<REndering> waitingRoomPage(
@RequestParam(name="queue", defaultValue="default") String queue,
@RequestParam(name="user_id") Long userId
){
return Mono.just(Rendering.view("waiting-room.html").build());
}
}
5. 프로젝트 실행
각 프로젝트에서 다음 명령어를 실행하여 서버를 시작할 수 있습니다.
# waiting-web 프로젝트 실행
./gradlew :waiting-web:bootRun
# waiting-api 프로젝트 실행
./gradlew :waiting-api:bootRun
서버가 각각 실행되면 waiting-web
은 사용자의 대기 상태를 확인하고 성공 시 예매 페이지로 리디렉션합니다. waiting-api
는 대기열 상태를 관리하고 필요한 정보를 제공하는 API를 서빙합니다.
6. 다음 단계
이제 두 개의 서버를 구축했으니, 다음 단계에서는 각 서버의 기능을 상세히 구현하고 통합하는 방법을 살펴보겠습니다. 예를 들어, waiting-web
과 waiting-api
간의 통신을 설정하거나 대기열 로직을 더욱 구체화할 수 있습니다.
'프레임워크 > 자바 스프링' 카테고리의 다른 글
접속자 대기열 시스템 #6- 접속 대기 웹페이지 개발 (1) | 2024.10.10 |
---|---|
접속자 대기열 시스템 #5- Redis를 이용한 대기열 관리 및 웹페이지 진입 API 구현 (1) | 2024.10.10 |
접속자 대기열 시스템 #4- 대기열 등록 API 개발 (6) | 2024.10.09 |
BlockHound: Java 비동기 애플리케이션에서 블로킹 호출을 감지하는 도구 (6) | 2024.10.08 |
Spring MVC와 Spring Webflux 성능비교 (0) | 2024.10.08 |