⭐ 프로젝트 개요
이 프로젝트는 다수의 사용자가 동시에 텍스트를 작성하고 수정할 수 있는 협업 텍스트 편집기입니다. 사용자는 실시간으로 서로의 입력을 확인하고, 텍스트 내용을 수정할 수 있습니다.
이 서비스는 웹 기반으로 구현되었으며, 백엔드와 프론트엔드 간의 원활한 통신을 위해 WebSocket 기술을 활용합니다.
이 글은 SpringBoot에서의 구현 방식만 소개하며 프론트는 html로만 간단히 구현했습니다.
⭐ 사용 기술
- 백엔드:
- Spring Boot
- Spring WebSocket
- Spring Data Redis
- Java
- 프론트엔드(그냥 백엔드 코드 확인하기 위한 수준):
- HTML
- CSS
- JavaScript
- SockJS
- Stomp.js
⭐ 파트별 구현 사항
✅ 백엔드 구현 사항
- Redis 설정: Redis를 사용하여 문서 데이터를 저장합니다.
- WebSocket 연결 설정: 클라이언트와의 실시간 통신을 위해 WebSocket을 설정합니다.
- REST API 구현: 문서의 CRUD 기능을 제공하는 RESTful API를 구현합니다.
- WebSocket 메시지 처리: 클라이언트로부터의 문서 업데이트 요청을 처리하고, 다른 클라이언트에 브로드캐스트합니다.
✅ 프론트엔드 구현 사항
- HTML/CSS 레이아웃: 사용자 인터페이스를 구축합니다.
- JavaScript WebSocket 연결: 서버와의 WebSocket 연결을 설정하고, 실시간으로 문서 내용을 업데이트합니다.
- API 호출: 문서 생성, 조회, 삭제를 위한 REST API 호출을 구현합니다.
- 입력 처리: 사용자의 입력을 처리하고, 서버에 실시간으로 전송합니다.
⭐ 전체적인 흐름(백엔드)
- 사용자가 index.html 페이지를 열면 WebSocket을 통해 서버에 연결합니다.
- 사용자가 텍스트 에디터에 내용을 입력하면 input 이벤트가 발생하고, 해당 내용이 서버로 전송됩니다.
- 서버에서는 WebSocketController의 updateDocument() 메서드가 호출되어 문서가 업데이트됩니다.
- 업데이트된 문서는 메시지 브로커를 통해 모든 클라이언트에게 전송되어 실시간으로 반영됩니다.
- 문서의 CRUD 작업은 DocumentController를 통해 HTTP 요청으로 처리됩니다.
⭐ 파일 구조 및 설명
✅ config/RedisConfiguration.java
package com.example.collab.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@EnableCaching
public class RedisConfiguration {
@Bean
public JedisConnectionFactory jedisConnectionFactory() {
return new JedisConnectionFactory();
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
return template;
}
}
- 역할: Redis 설정을 정의합니다.
- 기술: Spring Data Redis.
- 필요성: 캐싱 및 데이터 저장을 위해 Redis를 사용합니다.
- 구성:
- JedisConnectionFactory와 RedisTemplate을 Bean으로 등록하여 Redis와의 연결을 설정합니다.
✅ config/WebSocketConfig.java
package com.example.collab.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 config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
}
- 역할: WebSocket 설정을 정의합니다.
- 기술: Spring WebSocket.
- 필요성: 실시간 데이터 전송을 위해 WebSocket을 사용합니다.
- 구성:
- 메시지 브로커를 설정하고 /ws 엔드포인트를 등록하여 클라이언트가 WebSocket에 연결할 수 있도록 합니다.
✅ controller/DocumentController.java
package com.example.collab.controller;
import com.example.collab.model.Document;
import com.example.collab.repository.DocumentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/documents")
public class DocumentController {
private final DocumentRepository documentRepository;
@Autowired
public DocumentController(DocumentRepository documentRepository) {
this.documentRepository = documentRepository;
}
@PostMapping
public ResponseEntity<Document> createDocument(@RequestBody Document document) {
documentRepository.save(document);
return new ResponseEntity<>(document, HttpStatus.CREATED);
}
@GetMapping("/{id}")
public ResponseEntity<Document> getDocument(@PathVariable String id) {
Document document = documentRepository.findById(id);
if (document != null) {
return new ResponseEntity<>(document, HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteDocument(@PathVariable String id) {
documentRepository.delete(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
- 역할: HTTP 요청을 처리하는 REST 컨트롤러입니다.
- 기술: Spring MVC.
- 필요성: 클라이언트에서 문서 CRUD 작업을 처리합니다.
- 메서드:
- createDocument(): 문서를 생성합니다.
- getDocument(): 문서를 조회합니다.
- deleteDocument(): 문서를 삭제합니다.
✅ controller/WebSocketController.java
package com.example.collab.controller;
import com.example.collab.model.Document;
import com.example.collab.service.DocumentService;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class WebSocketController {
private final DocumentService documentService;
public WebSocketController(DocumentService documentService) {
this.documentService = documentService;
}
@MessageMapping("/update")
@SendTo("/topic/documents")
public Document updateDocument(Document document) {
documentService.createOrUpdateDocument(document);
return document;
}
}
- 역할: WebSocket 메시지를 처리합니다.
- 기술: Spring WebSocket.
- 필요성: 클라이언트에서 업데이트된 문서를 실시간으로 브로드캐스트합니다.
- 메서드:
- updateDocument(): 문서 내용을 업데이트하고 브로드캐스트합니다.
✅ model/Document.java
package com.example.collab.model;
import java.io.Serializable;
import java.util.List;
public class Document implements Serializable {
private String id;
private List<String> content; // List<String>으로 변경
// Getters and Setters
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public List<String> getContent() { // List<String> 타입으로 변경
return content;
}
public void setContent(List<String> content) { // List<String> 타입으로 변경
this.content = content;
}
}
- 역할: 문서 모델을 정의합니다.
- 기술: Java 기본 클래스.
- 필요성: 문서의 구조를 정의하고 직렬화를 위해 Serializable을 구현합니다.
✅ repository/DocumentRepository.java
package com.example.collab.repository;
import com.example.collab.model.Document;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.beans.factory.annotation.Autowired;
@Repository
public class DocumentRepository {
private final RedisTemplate<String, Object> redisTemplate; // Object로 변경
private HashOperations<String, String, Object> hashOperations; // Object로 변경
@Autowired
public DocumentRepository(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
this.hashOperations = redisTemplate.opsForHash();
}
public void save(Document document) {
hashOperations.put("DOCUMENT", document.getId(), document);
}
public Document findById(String id) {
return (Document) hashOperations.get("DOCUMENT", id); // 캐스팅 필요
}
public void delete(String id) {
hashOperations.delete("DOCUMENT", id);
}
}
- 역할: Redis와의 데이터 상호작용을 관리합니다.
- 기술: Spring Data Redis.
- 필요성: Redis에 문서를 저장, 조회 및 삭제합니다.
✅ service/DocumentService.java
package com.example.collab.service;
import com.example.collab.model.Document;
import com.example.collab.repository.DocumentRepository;
import org.springframework.stereotype.Service;
@Service
public class DocumentService {
private final DocumentRepository documentRepository;
public DocumentService(DocumentRepository documentRepository) {
this.documentRepository = documentRepository;
}
public void createOrUpdateDocument(Document document) {
documentRepository.save(document);
}
public Document getDocument(String id) {
return documentRepository.findById(id);
}
}
- 역할: 비즈니스 로직을 처리합니다.
- 기술: Spring Framework.
- 필요성: Repository를 통해 데이터를 관리하고 서비스 로직을 제공합니다.
✅ resources/static/index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>협업 텍스트 편집기</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.1/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<style>
#editor {
width: 100%;
height: 300px;
border: 1px solid #ccc;
padding: 10px;
overflow-y: auto;
white-space: pre-wrap; /* 줄바꿈을 위해 추가 */
}
</style>
</head>
<body>
<h1>협업 텍스트 편집기</h1>
<div id="editor" contenteditable="true"></div>
<script>
const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);
stompClient.connect({}, (frame) => {
console.log('WebSocket 연결 성공:', frame);
stompClient.subscribe('/topic/documents', (message) => {
const updatedDocument = JSON.parse(message.body);
console.log('수신된 문서:', updatedDocument);
document.getElementById('editor').textContent = updatedDocument.content.join('\n');
});
}, (error) => {
console.error('WebSocket 연결 실패:', error);
});
document.getElementById('editor').addEventListener('input', () => {
const content = document.getElementById('editor').textContent.split('\n');
const docToSend = { id: '1', content: content };
stompClient.send("/app/update", {}, JSON.stringify(docToSend));
});
// 엔터 키를 눌렀을 때 줄바꿈 처리
document.getElementById('editor').addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault(); // 기본 동작 방지
const range = window.getSelection().getRangeAt(0);
range.deleteContents(); // 현재 선택된 내용을 삭제
range.insertNode(document.createTextNode('\n')); // 줄바꿈 추가
range.collapse(false); // 커서를 줄바꿈 후 위치로 이동
}
});
</script>
</body>
</html>
- 역할: 클라이언트 측 UI를 제공합니다.
- 기술: HTML, JavaScript (SockJS, STOMP).
- 필요성: 사용자가 텍스트를 편집하고 실시간으로 업데이트할 수 있도록 합니다.
- 백엔드 위주로 개발했기 때문에 여기선 id를 1로 고정해둠.
✅ application.properties
spring.application.name=collab
spring.data.redis.host=localhost
spring.data.redis.port=6379
⭐ 테스트하는 방법
- 서버 실행
- redis 서버 실행 (redis가 없다면 https://github.com/microsoftarchive/redis/releases 여기서 최신 버전의 redis.zip 설치해서 폴더 안의 redis-server.exe 실행하면 됨)
- 두 탭에서 http://localhost:8080/index.html 접속
- 텍스트 치기
- 영상처럼 다른 곳에서도 쓴 내용이 실시간으로 써지는 걸 확인할 수 있음.
📌 깃허브