회원가입, 로그인 , 채팅 시 닉네임 자동으로 붙게하기 구현
User.java
package com.example.demo.model;
import jakarta.persistence.*;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/* @Id 바로 뒤에 오는 변수는 기본키를 의미한다. @GeneratedValue는 기본키 값을 자동으로
설정해주는 MYSQL의 AUTO_INCREMENT와 같은 역할을 한다. */
@Column(unique = true)
private String username; //username 컬럼에만 중복 금지(UNIQUE) 제약조건
private String password;
private String nickname;
// 기본 생성자
public User() {}
public User(String username, String password, String nickname) {
this.username = username;
this.password = password;
this.nickname = nickname;
}
// Getter & Setter 생략 가능 (롬복 쓰면 @Data)
// 아래 필요 시 직접 작성해도 됨
public Long getId() { return id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getNickname() { return nickname; }
public void setNickname(String nickname) { this.nickname = nickname; }
}
@Entity는 JPA에서 이 클래스가 데이터베이스 테이블과 연결되는 객체임을 나타내는 어노테이션이다.
@Id에 붙어있는 변수가 기본 키 임을 의미한다.
@GeneratedValue는 기본키 값을 자동으로 설정해주는 MYSQL의 AUTO_INCREMENT 역할이다.
즉, 사용자 정보를 표현하는 클래스(엔티티) 이며 회원가입시 이 객체가 DB에 저장된다.
UserRepository.java
package com.example.demo.repository;
import com.example.demo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
DB와 연결되는 DAO의 역할로 DAO는 데이터 베이스에 접근하는 로직을 따로 분리한 클래스(객체)를 말한다.
spring 에서는 @Repository 어노테이션을 붙인 클래스가 DAO역할을 하지만 JpaRepository를 상속하면 자동으로 내부에서 처리해준다.
JpaRepository를 상속하게 되면 아래의 기능 등 을 사용할 수 있다.
userRepository.save(user); // 저장 (insert or update)
userRepository.findAll(); // 전체 조회
userRepository.findById(1L); // id로 조회
userRepository.deleteById(1L); // 삭제
userRepository.count(); // 총 개수
findByUsername()을 통해 유저를 조회 한다. spring이 메서드 이름을 보고 자동으로 SQL을 만들어준다.
SELECT * FROM user WHERE username = ?
로그인에 사용된다.
UserController.java
package com.example.demo.controller;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserRepository userRepository;
@PostMapping("/create")
public String createUser(@RequestBody User user) {
userRepository.save(user);
return "User created!";
}
@GetMapping("/{username}")
public User getUser(@PathVariable String username) {
return userRepository.findByUsername(username);
}
@PostMapping("/login")
public String login(@RequestBody User user, HttpSession session) {
User foundUser = userRepository.findByUsername(user.getUsername());
if (foundUser != null && foundUser.getPassword().equals(user.getPassword())) {
session.setAttribute("nickname", foundUser.getNickname());
session.setAttribute("userId", foundUser.getId());
return "로그인 성공";
} else {
return "로그인 정보가 일치하지 않습니다.";
}
}
}
User 와 UserRepository의 엔티티를 사용하기 위해 먼저 import 해준다.
@RequestBody는 클라이언트가 요청 본문(body)에 JSON 데이터를 담아서 보내면 자바 객체로 자동 변환해준다.

@RequestMapping("/user")는 /user로 시작하는 요청을 처리한다는 의미이다.
/user/create로 post요청이 오면 클라이언트가 보낸 JSON 데이터를 User 객체로 받아서 userRepository.save(user)로 DB에 저장
/user/login으로 post 요청이 오면 username으로 DB에서 유저를 찾아보고 비밀번호가 같으면 로그인 성공 그리고 HttpSession에 nickname과 userId를 저장해서 로그인 상태를 유지 가능하게 한다.
세션에 저장한 nickname은 websocket을 연결할 때, HttpHandshakeInterceptor에서 꺼내서 실시간 채팅에 메시지를 보낼 때 닉네임으로 표시함.
ChatMessage.java
package com.example.demo.model;
public class ChatMessage {
private String sender;
private String content;
public ChatMessage() {}
public ChatMessage(String sender, String content) {
this.sender = sender;
this.content = content;
}
public String getSender() { return sender; }
public void setSender(String sender) { this.sender = sender; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
// getters/setters
}
채팅 메시지를 표현하는 DTO이다. DTO는 Data Transfer Object의 약자로 데이터를 전달할 때 사용하는 객체이다.
ChatController.java
package com.example.demo.controller;
import java.util.Map;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import com.example.demo.model.*;
//import jakarta.servlet.http.HttpSession;
@Controller
public class ChatController {
@MessageMapping("/chat")
@SendTo("/topic/messages")
public ChatMessage broadcast(ChatMessage message,
org.springframework.messaging.Message<?> msg) {
String nickname = (String) msg.getHeaders().get("simpSessionAttributes", Map.class).get("nickname");
message.setSender(nickname);
return message;
}
}

클라이언트가 /app/chat으로 메시지를 받을 때 ChatMessage 메서드가 호출되고 리턴값은 topic/messages를 구독하는 클라이언트에게 전송된다. (index.html에서 로그인할때 messages 구독함)
nickname을 Websocket 세션에서 꺼내서 메시지에 붙인다.
즉, websocket 메시지 처리 역할을 한다.
WebSocketConfig.java
package com.example.demo.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 configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic"); // 클라이언트 구독용
registry.setApplicationDestinationPrefixes("/app"); // 클라이언트가 보내는 경로
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").addInterceptors(new HttpHandshakeInterceptor())
.setAllowedOriginPatterns("*").withSockJS();
}
}
websocket 엔드포인트 /ws 생성
클라이언트는 /app/ 으로 메시지를 전송하고 서버는 /topic/ 으로 응답하도록 지정
HttpHandshakeInterceptor를 통해 nickname을 websocket 세션에 복사한다.
HttpHandshakeInterceptor.java
package com.example.demo.config;
import jakarta.servlet.http.HttpSession;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
public class HttpHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(
ServerHttpRequest request,
org.springframework.http.server.ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest servletRequest) {
HttpSession session = servletRequest.getServletRequest().getSession(false);
if (session != null) {
String nickname = (String) session.getAttribute("nickname");
attributes.put("nickname", nickname);
}
}
return true;
}
@Override
public void afterHandshake(
ServerHttpRequest request,
org.springframework.http.server.ServerHttpResponse response,
WebSocketHandler wsHandler,
Exception exception) {}
}
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>실시간 채팅</title>
<style>
/*채팅창과 회원가입은 기본 숨김 처리 */
#chatContainer,#registerModal{
display: none;}
#registerModal{
position:fixed;
top: 30%; left: 35%;
background:white;
border: 1px solid gray;
padding: 20px;
box-shadow: 2px 2px 10px rgba(0,0,0,0.3);
}
#chatBox{
height: 300px; overflow-y: scroll;
border: 1px solid #ccc; padding:10px; margin-bottom: 10px;
}
input{ margin:5px 0;}
</style>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
</head>
<body>
<!-- 로그인 화면 -->
<div id="loginContainer">
<h2>로그인</h2>
<input type="text" id="loginUsername" placeholder="아이디"><br>
<input type="password" id="loginPassword" placeholder="비밀번호"><br>
<button onclick="login()"> 로그인</button>
<button onclick="showRegisterModal()"> 회원가입</button>
<p id="loginResult" style="color: red;"></p>
</div>
<!-- 회원가입 모달 -->
<div id="registerModal">
<h3>회원 가입</h3>
<input type="text" id="regUsername" placeholder="아이디"><br>
<input type="password" id="regPassword" placeholder="비밀번호"><br>
<input type="text" id="regNickname" placeholder="닉네임"><br>
<button onclick="register()">가입하기</button>
<button onclick="hideRegisterModal()">닫기</button>
<p id="registerResult" style="color: green;"></p>
</div>
<!-- 채팅창 -->
<div id="chatContainer">
<h2>실시간 채팅</h2>
<div id="chatBox"></div>
<input type="text" id="messageInput" placeholder="메시지 입력">
<button onclick="sendMessage()">전송</button>
</div>
<script>
let stompClient = null;
// 회원가입 모달 열기/닫기
function showRegisterModal() {
document.getElementById("registerModal").style.display = "block";
}
function hideRegisterModal() {
document.getElementById("registerModal").style.display = "none";
}
// 회원가입 요청
function register() {
const data = {
username: document.getElementById("regUsername").value,
password: document.getElementById("regPassword").value,
nickname: document.getElementById("regNickname").value
};
fetch("http://localhost:8080/user/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
})
.then(res => res.text())
.then(msg => {
document.getElementById("registerResult").innerText = msg;
});
}
// 로그인 요청
function login() {
const data = {
username: document.getElementById("loginUsername").value,
password: document.getElementById("loginPassword").value
};
fetch("http://localhost:8080/user/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
})
.then(res => res.text())
.then(msg => {
if (msg.includes("성공")) {
document.getElementById("loginContainer").style.display = "none";
document.getElementById("chatContainer").style.display = "block";
connectChat();
} else {
document.getElementById("loginResult").innerText = msg;
}
});
}
// WebSocket 연결
function connectChat() {
const socket = new SockJS("http://localhost:8080/ws");
stompClient = Stomp.over(socket);
stompClient.connect({}, function () {
console.log("WebSocket 연결 성공");
stompClient.subscribe("/topic/messages", function (message) {
const msg = JSON.parse(message.body);
displayMessage(msg.sender, msg.content);
});
});
}
// 메시지 보내기
function sendMessage() {
const msgContent = document.getElementById("messageInput").value;
if (msgContent && stompClient) {
stompClient.send("/app/chat", {}, JSON.stringify({ content: msgContent }));
document.getElementById("messageInput").value = "";
}
}
// 채팅 메시지 출력
function displayMessage(sender, content) {
const chatBox = document.getElementById("chatBox");
const messageDiv = document.createElement("div");
messageDiv.innerHTML = `<b>${sender}:</b> ${content}`;
chatBox.appendChild(messageDiv);
chatBox.scrollTop = chatBox.scrollHeight;
}
</script>
</body>
</html>
function register() 에서 입력한 정보를 JSON 형태로 서버에 보내면 서버의 /user/create API가 @RequestBody User user로 받아서 DB에 저장
function login()도 마찬가지로 JSON 형태로 아이디와 비밀번호를 서버로 보내면 서버의 /user/login API가 DB에서 유저를 찾아서 비밀번호 일치를 확인하면 nickname과 userId를 세션에 저장후 성공 메시지를 보낸다. '성공' 단어를 확인하고 connectChat함수를 실행
function connectChat()는 websocket 연결 후 서버로부터 실시간 메시지를 수신 받는다.
function sendMessage() 는 /app/chat으로 전송한다.
function displayMessage()는 서버로부터 받은 메시지 내용을 채팅방에 동적으로 추가한다.
실행 화면

회원가입 버튼을 누르면 아래와 같이 회원가입 창이 뜨게된다. 가입을 완료하면 user created!

회원가입되지 않은 아이디는 오류가 난다.

로그인을 성공해서 들어가면 실시간 채팅방에 들어갈 수 있다

여러명도 가능하다

데이터 베이스를 확인하면 정보가 잘 들어갔음을 확인할 수 있다.

