1/10 ~ 1/11일 해커톤 회고록

해커톤 회고록

해커톤 기간에는 빠른 기능 구현을 위해 익숙한 코드나 레퍼런스를 참고하여 기능을 완성하는 데 집중했습니다.
프로젝트가 끝난 후, 내가 작성한 코드보다 더 나은 방법은 없는지, 이 기술을 이렇게 사용하는 것이 맞는지라는 의문을 해결하기 위해 이 글을 작성하였습니다.


1. 실시간 통신: STOMP vs Raw WebSocket

구현

게임방(Room) 단위로 이벤트를 전송해야 했기에, 서칭을 통해 STOMP 설정을 가져와 적용했습니다. 당시에는 enableSimpleBroker가 정확히 어떤 원리로 작동하는지 모른 채 주소만 맞추면 메시지가 간다는 사실 정도만 알고 코드를 작성하였습니다.

학습

해커톤이 끝난후 새로운 기술에 대한 이해를 위해 CS 이론과 함께 동작 원리를 고민해보았습니다.

CS 이론: registerStompEndpoints와 Handshake의 해부

registry.addEndpoint("/ws"))가 정확히 언제 개입하는지 분석을 진행했습니다.

[Step 1] L4 Transport Layer: TCP 3-way Handshake

  • 웹소켓도 결국 TCP 기반입니다. 클라이언트가 서버에 접속을 시도하면, 가장 먼저 OS 레벨에서 3-way Handshake(SYN -> SYN/ACK -> ACK)가 일어납니다.
  • 이 단계는 Spring Boot(애플리케이션)가 관여하기 전, 커널 단에서 처리되어 Connection이 만들어집니다.

[Step 2] L7 Application Layer: HTTP Upgrade Handshake

  • TCP 연결이 맺어진 직후, 클라이언트는 일반적인 HTTP 요청을 보냅니다. 단, 헤더가 조금 다릅니다.
    GET /ws HTTP/1.1
    Host: localhost:8080
    Upgrade: websocket
    Connection: Upgrade
  • 여기서 registerStompEndpoints가 작동합니다.
    • Spring Container는 요청 URL이 /ws임을 확인하고, 해당 엔드포인트가 WebSocketConfig에 등록되어 있는지 검사합니다.
    • 등록되어 있다면, 서버는 HTTP 프로토콜을 웹소켓 프로토콜로 전환(Switching)하겠다는 응답을 보냅니다.
      HTTP/1.1 101 Switching Protocols
      Upgrade: websocket
      Connection: Upgrade
  • 결론: registry.addEndpoint("/ws")는 단순한 URL 매핑이 아니라, "HTTP 프로토콜을 WebSocket 프로토콜로 업그레이드해 달라"는 요청을 승인하는 Gateway 역할을 합니다.

2. 라우팅 문제: Raw WebSocket은 왜 파이프에 불과한가?

STOMP과 Raw WebSocket 비교 분석했습니다.
가장 큰 차이는 '메시지를 누구에게 보낼 것인가(Routing)'에 대한 관리 주체입니다.

Raw WebSocket

  • Raw WebSocket은 클라이언트와 서버 간의 1:1 통신 라인일 뿐입니다. WebSocketSession 객체는 연결된 상태만 알 뿐, 이 유저가 1번 방에 있는지 2번 방에 있는지 알지 못합니다.
  • 특정 방(Room 1)에 있는 사람들에게만 메시지를 보내려면, 개발자가 직접 메모리에 지도를 그려야 합니다.
  • // 동시성 문제, 세션 종료 여부 체크 등 모든 것을 직접 관리해야 함

STOMP

  • 구조: STOMP는 Message Broker 패턴을 도입합니다.
  • 해결:
    • Subscribe: 클라이언트가 /topic/room/1이라는 주소를 구독하면, 브로커가 내부적으로 "이 세션은 1번 방 메시지를 받겠다"라고 메모해둡니다.
    • Publish: 서버가 /topic/room/1로 메시지를 던지면, 브로커가 알아서 해당 주소를 구독한 세션들을 찾아 뿌려줍니다.

2. 인증 객체: @AuthUser vs @AuthenticationPrincipal

구현

이 부분은 팀원이 진행하였습니다.

학습

기존에 UserDetails를 구현한 CustomUserPrincipal을 컨트롤러에서 직접 받아서 사용해본 경험이 있기때문에 어떤 방법이 더 나은 방법인지 알기 위해 학습을 진행했습니다.

장단점 분석

비교 항목기존 방식 (CustomUserPrincipal 직접 사용)개선 방식 (@AuthUser 커스텀 어노테이션)
장점• 별도의 설정 없이 Spring Security가 제공하는 기능을 즉시 사용 가능.
• 구현이 빠르고 직관적임.
결합도 감소: 컨트롤러가 Security 내부 구현체(CustomUserPrincipal)를 몰라도 됨.
가독성: @AuthUser만 붙이면 되므로 코드가 간결해짐.
유연성: Member 객체나 ID 등 원하는 형태로 가공해서 주입 가능.
단점강한 결합: 컨트롤러 코드가 Spring Security 기술에 종속됨.
• 매번 형변환이나 getter 호출이 필요해 코드가 지저분해짐.
• 초기 설정(Annotation 생성, Resolver 등록)에 공수가 들어감.
• 내부 동작 원리(Resolver)를 모르면 디버깅이 어려울 수 있음.
  • @AuthUser는 컨트롤러 계층과 보안 계층(Spring Security)을 분리해 주는 역할

3. JWT 인증 위치: Interceptor vs Filter

구현

이 부분 또한 팀원이 JWT 토큰 검증 로직을 Interceptor을 통해 구현했습니다.

학습

기존에 저는 filter를 통한 검증 방법을 알고 있었고 filter와 Interceptor로 구현하는 방법의 차이에 호기심을 가지고 Spring의 Request Lifecycle을 분석해 보았습니다.

  • 실행 시점의 차이:

    • Filter: DispatcherServlet(Spring MVC) 진입 전 (Web Container 영역)
    • Interceptor: DispatcherServlet 진입 후, 컨트롤러 실행 전 (Spring Context 영역)
  • 보안 원칙: 잘못된 토큰이나 악의적인 요청은 비즈니스 로직(Spring MVC)이 시작되기 전에 가장 앞단에서 차단해야 리소스 낭비를 막을 수 있음

  • Trade-off Filter에서 발생하는 예외는 @ControllerAdvice가 잡지 못하는 단점이 있었지만, 이는 Spring Security의 표준인 AuthenticationEntryPoint를 구현하여 해결할 수 있음


4. CORS 설정: YAML vs Java Config

학습

설정이 여기저기 흩어져 있는데, 하나로 통일해야 하지 않을까?라는 생각에 학습을 시작했습니다.

  • 분석:
    • YAML: Allowed Origins 같은 데이터는 환경(Local/Prod)마다 달라야 하므로 빌드 없이 변경 가능해야 함.
    • Java: Allowed Methods 같은 정책은 변하지 않는 로직이므로 코드에 명시해야 함.

끝으로

해커톤이 끝난 후, 기존에 알던 것과 다른 코드를 분석하여 비교하는 경험을 통해 성장하였습니다.

References

0개의 댓글