1. 사전 준비

① 노션 : https://www.notion.so/1-882180dd274943b683676575e8aae4dd

 

1조 기술멘토링 사전노트

코드 컨벤션

www.notion.so

 

② 준비한 질문 

  • GameSet에서 발언권이 바뀔 때 마다 DB에 접근을 해야될 것 같은데 속도나 효율성 때문에 Redis를 사용하고자 하는데 방향성이 맞을까요?
  • 한 방에 정답이 최대 4개인데 키워드 리스트를 Json화 시켜서 문자열로 DB에 저장을 했는데 이거를 다시 검증하려고 하면 배열화 시켜야하는 번거로움이 있는데, 조금 더 효율적인 방법이 있을까요?
  • queryDSL 예시를 찾아보던 중에 여러 queryDSL문 생성 후에 마지막에 flush / clear 로 DB에 저장하는 사례가 많았는데, 이 방법이 DB 접근을 최소화하기 위한게 맞을까요?
  • 실무에서 flush / clear를 사용할 일이 별로 없다는데 잘 이해가 안됩니다….ㅠㅠ (데이터를 DB로 보내는 다른 방법: 트랜잭션 commit() / JPQL로 만들어진 Query 실행)
  • Redis가 메모리 파편화 때문에 생각보다 많은 메모리를 차지하는데, Redis 메모리를 효율적으로 사용하는 방법이 있을까요?
  • WebRTC를 시그널링 서버를 구축하는 방향으로 구현은 성공했는데 최대 4명의 유저간 통신이 이루어지는 저희 서비스에서도 SFU 서버 통신을 고려해야 할까요?

 

2. 피드백 정리

① SA대시보드 피드백

 

② 1주차 피드백 (1월 7일 토요일)

1) 깃허브 

  • 코딩컨벤션 : 에어비엔비 참고 (스탠다드 처럼 쓰임 )  https://github.com/airbnb/javascript
  • 깃플로우 전략 : 순서가 중요( 하위 브랜치에서 상위 브랜치로 가야함 ) + Master branch 에는 배포된 코드만 있어야 한다
    • Hot fix branch - 실제로 현업에서 발생하는 경우는 많지 않음
    • 브랜치는 Commit 단위로 나누기 ( 커밋 로그를 나눠서 )

2) 기술스택

  • AWS - 기술스택이 아니고 인프라로 빼야함 
  • OpenVidu - 이것도 기술 스택 아님 

3) ERD

  • 현재 ERD - ERD라고 보기 어려움 ( 몇대 몇으로 맵핑되는지 보여야함 +  PK/FK 가 보여야함)
  • 개선할 부분
    • GameRoom - Member 사이에 관계를 맺어주기
    • GameRoomMember 도메인을 GameRoomAttendee로 변경
      • Id(pk), RoomId(fk), MemberId(fk) 요소가 만들어진 테이블 만들어주기
      • relation table 역할 : 진짜 필요한 데이터만 갖고 있어야 한다.
      • PK : Id, FK : RoomId, MemberId, nickname
    • 게임룸이랑 멤버요소는 지워줘도 괜찮음
    • 게임룸 안에 chatroom 을 뺴도 괜찮을 듯 (존재 이유가 명확해야 하는데 불명확 하기 때문 )
    • GameRoom <- GameRoomMember -> Member
    • 게임룸이 게임룸 멤버 정보를 들고있으면 안된다 ! 

 

4) 질문에 대한 답변 

 

1. Openvidu와 같은 미디어 서버를 사용해야 할까요?

  • 사용자가 많아지면 SFU를 사용하는게 맞지만 4명~8명처럼 소수라면 P2P 시그널링 서버를 쓰는게 좋다 : 성능적으로 좋다
  • 실시간성이 중요한건지 서버의 부하를 줄이는건지 선택하는게 중요
  • 완벽한 이상적인 해결책은 없다 ! 따라서 서비스에 따라 기술스택을 선택하면 된다 

2. Redis가 메모리 파편화 때문에 생각보다 많은 메모리를 차지하는데, Redis 메모리를 효율적으로 사용하는 방법이 있을까요?

  • Redis는 현업에서 아주 많이 사용하는 메모리 DB, 메모리 파편화라는 것을 신경쓸 필요가 없을 정도로 잘 구성이 되어있음
  • 메모리 파편화 해결 : 효율적인 자료구조로 사용 - 리스트나 맵보다 스트링으로 저장한다던지 하는 방법 

3. 한 방에 정답이 최대 4개인데 키워드 리스트를 Json화 시켜서 문자열로 DB에 저장을 했는데 이거를 다시 검증하려고 하면 배열화 시켜야하는 번거로움이 있는데, 조금 더 효율적인 방법이 있을까요?

  • 효율성을 생각하면 콜룬을 추가해주는게 맞지만 사실 추후 확장성을 고려했을때는 Json 형식이 맞다! 

4. 실무에서 flush / clear를 사용할 일이 별로 없다는데 사용한 예제를 봤다. 이유가 뭘까요?

  • flush / clear 명시적으로 사용할 수 있지만 거의 코드로 사용하지 않음
  • 암시적으로 자동으로 반영이 되기 때문에 굳이 써줄 필요가 없다. 
  • hibernate 에 auto commit 기능이 있기 때문에 알아서 처리가 된다. 

5) 그 외 조언

  • 자료 찾을때 블로그보는게 좋지는 않음 - 최대한 공식 문서 / 영어로 찾아보기
  • 잘짠 코드/ 좋은 코드 : 일관성 있는 코드,  간결함 = 여러명이 찐 코드여도 한사람이 짠 코드처럼 보이는 코드 
  • 프론트엔드 : 리덕스를 사용하기보다 리액트에서 최대한 관리하는게 좋다. 리덕스 사용을 최소화하기 - stateful (x) stateless (o)

 

③ 과제

  • 깃 , 깃허브랑, 깃플로우의 차이점
  • 스프링과 스프링부트의 차이점 ?
  • trunk based development ( TBD 새로 주신 키워드 - 학습해보기 )

한시간 반동안 열심히 피드백을 주신 김선우 개발자님 감사합니다 : )

덕분에 많이 궁금증이 해소되고 배울 수 있었어요. 받은 피드백 잘 기억해서 개선하는 개발자가 되겠습니다.

 

이번 포스트는 Custom한 예외처리 (StatusCode)를 이용하여 JWT토큰에 관련된 예외처리를 작성한 부분을 담으려 한다.


ExceptionTranslationFilter

ExceptionTranslationFilter는 2가지 종류의 예외를 처리    

  • FilterSecurityInterceptor(보안필터중 제일 마지막에 위치)가 발생시킴
  • 여기에서 인증,인가예외를 Throws함

 

    가. AuthenticationException - 인증예외처리

        -  AuthenticationEntryPoint 호출 : 로그인페이지 이동, 401오류 코드 전달

        -  인증예외가 발생하기 전의 요청 정보를 저장

             - RequestCache - 사용자의 이전 요청정보를 세션에 저장하고, 이를 꺼내오는 캐시 메커니즘

             - SavedRequest - 사용자가 요청했던 request파라미터 값들, 그 당시의 헤더값들 등이 저장 

 

    나. AccessDeniedException - 인가예외처리

         - AccessDeniedHandler에서 예외처리하도록 제공

 

 

AuthenticationEntryPoint란 ?

  • 인증 처리 과정에서 예외가 발생한 경우 예외를 핸들링하는 인터페이스
  •  AuthenticationException(인증되지 않은 요청)인 경우 AuthenticationEntryPoint를 사용하여 처리

AuthenticationEntryPoint 인터페이스 구현체 생성

 

SecurityConfig에서 .exceptionHandling() 메소드에 등록

 

전체 코드

1. JwtUtil

// 기능 : JWT 유틸
@Component
@RequiredArgsConstructor
public class JwtUtil {

    private final UserDetailsServiceImpl userDetailsService;
    public static final String AUTHORIZATION_HEADER = "Authorization";
    private static final String BEARER_PREFIX = "Bearer ";
    private static final long TOKEN_TIME = 60 * 60 * 1000L;

    @Value("${jwt.secret.key}")
    private String secretKey;

    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    // 토큰 생성
    public String createToken(String loginId){
        Date date = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(loginId)
//                        .claim(AUTHORIZATION_KEY, role)
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                        .setIssuedAt(date)
                        .signWith(key, signatureAlgorithm)
                        .compact();
    }

    public String resolveToken(HttpServletRequest request){
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);

        if(StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)){
            return bearerToken.substring(7);
        }
        return null;
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }

    public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }

    public Authentication createAuthentication(String email) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(email);
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}

 

2. JwtAuthFillter

// 기능 : JWT 필터
@Slf4j
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
    public final JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String token = jwtUtil.resolveToken(request);

        if(token != null) {
            if(!jwtUtil.validateToken(token)){
                response.setStatus(StatusCode.INVALID_TOKEN.getHttpStatus().value());
                response.setContentType("application/json; charset=UTF-8");
                try {
                    String json = new ObjectMapper().writeValueAsString(new GlobalResponseDto(StatusCode.INVALID_TOKEN));
                    response.getWriter().write(json);
                } catch (Exception e) {
                    log.error(e.getMessage());
                }
                return;
            }
            // 3. 토큰이 유효하다면 토큰에서 정보를 가져와 Authentication 에 세팅
            Claims info = jwtUtil.getUserInfoFromToken(token);
            setAuthentication(info.getSubject());
        }
        // 4. 다음 필터로 넘어간다
        filterChain.doFilter(request, response);
    }

    public void setAuthentication(String loginId) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = jwtUtil.createAuthentication(loginId);
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);
    }
    public void jwtExceptionHandler(HttpServletResponse response, String message, int statusCode) {
        response.setStatus(statusCode);
        response.setContentType("application/json");
        try {
            String json = new ObjectMapper().writeValueAsString(new GlobalResponseDto<>(StatusCode.INVALID_TOKEN));
            response.getWriter().write(json);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }
}

 

3. SecurityConfig

// 기능 : Spring Security 사용에 필요한 설정
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig {
    private final JwtUtil jwtUtil;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf().disable();

        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.httpBasic().disable()
                .authorizeRequests()
                .antMatchers("/auth/**").permitAll()
                .antMatchers(HttpMethod.GET, "/**").permitAll()
                .antMatchers("/ws-stomp").permitAll()
                .antMatchers("/signal/**").permitAll()
//                .antMatchers("/swagger-ui.html", "/swagger-ui/**", "/api-docs", "/api-docs/**").hasRole("USER")
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(new JwtAuthFilter(jwtUtil),
                        UsernamePasswordAuthenticationFilter.class);
        http.cors();

        // Spring Security에서 발생하는 예외를 커스텀 핸들링
        http.exceptionHandling()
                // 인증부분  (여기서만 사용하기 때문에 익명함수 람다식 이용)
                .authenticationEntryPoint((request, response, authException) -> {
                    securityExceptionResponse(response, INVALID_TOKEN);
                });

        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {

        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("http://localhost:3000");
        config.addAllowedOrigin("https://S3 버켓이름.s3-website.ap-northeast-2.amazonaws.com"); 
        config.addAllowedOrigin("https://프론트엔드 서버주소.cloudfront.net"); 
        config.addExposedHeader(JwtUtil.AUTHORIZATION_HEADER);
        config.addAllowedMethod("*");
        config.addAllowedHeader("*");
        config.setAllowCredentials(true);
        config.validateAllowCredentials();
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);

        return source;
    }

    // 예외 응답처리 메소드
    public void securityExceptionResponse(HttpServletResponse response, StatusCode statusCode) throws IOException {
        response.setStatus(statusCode.getHttpStatus().value());
        response.setHeader("content-type", "application/json; charset=UTF-8");
        String json = new ObjectMapper().writeValueAsString(new GlobalResponseDto(statusCode));
        response.getWriter().write(json);
        response.getWriter().flush();
        response.getWriter().close();
    }
}

 


[ 참고 자료 ]

 

 

[SpringSecurity] 스프링시큐리티 - 인증인가 예외발생시 처리 (ExceptionTraslationFilter)

[SpringSecurity] 스프링시큐리티 - 인증인가 예외발생시 처리 (ExceptionTraslationFilter) 1. ExceptionTranslationFilter는 2가지 종류의 예외를 처리 --> FilterSecurityInterceptor(보안필터중 제일 마지막에 위치)가 발생

fenderist.tistory.com

 

AuthenticationEntryPoint

개인 공부 목적으로 작성한 포스팅입니다. 아래 출처를 참고하여 작성하였습니다. 1. AuthenticationEntryPoint란 ? 인증 처리 과정에서 예외가 발생한 경우 예외를 핸들링하는 인터페이스입니다. e.g. Au

batory.tistory.com

 

1. JSONObject()

  • JSONObject()를 호출 한 다음 해시 맵을 전달하는 방법
  • 의존성 추가
 // build.gradle 의존성 추가 부분 JsonObject
    implementation group: 'org.json', name: 'json', version: '20090211'
import org.json.simple.JSONObject;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args){

        ArrayList<String> stringArrayList = new ArrayList<>();
        stringArrayList.add("firstString");
        stringArrayList.add("secondString");
        stringArrayList.add("thirdString");

        Map<String, Object> map = new HashMap();
        map.put("key1", "value1");
        map.put("key2", "value2");
        map.put("stringList", stringArrayList);

        JSONObject json =  new JSONObject(map);
        System.out.printf( "JSON: %s", json);

    }
    
    
    출력 : JSON: {"key1":"value1","key2":"value2","stringList":["firstString","secondString","thirdString"]}

 

2. Jackson 라이브러리

  • Java에는 해시 맵을 유연성이 뛰어난 JSON 객체로 변환하는 데 도움이되는 라이브러리가 있다.
  • Jackson은 Java map을 가져온 다음지도를 JSON 형식으로 변환하는 라이브러리 중 하나
  • ObjectMapper().writeValueAsString(map)이 호환되지 않는 데이터 형식을 발견하면 예외를 던질 수 있으므로JsonProcessingException을 처리하는 것을 잊지 말아야한다.
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) throws JsonProcessingException {

        Map<String, Object> map = new HashMap();
        map.put("key1", "value1");
        map.put("key2", "value2");

        String json = new ObjectMapper().writeValueAsString(map);
        System.out.printf( "JSON: %s", json);

    }
    
    출력 : JSON: {"key1":"value1","key2":"value2"}

 

3. GSON 라이브러리

  • Gson 라이브러리는 해시 맵을 JSON 객체로 변환하는 데 가장 많이 사용되는 라이브러리 중 하나이다.
  • Gson 클래스에는 맵을 JSON 트리로 변환하는toJsonTree 메소드가 있다.
  • JSON 객체가 필요하므로 toJsonObject()를 사용하여 JsomTree를 JSON 객체로 만들 수 있다.
import com.google.gson.Gson;
import com.google.gson.JsonObject;

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args){

        Map<String, Object> map = new HashMap();
        map.put("key1", "value1");
        map.put("key2", "value2");

        Gson gson = new Gson();
        JsonObject json = gson.toJsonTree(map).getAsJsonObject();

        System.out.printf( "JSON: %s", json);

    }
    
    출력 : JSON: {"key1":"value1","key2":"value2"}

 

이번 포스팅을 알아보면서 너무 좋은 사이트를 발견했다 ! -> https://www.delftstack.com/ 

 

Delft Stack

 

www.delftstack.com

유용한 정보가 많아서 앞으로 자주 이용할 것 같다.


[ 참고 자료 ]

 

 

Java에서 Hashmap을 JSON 객체로 변환하는 방법

이 기사에서는 해시 맵을 간단한 JSON 객체로 변환 할 수있는 방법을 소개합니다.

www.delftstack.com

 

 

Java - JSON 라이브러리 사용 방법 (JSONObject, JSONArray)

Java에서 org.json 라이브러리를 이용하여 JSON 데이터를 다룰 수 있습니다. 이 라이브러리의 JSONObject, JSONArray 클래스는 JSON 데이터를 갖고 있고, JSON 형식의 문자열로 출력할 수 있습니다. JSON 라이브

codechacha.com

 

 

※ 이 게시글은 카카오 로그인 코드가 구현되어 있다는 전제하에 PostMan으로 테스트 하는 방법을 담은 글 입니다 :) 

 

1. 사전 준비 ( kakao developers)

 

  • https://developers.kakao.com/ 
  • 위 사이트 접속 - [내 애플리케이션] - [요약정보] 에서 Client ID로 쓰일 REST API키를 복사해 놓기! 

 

  • [내 어플리케이션] - [카카오로그인] - Redirect URI 부분에 포스트맨 주소와 그 외 필요한 주소들을 저장한다.
  • 포스트맨을 사용하기 위해선 https://getpostman.com/oauth2/callback 을 입력해준다.
  • 내 로컬테스트 콜백 주소인 http://localhost:8080/auth/kakao/callback 주소도 오늘 테스트에 사용될 예정이다.

 

2. PostMan에서 test 진행 하는 법

 

  • https://kapi.kakao.com/v2/user/me, POST방식으로 URL 주소를 입력하고
  • Authorization 에서 Type 을 OAuth 2.0로 선택해 준다.

 

  • Token Name : 원하는 이름을 지어준다.
  • Grant Type : Authorization Code
  • Callback URL : https://getpostman.com/oauth2/callback
  • Auth URL : https://kauth.kakao.com/oauth/authorize
  • Access Token URL : https://kauth.kakao.com/oauth/token
  • Client ID : REST API 값 입력
  • 그리고 Get New Access Token 를 클릭한다! 

 

 

  • 그럼 이런 카카오 로그인 창이 뜬다.

 

 

  • 다음 Use Token을 클릭한다.

 

 

  • 그럼 위 사진과 같이 자동으로 code가 생성된다.

 

 

  • http://localhost:8080/auth/kakao/callback, GET방식 URL 을 입력한다.
  • Params 형식으로 Key는 code, VALUE 에 아까 받은 code를 입력한다.
  • 그리고 Send 를 클릭한다.

 

 

  • 로그인 성공 !
  • 반환값으로 Body에 닉네임을 주었기 때문에 내 카카오 닉네임인 Jini가 반환되었다.

 

 

  • 다음으로 Headers부분에서 Authorization 값으로 Bearer 토큰이 반환되었다.

 

3. 카카오 로그인에 있는 닉네임 변경하기

 

  • 닉네임 변경 URL인 http://localhost:8080/auth/changeNickname 을 PUT 방식으로 저장한다.
  • Headers 부분에 위에서 발급받은 Bearer 토큰을 담아준다.

 

  • 닉네임 변경을 위해 body 부분에 JSON 형식으로 바꾸고자하는 닉네임을 입력하고 Send 를 클릭 !

 

 

  • MySQL DB에도 성공적으로 변경된 닉네임을 확인 할 수 있다.

WebSocket

일반적인 클라이언트와 서버의 소통방식은 HTTP/HTTPS 통신을 거친다. 이는 클라이언트가 서버에 요청을 보내면 서버가 요청에대한 응답을 보내주는 구조이다. 실시간으로 채팅을 주고받아야하는 서비스에서 매 요청마다 서버에 요청을 보내고 응답하는 것은 굉장히 비효율 적이다. 그래서 등장한 WebSocket ! 

 

WebSocket의 동작원리를 간단히 설명해 보자면, 클라이언트가 원하는 토픽에 구독을 신청하고, 특정 사용자가 토픽에 대한 메세지를 발행하면 해당 토픽을 구독하고 있는 모든 사용자에게 메세지를 전달하는 방식이다. 여기서 토픽이란 HTTP 통신은 *URI 라고 생각하면 된다.웹소켓을 이용한 통신은 한번 연결을 하면 그 연결이 유지되기 때문에 서버에 계속 요청을 할 필요가 없다.

 

* URI vs URL 차이점 열어보기↓

더보기
https://www.elancer.co.kr/
URI URL
Uniform Resource Identifier의 약자
우리말로 ‘
통합 자원 식별자’ 이다.
URI= 식별자
URL은 Uniform Resource Locator
네트워크상에서 통합 자원(리소스)의 “위치”를 나타내기 위한 규약 
 URL=식별자+위치
1. Uniform은 리소스를 식별하는 통일된 방식
2. Resource란, URI로 식별이 가능한 모든 종류의 자원
(웹 브라우저 파일 및 그 이외의 리소스 포함)을 지칭한다.

3. Identifier는 다른 항목과 구분하기 위해 필요한 정보
특정 웹 페이지의 주소에 접속하기 위해서는 웹 사이트의 주소뿐만 아니라 프로토콜(https, http, sftp, smp 등)을 함께 알아야 접속이 가능한데, 이들을 모두 나타내는 것이 URL
차이점 정리 -----> 1. URL은 일종의 URI이다.
2. URL은 프로토콜과 결합한 형태이다.
3. URI는 그 자체로 이름이 될 수 있다.
elancer.co.kr https://elancer.co.kr

 

 

 

1. WebSocket 이란?

  • 기존의 단방향 HTTP 프로토콜과 호환되어 양방향 통신을 제공하기 위해 개발된 프로토콜
  • 접속까지는 HTTP 프로토콜을 이용하고, 그 이후 통신은 자체적인 WebSocket 프로토콜로 통신
  • HTTP를 이용한 정보 송수신은 클라이언트의 요청이 없다면, 서버로부터 응답을 받을 수 없는 구조 ( = 반이중 통신(Half Duplex))이다
  • 하지만 웹소켓에서는 서버와 브라우저 사이에 양방향 소통이 가능하다(= 전 이중 통신,양방향 통신 (Full-Duplex)). 즉, 클라이언트가 먼저 요청하지 않아도 서버가 먼저 데이터를 보낼 수도 있고, 사용자가 다른 웹사이트로 이동하지 않아도 최신 데이터가 적용된 웹을 볼 수 있게 해준다.

 

2. WebSocket 동작 원리

  • 서버와 클라이언트간의 웹소켓 연결은 HTTP프로토콜을 통해 이루어진다. 연결이 정상적으로 이루어진다면 서버와 클라이언트 간에 웹소켓 연결(TCP/IP기반)이 이루어지고 일정 시간이 지나면 HTTP연결은 자동으로 끊어진다.
  • 웹소켓 주소는 ws나 wss로 시작하는데 ws는 일반 웹소켓이고 wss는 SSL이적용된 웹소켓이다(Https)

https://colinch4.github.io/2021-06-28/%EC%86%8C%EC%BC%93%EC%97%B0%EA%B2%B0%EC%9D%B4-%EC%9D%B4%EB%A4%84%EC%A7%80%EB%8A%94-%EB%B0%A9%EC%8B%9D/

 

  • 서버와 클라이언트간의 웹 소켓 연결은 HTTP 프로토콜을 통해 이루어진다. HANDSHAKE과정이 성공적으로 끝나면 HTTP를 웹소켓 프로토콜로 바꾸는 protocol switching 과정이 진행된다. 이후 웹소켓을 위한 새로운 소켓이 만들어지고 이 소켓을 이용해 통신한다.

 

 

좀 더 구체적으로 들여다 보자면, 웹 소켓 동작 과정은 크게 세가지로 나눌 수 있다.

  • 위 이미지의 빨간 색 박스에 해당하는 Opening Handshake
  • 위 이미지의 노란 색 박스에 해당하는 Data transfer
  • 위 이미지의 보라 색 박스에 해당하는 Closing Handshake

 Handshake 

Opening Handshake 와 Closing Handshake 는 일반적인 HTTP TCP 통신의 과정 중 하나이다.

접속 요청은 HTTP 로 한 뒤, 웹소켓 프로토콜로 변경된다. (WS)

웹소켓 프로토콜로 변경되기 위한 HTTP 헤더는 아래처럼 구성되어 있다.
(ws://localhost:8080/chat으로 접속하려고 한다고 가정한다.)

( Handshake 참고자료 - https://doozi0316.tistory.com/entry/HTTPHTTPS%EB%9E%80-TCP-UDP-HandShake-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC )

Data Transfer

Opening HandShake에서 승인이 나고나면,
웹 소켓 프로토콜로 노란색 박스 부분인 Data transfer 이 진행된다.
여기서 데이터는 메시지라는 단위로 전달된다.

 


WebRTC

이번 프로젝트에서 문자 채팅은 WebSocket과 Stomp를 사용하여 구현을 했다. 그리고 화상채팅의 경우 WebRTC와 시그널링 서버를 이요해 구현을 하였다. 간단히 Wev RTC에 대해 정리해 보려 한다.

 

1. WebRTC 란?

  • WebRTC(Web Real-Time Communications)란, 웹 어플리케이션(최근에는 Android 및 IOS도 지원) 및 사이트들이 별도의 소프트웨어 없이 음성, 영상 미디어 혹은 텍스트, 파일 같은 데이터를 브라우져끼리 주고 받을 수 있게 만든 기술 
  • WebRTC로 구성된 프로그램들은 별도의 플러그인이나 소프트웨어 없이 실시간으로 P2P 화상회의 및 데이터 공유를 한다.
  • 즉, 웹 브라우저 상에서 어떠한 플러그인도 필요 없이 음성 채팅과 화상채팅, 데이터 교환까지도 가능하게 하는 기술 !

 

2. WebRTC 동작 원리

 

1. Signaling을 통해 통신할 peer간 정보를 교환한다. ex) 세션 제어 메세지, 네트워크 구성정보, 미디어 기능 등의 정보
2. WebRTC를 사용해 연결을 맺고, peer의 단말에서 미디어를 가져와 교환한다.

 

시그널링

  • WebRTC 는 P2P 연결을 통해 직접 통신하지만, 이를 중계해주는 과정이 필요하다. 이를 시그널링 이라 부른다. 그리고 이를 수행하는 서버를 시그널 서버라 칭한다.
  • 시그널 서버는 채팅방과 같은 형태로 연결하고자 하는 Peer 들을 논리적으로 묶고 서로간에 SDP 와 Candidate 를 교환하여 준다.
  • 시그널링의 핵심은 비동기적으로 발생하는 Peer 들의 정보(SDP, Candidate)를 교환하는 일이다. 그러므로 전이중 통신을 지원하는 websocket 으로 이를 구현하는 것이 가장 적합하다.

 

시그널링의 역할

다음과 같은 세가지의 정보를 교환하게 한다.

  • Session control messages : 통신의 초기화, 종료, 그리고 에러 메시지
  • Network configuration : 외부에서 바라보는 IP와 포트 정보
  • Media capabilities : 상호 두 단말의 브라우저에서 사용 가능한 코덱, 해상도


서버의 역할 

P2P로 클라이언트끼리 통신을 한다해도 서버가 필요한 이유는 다음과 같다.

  • Signaling 사용자 탐색과 통신
  • 방화벽과 NAT 트래버셜
  • P2P 통신 중계서버

 

용어 총정리

STUN


NAT 트래버셜 작업은 STUN(Session Traversal Utilities for NAT) 서버에 의해 이루어진다. STUN 방식은 단말이 자신의 "공인 IP 주소와 포트"를 확인하는 과정에 대한 프로토콜이다. 클라이언트가 STUN 서버에 요청을 보내면 공인 IP주소 와 함께 통신에 필요한 정보들을 보내주는데, 클라이언트는 이를 이용해 다른 기기와 통신한다. 하지만 이러한 경우에도 통신이 되지 않는다면 TURN 서버로 넘기게 된다.

 

TURN


STUN 서버를 이용하더라도 항상 자신의 정보를 알아낼 수 있는 것은 아니다. 어떤 라우터들은 방화벽 정책을 달리 할 수도 있고, 이전에 연결된 적이 있는 네트워크만 연결할 수 있게 제한을 걸기도 한다(Symmetric NAT). 이 때문에 STUN 서버를 통해 자기 자신의 주소를 찾아내지 못했을 경우, TURN(Traversal Using Relay NAT) 서버를 대안으로 이용하게 된다.

 

 

ICE와 Candidate

STUN, TURN 서버를 이용해서 획득했던 IP 주소와 프로토콜, 포트의 조합으로 구성된 연결 가능한 네트워크 주소들을 후보(Candidate)라고 부른다. PeerConnection 객체를 생성하면 candidate 를 얻을 수 있다.

한편, 이 모든 과정은 ICE(Interactive Connectivity Establishment)라는 프레임워크 위에서 이루어진다. ICE는 두 개의 단말이 P2P 연결을 가능하게 하도록 최적의 경로를 찾아주는 프레임워크이다. ICE 는 STUN 과 TURN 을 활용하여 여러 Candidate 를 검출하고 이들 중 하나를 선택하여 Peer 간 연결을 수행한다.

 

SDP

SDP(Session Description Protocol)는 WebRTC에서 스트리밍 미디어의 해상도나 형식, 코덱 등의 멀티미디어 컨텐츠의 초기 인수를 설명하기 위해 채택한 프로토콜이다. 미디어에 대한 메타 데이터로 사용할 수 있는 코덱은 무엇들이 있으며, 어떤 프로토콜을 사용하고, 비트레이트는 얼마이며, 밴드위드스는 얼마이다 와 같은 데이터가 텍스트 형태로 명시되어 있다.

PeerConnection 객체를 생성하게 되면 PeerConnection 객체에서 offer SDP, answer sdp 를 얻을 있다. 이처럼 미디어와 관련된 초기 세팅 정보를 기술하는 SDP는 발행 구독 모델(Pub/Sub)과 유사한 제안 응답 모델(Offer/Answer)을 갖고 있다.

 

3. WebRTC 방식 ( Mesh, SFU, MCU )

1. P2P(Mesh) 

 

  • 클라이언트 즉, Peer 간의 연결을 진행하기 때문에 서버는 단순 연결을 위한 정보를 중계할 때만 사용된다.
  • 위의 그림에서처럼 클라이언트마다 자신의 미디어 정보를 송신할 링크(Uplink) 1개와 미디어 정보를 수신할 링크(Downlink) 1개를 가지게 된다.
  • 만약, 위의 그림처럼 5명의 클라이언트가 각각에 대해 Peer Connection을 맺는다면 각 클라이언트는 나머지 4명의 클라이언트에게 자신의 미디어 정보를 송신할 링크(Uplink) 4개와 4명의 클라이언트로부터 미디어 정보를 수신할 링크(Downlink) 4개를 가져 총 8개의 링크를 가지게 된다. 

 

특징

  • 클라이언트끼리 직접 연결을 진행하고 서버는 단순 정보를 중계하기에 서버 부하가 적다.
  • 직접 연결로 데이터를 송수신하기 때문에 실시간 송수신이 보장된다.
  • 단순 1:1을 넘어 N:M 연결로 확장시 클라이언트가 유지해야하는 링크가 증가함에 따라 클라이언트의 부하가 심해진다.

 

2. SFU(Selective Forwarding Unit)

 

 

 

  • P2P와 달리 서버에서 미디어 트래픽을 중계해준다.
  • 이전에는 클라이언트(Peer) - 클라이언트(Peer) 간의 연결을 진행했다면 이번에는 서버(Peer) - 클라이언트(Peer) 간의 연결을 진행하게 된다.
  • N:M 상황에서 여러 명과의 링크를 유지해야하는 상황에서 서버와의 링크를 유지하면서 서버와 미디어 정보를 송수신하면 된다.
  • 위의 그림에서처럼 자신의 미디어 정보를 송신할 링크(Uplink) 1개와 미디어 정보를 수신할 링크(Downlink) N개를 가지게 된다.
  • 이전 Mesh 구조에 비해 Connection을 덜 유지하게 된다.

특징

  • 서버가 미디어 트래픽을 중계해주기 때문에 모든 Connection에 대해 클라이언트가 직접 관리할 필요가 줄어들기 때문에 클라이언트의 부하가 줄어들지만 서버의 부하가 증가한다.
  • P2P보다는 실시간성이 떨어질순 있지만 P2P에 걸맞는 실시간 송수신이 보장된다.

 

3. MCU(Multi-point Control Unit)

 

  • SFU와 비슷하게 서버가 중간에서 미디어 트래픽을 중계해준다.
  • SFU는 각각의 클라이언트에서 오는 미디어 정보들을 그대로 포워딩해준 반면 MCU에서는 각각의 클라이언트에서 오는 미디어 정보들을 혼합하고 가공해서 수신측으로 전송해준다.
  • 미디어 정보를 송신할 링크(Uplink) 1개와 미디어 정보를 수신할 링크(Downlink) 1개 총 2개의 링크만을 유지하면 되기에 네트워크에 최적화된 방식이라고 볼 수 있다.
  • 하지만 위에서 언급했듯이, 연결되어있는 모든 클라이언트들의 미디어 정보들을 수신하고 이를 혼합 및 가공해야하기 때문에 서버의 부하가 기하급수적으로 높아진다.

특징

  • 클라이언트는 단순 링크 2개(Uplink, Downlink 각각 1개)만을 유지하면 되기에 클라이언트의 부하가 상당히 줄어든다.
  • N:M 연결에서 효율적인 모습을 보여줄 수 있다.
  • 모든 클라이언트들의 정보를 수집하고 한번에 혼합 및 가공을 진행하기에 실시간 송수신 보장이 어려울 수 있다.
  • 서버가 모든 미디어 정보를 중계하고 이를 혼합 및 가공하는 과정까지 거쳐야하기 때문에 서버의 부하가 상당히 심해진다.

 


[ 참고 자료 ]

 

WebSocket 과 WebRTC 이해해 도움을 주신 아래의 모든 블로그 주인분들께 감사의 인사를 드립니다 !! 

 

WebSocket 

 

WebRTC

+ Recent posts