자바&스프링

[Spring] WebSocket & STOMP로 채팅 구현하기 (2)

온한온 2025. 3. 17. 16:54

우선, 기본 세팅으로 Entity와 WebSocketConfig를 설정했습니다.

 

Entity

ChatRoom
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "chat_room")
public class ChatRoom extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "title", nullable = false)
    @Size(min = 1, max = 100)
    private String title;

    @Column(name = "is_secret", length = 1, nullable = false)
    private String isSecret;
}

 

ChatMessage
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "chat_message")
public class ChatMessage extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "message", columnDefinition = "TEXT")
    @NotEmpty
    private String message;

    @ManyToOne
    @JoinColumn(name = "chat_room_id", referencedColumnName = "id", nullable = false)
    private ChatRoom chatRoom;

    @ManyToOne
    @JoinColumn(name = "employee_id", referencedColumnName = "id", nullable = false)
    private Employee employee;

    public void updateMessage(String message) {
        this.message = message;
    }
}

 

ChatRoom Entity나 ChatMessage Entity는 크게 어려움 없이 만들어줬습니다.

이 부분도 설정한 ERD에 맞춰서 원하는 대로 하면 될 듯 합니다...

 

Chat & ChatId

Chat
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "chat")
public class Chat {

    @EmbeddedId
    private ChatId id;

    @MapsId("chatRoomId")
    @ManyToOne(targetEntity=ChatRoom.class)
    private ChatRoom chatRoom;

    @MapsId("employeeId")
    @ManyToOne(targetEntity=Employee.class)
    private Employee employee;

}

 

@EmbeddedId로 ChatId가 기본키라는 것을 알려줍니다.

@MapsId로 식별자 클래스의 기본키인 chatRoomId와 employeeId를 잡아줬습니다.

 

@MapsId를 사용하지 않으면 오류가 나거나 하기도 합니다...

저는 이걸 안 잡아줬을 때 각각의 외래키가 2개씩... 중복되어 생겼었습니다.

 

ChatId
@Getter
@Setter
@Embeddable
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatId implements Serializable {
    @JoinColumn(name = "chat_room_id")
    private Long chatRoomId;

    @JoinColumn(name = "employee_id")
    private Long employeeId;

    // equals와 hashCode()...
}

임베디드 타입을 사용하기 위해 ChatId 클래스를 만들고 @Embeddable를 붙여줬습니다.

또한, 직렬화하기 위해서 Serializable를 implements 했습니다.

 

💡 식별자 클래스는 다음의 내용을 만족해야 합니다

  • @Embeddable 어노테이션 사용
  • Serializable 인터페이스 구현
  • equals, hashCode 구현
  • 기본 생성자 필요
  • 식별자 클래스의 접근 제어자 범위 : public

이런 식으로 복합키를 설정해줬습니다.

 

WebSocketConfig

@Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // stomp 접속 주소 url = ws://localhost:8080/ws
        registry.addEndpoint("/ws") // socket 연결 엔드포인트
                .setAllowedOrigins("http://localhost:3000") // CORS 허용범위
                .withSockJS(); // 브라우저 호환성을 위해
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic", "/queue"); // 메시지를 구독 요청하는 엔드 포인트(public, private)
        registry.setApplicationDestinationPrefixes("/app"); // 메시지를 발행하는 엔드 포인트
    }

Security 적용 전 기본적인 WebSocketConfig 설정입니다. 

registerStompEndpoints

📌 addEndpoint

addEndpoint는 socket 연결 엔드포인트입니다.

React에서 웹소켓으로 연결할 때 주석에 적힌 것처럼 new SockJS('http://localhost:8080/ws') 이런 식으로 적어주면 됩니다.

 

📌 setAllowedOrigins

setAllowedOrigins는 CORS 허용범위로 setAllowedOrigins("*") 라고 적으면 모든 CORS 요청을 허용하는 것입니다.

저는 Security를 써서 프론트와 연결하기 위해 .setAllowedOrigins("http://localhost:3000")라고 적었습니다.

주소를 더 추가하고 싶다면 .setAllowedOrigins("http://localhost:3000", "http://domain.com", ...) 이처럼 뒤에 추가하면 됩니다.

 

📌 withSockJS

WebSocket을 지원하지 않는 브라우저에서도 WebSocket 요청을 연결하기 위해서 사용합니다.

더불어 React에서 연결할 때 new SockJS('http://localhost:8080/ws')로 http 연결이 가능합니다만,

witSockJS가 없을 경우 ws://localhost:8080/ws로 ws 연결을 해줘야 합니다.

 

configureMessageBroker

📌 enableSimpleBroker("/topic", "/queue")

주석에 적힌대로 메시지를 구독 요청하는 엔드 포인트(public, private)입니다.

백엔드의 ChatMessageController에서 사용했습니다.

 

topic은 여러 명의 채팅을, queue는 일대일 채팅 정도로 생각하면 됩니다.

원래 채팅 시스템을 만들 때 일대일 채팅도 추가하기 위해서 /queue까지 넣었지만........

 

📌 setApplicationDestinationPrefixes("/app")

메시지를 발행하는 엔드 포인트입니다.

React에서 백엔드로 메시지를 보낼 때 사용했습니다.