서버, 클라이언트 연결

간단한(하지만 간단하지 않은) 채팅 앱을 만들어서 그것에 대한 트래픽을 제어하고 성능을 개선하는 프로젝트를 구상했다. 이론에 그치고 어렴풋이 알고 있던 내용들을 좀 더 보강함과 동시에, 같은 방법이어도 다른 수단인 것들에 대한 이해도를 높이기 위해 해당 프로젝트를 구상했다.

예전에 공부했던 프론트엔드 지식까지 최대한 활용해서 타입스크립트 리액트 기반 클라이언트를 구축하고, 내가 지향하는 백엔드 지식인 자바 스프링 기반 서버를 구축해서 상호 간의 실시간 통신을 구축해야겠다고 생각했다.

1. MVP(Minimum Viable Product)

프로젝트의 최소 달성 수단에 대해 고민했다. 뭔가 하나에 빠지면 그거에 계속 몰두하는 내 성격상, 자칫하다가 프론트엔드 트러블 슈팅에 빠질 염려가 다분했기 때문. 그리고 각각의 달성 수단에 대한 서순 역시 중요한 문제기도 했다. 프로젝트의 구현도가 복잡해질 수록 점점 돌이킬 수(?) 없어지고 수정이 어려워지기 때문.

1) 실시간 채팅

(1) 전체 채팅과 개인 채팅

간단히 말해서, 공개된 전체 채팅회원과 회원 간의 개인 일대일 채팅에 대한 구현의 선택 기준이다. 실시간 통신이라는 키워드에 집중한다면 공개 전체 채팅에만 그쳐도 상관이 없겠지만, 아무리 백엔드에 집중하는 프로젝트여도 핵심 기능이 되는 채팅 자체가 프레임 수준으로만 구축이 된다면 그만큼 향후의 확장성이나 성능 테스트를 수행할 대상이 너무 적을 것 같았다. 그래서 방향을 전체 채팅을 우선 구현하면서 구독자 관리 등의 보편적인 채팅 기능을 구현하고, 이것을 바탕으로 개인 일대일 채팅까지 바라보는 방향을 생각헀다.

(2) 보편적인 채팅 기능

현재 생각하는 것은, 채팅에 참여하는 회원의 관리 및 클라이언트 표시다. 일반적인 채팅 애플리케이션은 중앙부에 채팅 입력 및 메세지 송수신창이 있고, 측면에 참여자의 정보를 파악할 수 있는 UI가 제공된다. 이런 구독자를 관리할 수 있는 기능을 구현하면, 향후 회원 간의 친구 관계, 온라인 및 오프라인 여부 표시까지 고려할 수 있다.

또한, 채팅방에 대한 구독의 관리 역시 생각할 수 있다. 일반적인 메신저는 뒤로 가기 등의 버튼을 눌렀다고 바로 채팅창의 구독을 종료시키지 않는다. 회원의 명시적인 의사 표현에 따라 채팅의 구독 종료 여부를 설정해야 한다. 이는 곧 채팅 로그에 대한 관리 및 알람 구현까지 생각할 수도 있다. 설령 앱을 종료했다고 해도 그때까지 수신된 메세지를 확인할 수도 있고, 그때까지의 채팅 로그를 관리해야 하며 알람을 제공할 수도 있을 것이다.

2) 구현 기술 수단 및 아키텍처

(1) PostgreSQL

회원, 채팅 구조 객체 등에 대한 관리는 JPA 기반으로 CRUD를 수행하고 그것을 보관할 수 있는 RDBMS를 채택한다. MySQL과 PostgreSQL 중 어떤 것을 선택할까 고민했고, 단순 읽기 성능만 봤을 때는 MySQL이 유리하지만, 향후 고급 기능을 확장했을 때의 시나리오를 고려해서 PostgreSQL을 선택했다.

(2) Redis

지난 연습 프로젝트에서도 자주 활용했고, 기능의 유용성을 익히 실감한 바가 있어서 이번에도 채택을 하였고 또한 채팅 앱에서의 실시간 메세지 브로드캐스트의 수단으로 사용하기 위해 선택했다. 비단 메세지 브로드캐스트 외에도 입출력 성능이 뛰어난 인메모리 데이터베이스라는 특징을 살려, 조회가 자주 일어나는 이른바 인증 정보 같은 경우에도 활용할 수 있다고 생각했다.

(3) WebSocket STOMP

실시간 양방향 통신을 오픈하면서 HTTP를 업그레이드함으로써 채팅을 실현할 수 있는 중요 수단이다. 클라이언트와 서버 간의 1차적으로 실시간 통신을 개설하는 역할을 맡고, 상술한 Redis가 2차적으로 채팅방 구독 및 메세지 전파를 맡게 된다. 단순 WebSocket에 그치지 않고 메세지 브로커와의 통합을 위해 STOMP를 활용한다.

(4) Spring JPA, Security

SQL문에 대한 의존성을 줄여 서버 비즈니스 로직 개발에 집중하게 하는, 자바 스프링 영역의 ORM인 JPA를 통해 기본적인 개발을 수행하고 회원 인증에 대한 검증 수단을 DispatcherServelet을 지나기 전의 Filter 단계에서 확보시키는 Spring Security를 통해 구현한다.

(5) 클라이언트

프론트엔드는 크게 언급하진 않겠지만, 기본적으로 TypeScript를 기반으로 한 React를 통해 개발하며, UI의 동작 로직은 Styled Components를 사용하고 전역적인 상태 관리는 웹 스토리지와 더불어 Redux Toolkit로 수행한다. 서버와의 HTTP API 콜은 Axios를 사용하며, 실시간 통신 개설은 서버와 마찬가지로 WebSocket을 활용한다.

1차적으로 완성된 개발 아키텍처는 아래와 같으며, 모놀리식 아키텍처를 채택해서 기본 기능인 단체 공개 채팅을 우선 구현한다.

2. CORS 극복

아키텍처에서 보았듯이 클라이언트는 리액트 라이브러리로 구현되고, 서버는 스프링 프레임워크로 구현된다. 그리고 둘의 기본 동작 포트는 각각 3000, 8080이다. 물론 수정은 가능하나 말하고자 하는 것은 둘은 서로 다른 포트에서 개발 서버가 돌아간다.

기본적으로 URL은 위와 같은 구성 요소를 지닌다. 그리고 동일 출처 정책(Same Origin Policy)이라는 것이 있다. 이것은 프로토콜, 도메인, 호스트가 같아야 함을 의미한다. 이것을 지키지 않으면 CORS, 즉 Cross Origin Resources Sharing(교차 출처 리소스 공유 문제)가 발생하게 된다.

프로토콜, 도메인, 호스트가 같아야 CORS 문제가 발생하지 않는다. 보안을 위해 동일 출처가 아닌 경우는 악의적인 다른 출처의 접근으로 간주시키기 때문이다. 현재는 개발 단계로써, HTTP 프로토콜localhost 도메인 호스트는 동일하지만 포트 번호가 다르기 때문에 CORS 문제가 발생하는 것이다.

이를 확인하고 선제적인 조치를 취했으나, 아마 향후 배포할 때 수정 작업이 이뤄질 것 같다. 왜냐하면 클라이언트의 배포는 Vercel을 생각하고 있는데 Vercel은 HTTPS로 자동 배포가 되기 때문이다. 즉, 서버 역시 HTTPS 배포가 되어야 할 것이다(...)

1) 서버에서의 조치

서버는 현재 스프링부트를 기반으로, 스프링 시큐리티에서 인증 작업을 담당한다. 하여 스프링 시큐리티 설정에서 CORS 관련하여 추가 설정을 진행한다.

// WebSecurityConfig

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable);  // csrf 토큰 무효화 설정을 해야 인증 예외 허용 가능
        http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
        
        // ...
        
          }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList(clientUrl));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

CorsConfigurationSource 타입 반환 메소드에서 클라이언트 URL 및 허용 관련 설정들을 세팅 후, 해당 메소드를 빈으로 등록시킨다. 이를 통해 클라이언트 개발 서버와의 CORS 문제를 극복할 수 있게 된다.

2) 클라이언트에서의 조치

크게 언급하지는 않겠지만, React에서의 미들웨어를 세팅해서 CORS를 극복할 수 있다.

const apiBaseUrl = process.env.REACT_APP_API_BASE_URL;

module.exports = function(app: any){
    app.use(
      "/api",
      createProxyMiddleware( {
        target: apiBaseUrl,
        changeOrigin: true
      })
    )
};

위와 같은 세팅을 마무리하면 정상적으로 통신이 이뤄지고 CORS를 극복할 수 있게 된다. 다만 위에서 언급했듯이 배포까지 이뤄지면 추가적인 세팅을 해야할 수도 있다. 늘 느끼지만, 배포가 제일 무섭다(...)

profile
scientia est potentia / 벨로그 이사 예정...

0개의 댓글