사이먼's 코딩노트

[SpringBoot] 멀티 채팅방 (1) 본문

Java/SpringBoot

[SpringBoot] 멀티 채팅방 (1)

simonpark817 2024. 6. 19. 11:54

[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