사이먼's 코딩노트
[SpringBoot] 멀티 채팅방 (1) 본문
[Ajax, Stomp 기반 멀티 채팅방 구현]
- 이번 포스팅에서는 Ajax, Stomp 기반의 멀티 채팅방을 구현해 볼 예정입니다.
- 전체 코드를 포스팅할 순 없기 때문에 전체 구조와 코드를 확인하시려면 리포지터리 주소를 참고 부탁드립니다.
- 해당 프로젝트는 SpringBoot v_3.3.0, Java 21 기반으로 세팅되어 있습니다.
- 리포지터리 URL 주소 : https://github.com/psm817/chat_review
GitHub - psm817/chat_review
Contribute to psm817/chat_review development by creating an account on GitHub.
github.com
[Ajax란?]
- Ajax는 Asynchronous JavaScript and XML의 약어로 JavaScript와 XML을 이용한 비동기적 정보 교환 기법으로 요즘에는 XML보다는 JSON을 주로 사용하기도 한다.
- 브라우저의 HttpRequest를 이용해서 전체 페이지를 새로고침하지 않아도 페이지 일부를 변경할 수 있도록 JS를 실행해 서버에 데이터만을 별도로 요청하는 기법이다.
- 사실 간단하게 생각하면 사용자 각자가 같은 브라우저 공간에서 게시물을 등록할 때 Ajax와 같은 기법을 통해 실시간으로 등록한 게시물이 마치 멀티 채팅방처럼 서로 보인다고 생각하면 쉽다.
- 보통 jQuery를 이용해서 Ajax를 실행하지만 이번 멀티 채팅방 구현에서는 jQuery를 따로 사용하지는 않을 예정이다.
- Ajax의 장점은 웹페이지의 속도 향상과 서버의 처리가 완료될 때 까지 기다리지 않고 처리가 가능하다는 점이다.
- Ajax의 단점은 페이지 이동이 없기 때문에 보안상 문제가 있을 수 있고, 연속으로 데이터 요청 시 서버 부하가 증가된다.
[ChatRoom 생성]
- 먼저 채팅이 이루어지기 위해 채팅방과 관련된 ChatRoom 엔티티를 생성해야한다.
- ChatRoom.java 클래스를 생성하여 아래와 같이 코드를 작성한다.
- 하나의 채팅방에 여러개의 채팅 메시지가 포함되기 때문에 @OneToMany 어노테이션을 포함하고, 최근 채팅 메시지가 써진 순서대로 나열되도록 @OrederBy("id DESC) 어노테이션도 포함해준다.
- writeMessage() 메서드를 통해 실제로 채팅 메시지를 저장하는 기능을 구현한다.
@Entity
@AllArgsConstructor(access = PROTECTED)
@NoArgsConstructor(access = PROTECTED)
@SuperBuilder
@Getter
@Setter
@ToString(callSuper = true)
public class ChatRoom extends BaseEntity {
@Getter
private String name;
@OneToMany(mappedBy = "chatRoom", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
@Getter
@ToString.Exclude
@OrderBy("id DESC")
@JsonIgnore
private List<ChatMessage> chatMessages = new ArrayList<>();
public ChatRoom(String name) {
this.name = name;
}
public ChatMessage writeMessage(String writerName, String content) {
ChatMessage chatMessage = ChatMessage
.builder()
.chatRoom(this)
.writerName(writerName)
.content(content)
.build();
chatMessages.add(chatMessage);
return chatMessage;
}
}
[ChatRoomController 생성]
- 엔티티를 작성하였다면, 이제 ChatRoomController를 통해 채팅방 리스트, 채팅방 생성, 채팅방 상세보기를 위한 주소 매핑을 해야한다.
- ChatRoomController.java 클래스를 생성하여 아래와 같이 코드를 작성한다.
- showRoom() 메서드는 chatRoom의 id를 기준으로 각 채팅방을 불러와 room.html를 return하여 채팅방 화면을 보여준다.
- showMake()와 make() 메서드는 채팅방을 생성하기 위해 코드를 작성했다.
- showList() 메서드는 채팅방 리스트에서 생성된 모든 채팅방을 보여주기 위해 작성했고 findAll()을 통해 모든 채팅방을 불러와 list.html를 return하여 리스트 화면을 보여준다.
- write() 메서드는 각 채팅방에서 채팅 메시지를 작성하기 위해 작성된 코드이고 RsData 형식을 통해 채팅 메시지 ㅣ작성이 완료되면 return 값으로 resultCode와 성공 msg, 메시지 데이터를 보내준다.
- getMessageAfter() 메서드는 작성된 채팅 메시지를 최신화하여 불러오며, 이 기능이 실질적으로 실시간 채팅이 가능하게끔 역할을 한다.
@Controller
@RequestMapping("/chat/room")
@RequiredArgsConstructor
public class ChatRoomController {
private final ChatRoomService chatRoomService;
private final ChatMessageService chatMessageService;
@GetMapping("/{roomId}")
public String showRoom(
@PathVariable("roomId") final long roomId,
@RequestParam(value = "writerName", defaultValue = "NoName") final String writerName,
Model model
) {
ChatRoom room = chatRoomService.findById(roomId).get();
model.addAttribute("room", room);
return "domain/chat/chatRoom/room";
}
@GetMapping("/make")
public String showMake() {
return "domain/chat/chatRoom/make";
}
@PostMapping("/make")
public String make(
@RequestParam(value = "name") final String name
) {
chatRoomService.make(name);
return "redirect:/chat/room/list";
}
@GetMapping("/list")
public String showList(Model model) {
List<ChatRoom> chatRooms = chatRoomService.findAll();
model.addAttribute("chatRooms", chatRooms);
return "domain/chat/chatRoom/list";
}
@Getter
@Setter
public static class WriterRequestBody {
private String writerName;
private String content;
}
@Getter
@AllArgsConstructor
public static class WriterResponseBody {
private Long chatMessageId;
}
@PostMapping("/{roomId}/write")
@ResponseBody
public RsData<WriterResponseBody> write(
@PathVariable("roomId") final long roomId,
@RequestBody final WriterRequestBody requestBody
) {
ChatMessage chatMessage = chatRoomService.write(roomId, requestBody.getWriterName(), requestBody.getContent());
return RsData.of(
"S-1",
"%d번 메시지를 작성하였습니다.".formatted(chatMessage.getId()),
new WriterResponseBody(chatMessage.getId())
);
}
@Getter
@AllArgsConstructor
public static class GetMessagesAfterResponseBody {
private List<ChatMessage> messages;
}
@GetMapping("/{roomId}/messagesAfter/{afterId}")
@ResponseBody
public RsData<GetMessagesAfterResponseBody> getMessagesAfter(
@PathVariable("roomId") final long roomId,
@PathVariable("afterId") final long afterId
) {
List<ChatMessage> messages = chatMessageService.findByChatRoomIdAndIdAfter(roomId, afterId);
return RsData.of(
"S-1",
"%d개의 메시지를 가져왔습니다.".formatted(messages.size()),
new GetMessagesAfterResponseBody((messages))
);
}
}
[ChatRoomService 생성]
- ChatRoomController 작성이 완료됐다면, 컨트롤러에서 서비스를 통해 불러온 메서드들을 정상적으로 수행하기 위해서 코드 작성이 필요하다.
- ChatRoomService.java 클래스를 생성하여 아래와 같이 코드를 작성한다.
- make() 메서드는 컨트롤러에서 채팅방 생성이라는 주소가 매핑됐을 때 수행하며 builder().build()를 통해 채팅방을 생성하고 리포지터리를 통해 생성된 채팅방을 save한다.
- findAll()은 컨트롤러에서 전체 채팅방 리스팅을 위해 사용되는 메서드로 리포지터리를 통해 findAll() 메서드를 호출한다.
- findById()는 각 채팅방의 정보를 가져오기 위해 사용되는 메서드로 리포지터리를 통해 findById() 메서드를 호출한다.
- write()메서드는 각 채팅방 정보를 findById()로 가져오고 ChatRoom 엔티티에 작성한 writeMessage() 메서드를 호출한다.
@Service
@RequiredArgsConstructor
public class ChatRoomService {
private final ChatRoomRepository chatRoomRepository;
@Transactional
public ChatRoom make(String name) {
ChatRoom chatRoom = ChatRoom.builder()
.name(name)
.build();
chatRoomRepository.save(chatRoom);
return chatRoom;
}
public List<ChatRoom> findAll() {
return chatRoomRepository.findAll();
}
public Optional<ChatRoom> findById(long roomId) {
return chatRoomRepository.findById(roomId);
}
@Transactional
public ChatMessage write(long roomId, String writerName, String content) {
ChatRoom chatRoom = chatRoomRepository.findById(roomId).get();
ChatMessage chatMessage = chatRoom.writeMessage(writerName, content);
return chatMessage;
}
}
[ChatRoomRepository 생성]
- 리포지터리에서는 따로 코드작성은 필요없고 JpaRepository를 상속받게 하여 기본적으로 내장되어 있는 save(), findAll(), findById()와 같은 메서드가 실행될 수 있도록 한다.
- ChatRoomRespository.java 클래스를 생성하여 아래와 같이 interface로 변경한다.
@Repository
public interface ChatRoomRepository extends JpaRepository<ChatRoom, Long> {
}
[ChatMessage 생성]
- ChatRoom 채팅방 관련 MVC를 모두 구현하였다면, 이번에는 채팅 메시지와 관련된 ChatMessage MVC를 구현해야된다.
- 컨트롤러의 경우는 이미 ChatRoomController에서 채팅 메시지와 관련된 매핑이 모두 포함되어있기 때문에 따로 작성은 하지 않을 예정이다.
- 먼저 ChatMessage.java 클래스를 생성하여 아래와 같이 코드를 작성한다.
- ChatRoom과 반대로 여러개의 채팅 메시지가 하나의 채팅방에 속하기 때문에 @ManyToOne 어노테이션을 포함해아한다.
- 나머지는 채팅 메시지를 작성한 작성자 이름과 메시지 내용 컬럼을 생성한다.
@Entity
@AllArgsConstructor(access = PROTECTED)
@NoArgsConstructor(access = PROTECTED)
@SuperBuilder
@Getter
@Setter
@ToString(callSuper = true)
public class ChatMessage extends BaseEntity {
@ManyToOne
private ChatRoom chatRoom;
@Getter
private String writerName;
@Getter
private String content;
}
[ChatMessageService 생성]
- ChatRoomController.java에서 getMessageAfter() 라는 메서드를 통해 최근 작성된 메시지를 불러오도록 구현하였고, 채팅 메시지 서비스에서 해당 역할을 수행하는 findByChatRoomIdAndIdAfter() 메서드를 구현해야한다.
- ChatMessageService.java 클래스를 생성하여 아래와 같이 코드를 작성한다.
- findByChatRoomIdAndIdAfter() 메서드의 역할은 roomId를 통해 현재 메시지를 작성한 채팅방을 불러오고, afterId를 통해 최근에 작성된 채팅 메시지를 가져오는 역할을 한다.
- 이 기능은 실제로 리포지터리에서 수행하기 때문에 return 값으로 리포지터리를 통해 해당 메서드를 호출한다.
@Service
@RequiredArgsConstructor
public class ChatMessageService {
private final ChatMessageRepository chatMessageRepository;
public List<ChatMessage> findByChatRoomIdAndIdAfter(long roomId, long afterId) {
return chatMessageRepository.findByChatRoomIdAndIdAfter(roomId, afterId);
}
}
[ChatMessageRepository 생성]
- 리포지터리에서는 ChatRoomRepository와 마찬가지로 JpaRepository를 상속받게 하여 서비스에서 호출한 findByChatRoomIdAndIdAfter()를 수행하도록 한다.
- ChatMessageRepository.java 클래스를 생성하여 아래와 같이 코드를 작성한다.
@Repository
public interface ChatMessageRepository extends JpaRepository<ChatMessage, Long> {
List<ChatMessage> findByChatRoomIdAndIdAfter(long roomId, long afterId);
}
반응형
'Java > SpringBoot' 카테고리의 다른 글
[SpringBoot] 멀티 채팅방 (3) (0) | 2024.06.19 |
---|---|
[SpringBoot] 멀티 채팅방 (2) (0) | 2024.06.19 |
[SpringBoot] REST API (4) (0) | 2024.06.19 |
[SpringBoot] REST API (3) (0) | 2024.06.16 |
[SpringBoot] REST API (2) (0) | 2024.06.16 |