학습 목표
- 폴링(Polling) 통신 방식의 개념과 동작 원리 이해
- 스프링 부트를 활용한 간단한 채팅 시스템 구현
- 실시간 통신의 기본 개념 습득 (폴링 → SSE → WebSocket 로드맵의 첫 단계)
- 스프링 부트 핵심 기술 (JPA, Mustache, MVC) 실습
사전 기반 지식
- Spring Boot 기본: @Controller, @Service, @Repository 어노테이션 이해
- Spring Data JPA: Entity, Repository 패턴 숙지
- HTTP 기본: GET, POST 요청/응답 이해
- HTML/JavaScript 기초: 기본 DOM 조작과 이벤트 처리
- Mustache 템플릿 엔진: 기본 문법 ({{}}, {{#}})
핵심 개념 이해
1. 폴링(Polling)이란?
폴링은 클라이언트가 서버에 주기적으로 요청을 보내 새로운 데이터가 있는지 확인하는 통신 방식입니다.
클라이언트 ──[요청]──> 서버
클라이언트 <──[응답]── 서버
(2초 대기)
클라이언트 ──[요청]──> 서버 (반복)
(2초 대기)
클라이언트 ──[요청]──> 서버 (반복)
폴링(Polling)이라는 용어는 클라이언트가 서버로부터 데이터를 계속해서 당겨오는(pulling) 행위에서 유래했습니다. 즉, 클라이언트가 서버에게 "새로운 데이터가 있나요?"라고 지속적으로 질문을 던져 응답을 수집하는 행위를 말합니다.
- 장점: 구현이 간단하고, HTTP 기반이므로 방화벽 문제에서 자유롭습니다.
- 단점:
- 불필요한 요청: 새로운 데이터가 없어도 계속 요청을 보냅니다.
- 서버 부하 증가: 동시 접속자가 많을수록 서버에 과도한 부하를 줄 수 있습니다. 예를 들어, 1,000명의 사용자가 2초마다 요청하면 서버는 2초마다 1,000개의 요청을 처리해야 합니다.
- 실시간성 제한: 데이터가 즉시 전달되지 않고, 폴링 주기에 따라 지연이 발생합니다.
시스템 목적
이 강의에서는 스프링 부트(Spring Boot)를 활용하여 간단한 실시간 채팅 시스템을 구축합니다. 이 시스템은 실시간 통신의 가장 기본적인 방법인 폴링(Polling) 방식으로 동작합니다.
시스템의 주요 기능
- 메시지 작성: 사용자가 채팅 메시지를 입력하고 전송할 수 있습니다.
- 메시지 조회: 웹 페이지에 접속하면 저장된 모든 채팅 메시지를 볼 수 있습니다.
- 자동 업데이트: 웹 페이지는 2초마다 자동으로 새로고침되어 새로운 메시지가 있는지 확인하고 화면에 표시합니다.
이 시스템은 다음의 계층 구조로 구성됩니다.
- 데이터 모델 (Chat.java): 채팅 메시지의 구조를 정의합니다. 메시지 내용(msg)과 고유 식별자(id)를 가집니다.
- 데이터 접근 계층 (ChatRepository.java): 데이터베이스에서 메시지를 저장하고 조회하는 역할을 담당합니다.
- 비즈니스 로직 계층 (ChatService.java): 메시지 저장 및 전체 메시지 조회와 같은 핵심 비즈니스 로직을 처리합니다.
- 웹 컨트롤러 (ChatController.java): 사용자의 웹 요청(GET, POST)을 받아 적절한 응답(HTML)을 반환합니다.
프로젝트 생성 하기
의존성 확인
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-mustache'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
설정 확인
spring.application.name=class_step_websocket
# UTF-8 설정
# 웹 서버의 인코딩 UTF-8로 강제합니다.
# 한글이 깨지는 현상방지, 모든 요청과 응답에 대해서 UTF-8로 통일 함.
server.servlet.encoding.charset=utf-8
server.servlet.encoding.force=true
# 1. DB 연결
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:test
spring.datasource.username=sa
# 2. ORM -> 스프링 부트 (JPA) -> hibernate 셋팅
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
#3. 템플릿 엔진 = 기본설정
# 템플릿 파일 위치 : src/main/resources/templates
# 파일 확장자 : .mustache
채팅 작성 화면 시안

채팅 목록 화면 시안

프로젝트 구축 하기
데이터 모델 정의 (Chart.java)
엔티티란? 데이터베이스 테이블과 1:1로 매핑되는 클래스 입니다. 넓은 의미에서 모델 클래스라고 부르기도 하지만 JPA를 사용할 때는 엔티티라고 부르기도 합니다.
package com.example.class_step_websocket.chat;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@Getter
@Table(name = "chat_tb")
@Entity
public class Chat {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String msg;
@Builder
public Chat(Integer id, String msg) {
this.id = id;
this.msg = msg;
}
}
2단계: 데이터 접근 계층 (ChatRepository.java)
package com.example.class_step_websocket.chat;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ChatRepository extends JpaRepository<Chat, Integer> {
// 기본적인 CRUD 완성
}
3단계 : 비즈니스 로직 계층(ChatService.java)
package com.example.class_step_websocket.chat;
import org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties;
import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Transactional(readOnly = true) // 읽기 전용 트랜잭션 상태
@RequiredArgsConstructor
@Service
public class ChatService {
private final ChatRepository chatRepository;
// 채팅 메세지 저장
@Transactional // 쓰기 작업이므로 읽기 전용 해제 처리
public Chat save(String msg) {
Chat chat = Chat.builder().msg(msg).build();
return chatRepository.save(chat);
}
// 채팅 메세지 리스트
public List<Chat> findAll() {
// 내림 차순으로 정렬하고 싶다면
Sort desc = Sort.by(Sort.Direction.DESC, "id");
return chatRepository.findAll(desc);
}
}
컨트롤러 구축
package com.example.class_step_websocket.chat;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@RequiredArgsConstructor
@Controller // 뷰 반환
public class ChatController {
private final ChatService chatService;
// 메세지 작성 폼 페이지
@GetMapping("/save-form")
public String saveForm() {
return "save-form";
}
// 채팅 목록 페이지
@GetMapping("/")
public String index(Model model) {
model.addAttribute("models", chatService.findAll());
return "index";
}
// 채팅 메세지 저장 요청
@PostMapping("/chat")
public String save(String msg) {
chatService.save(msg);
// POST 맵핑에서 (웹 에서 주의할 점) - 중복 등록 조심
return "save-form";
// PRG 패턴
//return "ridirect:/";
}
}
index
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>채팅</title>
<style>
body { margin: 20px; font-family: Arial, sans-serif; }
nav ul { list-style: none; padding: 0; display: flex; gap: 10px; }
nav a { padding: 8px 15px; background: #666; color: white; text-decoration: none; }
/* 채팅창 */
.chat-area {
border: 1px solid #ccc;
height: 300px;
padding: 10px;
overflow-y: scroll;
background: #f9f9f9;
}
/* 메시지 */
.message {
padding: 5px 10px;
margin: 3px 0;
background: white;
border-radius: 8px;
max-width: 80%;
}
.message:nth-child(odd) {
background: #e1f5fe;
margin-left: 20%;
}
</style>
</head>
<body>
<nav>
<ul>
<li><a href="/">목록</a></li>
<li><a href="/save-form">작성</a></li>
</ul>
</nav>
<h3>채팅</h3>
<p style="color: #666; font-size: 13px;">2초마다 업데이트</p>
<div class="chat-area">
{{#models}}
<div class="message">{{msg}}</div>
{{/models}}
</div>
<script>
<!--2초 후 새로고침-->
setTimeout(() => location.reload(), 2000);
<!--스크롤 아래로 - 클래스이름, ID 등으로 접근 -->
document.querySelector(".chat-area").scrollTop = 999;
</script>
</body>
</html>
save-form
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>메시지 작성</title>
<style>
/* 기본 설정만 */
body { margin: 20px; font-family: Arial, sans-serif; }
/* 네비게이션 */
nav ul { list-style: none; padding: 0; display: flex; gap: 10px; }
nav a { padding: 8px 15px; background: #666; color: white; text-decoration: none; }
/* 폼 */
form { margin-top: 20px; }
input { padding: 8px; width: 300px; margin-right: 10px; }
button { padding: 8px 15px; background: #007bff; color: white; border: none; }
</style>
</head>
<body>
<nav>
<ul>
<li><a href="/">목록</a></li>
<li><a href="/save-form">작성</a></li>
</ul>
</nav>
<h2>메시지 작성</h2>
<form action="/chat" method="post">
<input type="text" name="msg" id="msg" placeholder="메시지 입력..." required>
<button type="submit">전송</button>
</form>
<script>
document.getElementById('msg').focus();
document.getElementById('msg').onkeypress = function(e) {
if(e.key == 'Enter') this.form.submit();
};
</script>
</body>
</html>
'Spring boot > Spring boot websocket' 카테고리의 다른 글
| Spring Boot - STOMP(DTO) 변환 (0) | 2025.09.09 |
|---|---|
| Spring Boot - STOMP 프로토콜 채팅 (0) | 2025.09.05 |
| Spring Boot WebSocket 기본 채팅 시스템 (0) | 2025.09.04 |
| Spring Boot SSE(Server-Sent Events) 채팅 시스템 (0) | 2025.09.04 |