소켓
소켓 vs 웹소켓 차이점
📌 핵심 차이점:
- **소켓(Socket)**은 일반적으로 네트워크 애플리케이션에서 TCP 또는 UDP를 직접 다루는 저수준 API
- TCP/UDP 네트워크 애플리케이션이면 → 소켓(Socket) 사용
- **웹소켓(WebSocket)**은 HTTP 환경에서 양방향 실시간 통신을 가능하게 해주는 기술
- 웹 환경에서 실시간 통신이 필요하면 → 웹소켓(WebSocket) 사용
1. os 에서 tcp 연결 : ip, port 필요
2. 스트림으로 데이터 주고 받음
3. sessionManager 필요 - accept() 여러번 호출
4. 단체 카톡방 원리:
클라이언트1이 서버로 메시지를 보내고, 받은 메시지를 서버가 다른 클라이언트 2,3에게 전달함
WebSocketSession vs HttpSession 차이점
WebSocket에서 연결된 클라이언트를 관리하기 위해 WebSocketSession을 사용합니다.
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
// 채팅방별 WebSocket 세션 저장 (roomId -> Set<WebSocketSession>)
private final Map<String, Set<WebSocketSession>> chatRooms = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// WebSocket 연결이 성공하면 세션을 저장
String roomId = getRoomId(session);
chatRooms.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet()).add(session);
System.out.println("새로운 클라이언트 접속: " + session.getId() + " / 채팅방: " + roomId);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 메시지를 같은 채팅방에 있는 모든 세션에게 전송
String roomId = getRoomId(session);
for (WebSocketSession wsSession : chatRooms.getOrDefault(roomId, Collections.emptySet())) {
if (wsSession.isOpen()) {
wsSession.sendMessage(message);
}
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// 연결이 닫히면 세션 제거
String roomId = getRoomId(session);
chatRooms.getOrDefault(roomId, Collections.emptySet()).remove(session);
System.out.println("클라이언트 연결 종료: " + session.getId());
}
private String getRoomId(WebSocketSession session) {
return session.getUri().toString().split("/chat/")[1]; // 예: "/chat/{roomId}" 에서 roomId 추출
}
}
HttpSession을 WebSocket에서 활용
WebSocketSession은 HTTP 요청의 HttpSession과 별개지만, HttpSession 정보를 WebSocket에서 가져올 수 있습니다.
WebSocket에서 HttpSession 접근하기
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// HttpSession에 접근하기 위한 속성 가져오기
Map<String, Object> attributes = session.getAttributes();
HttpSession httpSession = (HttpSession) attributes.get("HTTP_SESSION");
if (httpSession != null) {
String userId = (String) httpSession.getAttribute("USER_ID");
System.out.println("WebSocket 연결된 사용자 ID: " + userId);
}
}
채팅 설계: 카카오톡
- 실시간 채팅: WebSocket + Redis Pub/Sub
- 메시지 저장: MongoDB or RDBMS (PostgreSQL)
- Redis: 캐싱 및 Pub/Sub 활용
채팅방에 여러 유저가 접근하는 방법
- 채팅방 관리
- ChatRoom 엔티티를 생성하여 방을 관리
- 각 사용자는 특정 roomId를 가지고 방에 접속
- Redis Pub/Sub 활용
- 특정 채팅방에 있는 모든 유저에게 메시지 브로드캐스트
- 여러 서버에서 WebSocket을 처리할 경우, Redis가 중앙 메시지 브로커 역할 수행
- WebSocket 연결 관리
- 각 채팅방에 대한 연결된 세션을 관리
- 메시지를 보낼 때 해당 방에 있는 모든 클라이언트에게 전송
채팅방 엔티티
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "chatRooms")
public class ChatRoom {
@Id
private String roomId;
private String roomName;
private Set<String> users = new HashSet<>(); // 채팅방에 있는 유저 목록
}
채팅방 관리 서비스
@Service
public class ChatRoomService {
private final ChatRoomRepository chatRoomRepository;
public ChatRoomService(ChatRoomRepository chatRoomRepository) {
this.chatRoomRepository = chatRoomRepository;
}
// 채팅방 생성
public ChatRoom createRoom(String roomName) {
ChatRoom chatRoom = new ChatRoom(UUID.randomUUID().toString(), roomName, new HashSet<>());
return chatRoomRepository.save(chatRoom);
}
// 특정 채팅방 조회
public ChatRoom getRoomById(String roomId) {
return chatRoomRepository.findById(roomId)
.orElseThrow(() -> new RuntimeException("채팅방을 찾을 수 없습니다."));
}
// 유저 추가
public void addUserToRoom(String roomId, String userId) {
ChatRoom chatRoom = getRoomById(roomId);
chatRoom.getUsers().add(userId);
chatRoomRepository.save(chatRoom);
}
}
WebSocket 채팅 핸들러 (여러 유저 지원)
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
private final RedisTemplate<String, Object> redisTemplate;
private final Map<String, Set<WebSocketSession>> chatRoomSessions = new ConcurrentHashMap<>();
public ChatWebSocketHandler(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 세션 연결 시 roomId 저장 (URL에서 추출)
String roomId = getRoomId(session);
chatRoomSessions.computeIfAbsent(roomId, k -> new HashSet<>()).add(session);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
ChatMessage chatMessage = objectMapper.readValue(message.getPayload(), ChatMessage.class);
String roomId = chatMessage.getRoomId();
// 메시지를 Redis에 저장하고 Pub/Sub으로 전송
redisTemplate.convertAndSend("chat:" + roomId, chatMessage);
// 같은 채팅방에 있는 모든 유저에게 메시지 전송
broadcastMessage(roomId, chatMessage);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// 연결 종료 시 세션 제거
String roomId = getRoomId(session);
chatRoomSessions.getOrDefault(roomId, new HashSet<>()).remove(session);
}
private void broadcastMessage(String roomId, ChatMessage chatMessage) throws IOException {
Set<WebSocketSession> sessions = chatRoomSessions.getOrDefault(roomId, Collections.emptySet());
for (WebSocketSession session : sessions) {
if (session.isOpen()) {
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(chatMessage)));
}
}
}
private String getRoomId(WebSocketSession session) {
String uri = session.getUri().toString();
return uri.substring(uri.lastIndexOf("/") + 1); // 예: "/chat/{roomId}"
}
}
Redis Pub/Sub 사용 (멀티 서버 지원)
@Service
public class RedisSubscriber implements MessageListener {
private final ObjectMapper objectMapper = new ObjectMapper();
private final ChatWebSocketHandler chatWebSocketHandler;
public RedisSubscriber(ChatWebSocketHandler chatWebSocketHandler) {
this.chatWebSocketHandler = chatWebSocketHandler;
}
@Override
public void onMessage(Message message, byte[] pattern) {
try {
String body = new String(message.getBody(), StandardCharsets.UTF_8);
ChatMessage chatMessage = objectMapper.readValue(body, ChatMessage.class);
chatWebSocketHandler.broadcastMessage(chatMessage.getRoomId(), chatMessage);
} catch (Exception e) {
e.printStackTrace();
}
}
}
WebSocket 설정
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
private final ChatWebSocketHandler chatWebSocketHandler;
public WebSocketConfig(ChatWebSocketHandler chatWebSocketHandler) {
this.chatWebSocketHandler = chatWebSocketHandler;
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatWebSocketHandler, "/chat/{roomId}")
.setAllowedOrigins("*");
}
}
채팅방 컨트롤러
@RestController
@RequestMapping("/chat")
public class ChatRoomController {
private final ChatRoomService chatRoomService;
public ChatRoomController(ChatRoomService chatRoomService) {
this.chatRoomService = chatRoomService;
}
@PostMapping("/room")
public ResponseEntity<ChatRoom> createRoom(@RequestParam String roomName) {
return ResponseEntity.ok(chatRoomService.createRoom(roomName));
}
@GetMapping("/room/{roomId}")
public ResponseEntity<ChatRoom> getRoom(@PathVariable String roomId) {
return ResponseEntity.ok(chatRoomService.getRoomById(roomId));
}
@PostMapping("/room/{roomId}/join")
public ResponseEntity<Void> joinRoom(@PathVariable String roomId, @RequestParam String userId) {
chatRoomService.addUserToRoom(roomId, userId);
return ResponseEntity.ok().build();
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
}
WebSocket 설정 (실시간 메시지 전송)
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatHandler(), "/chat").setAllowedOrigins("*");
}
@Bean
public WebSocketHandler chatHandler() {
return new ChatWebSocketHandler();
}
}
WebSocket 핸들러 (메시지 처리)
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
private final RedisTemplate<String, Object> redisTemplate;
public ChatWebSocketHandler(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
ChatMessage chatMessage = objectMapper.readValue(message.getPayload(), ChatMessage.class);
// 메시지 저장 (MongoDB 또는 Redis)
redisTemplate.convertAndSend("chat", chatMessage);
// 모든 클라이언트에 메시지 전송
for (WebSocketSession webSocketSession : session.getOpenSessions()) {
webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(chatMessage)));
}
}
}
채팅 메시지 DTO
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatMessage {
private String sender;
private String message;
private String roomId;
private LocalDateTime timestamp;
}
Redis Pub/Sub (채팅 메시지 전파)
@Service
public class RedisSubscriber implements MessageListener {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onMessage(Message message, byte[] pattern) {
try {
String body = new String(message.getBody(), StandardCharsets.UTF_8);
ChatMessage chatMessage = objectMapper.readValue(body, ChatMessage.class);
System.out.println("Received message: " + chatMessage);
} catch (Exception e) {
e.printStackTrace();
}
}
}
MongoDB 메시지 저장
@Document(collection = "chatMessages")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatMessageEntity {
@Id
private String id;
private String sender;
private String message;
private String roomId;
private LocalDateTime timestamp;
}
@Repository
public interface ChatMessageRepository extends MongoRepository<ChatMessageEntity, String> {
List<ChatMessageEntity> findByRoomIdOrderByTimestamp(String roomId);
}
채팅 메시지 저장 서비스
@Service
public class ChatService {
private final ChatMessageRepository chatMessageRepository;
public ChatService(ChatMessageRepository chatMessageRepository) {
this.chatMessageRepository = chatMessageRepository;
}
public void saveMessage(ChatMessage chatMessage) {
ChatMessageEntity entity = new ChatMessageEntity(
UUID.randomUUID().toString(),
chatMessage.getSender(),
chatMessage.getMessage(),
chatMessage.getRoomId(),
chatMessage.getTimestamp()
);
chatMessageRepository.save(entity);
}
public List<ChatMessageEntity> getChatHistory(String roomId) {
return chatMessageRepository.findByRoomIdOrderByTimestamp(roomId);
}
}
'학습 기록 (Learning Logs) > Today I Learned' 카테고리의 다른 글
Spring Boot + STOMP로 카카오톡 스타일 채팅방 만들기 (0) | 2025.03.10 |
---|---|
인공 지능을 실현하기 위한 기술 (0) | 2025.03.09 |
인공지능의 신뢰성 (0) | 2025.03.02 |
인공지능의 진화 (0) | 2025.03.02 |
reddit 기능 파악 (0) | 2025.03.01 |