카테고리 없음

[Numble] 챌린지 회고록

Andrew-Yun 2022. 5. 16. 16:59

프로젝트 소개

동물에 관한 짧은 영상을 올리는 숏폼을 주제로 자신의 반려동물을 자랑하고 싶은 사람 혹은 반려동물을 키우고 싶지만 여건이 되지 않는 사람들을 위해 영상을 직접 올리거나 모아둘 수 있는 서비스를 기획했습니다.

 

역할 담당

CI & CD, 인프라 구축 및 동영상, 댓글, 댓글 좋아요 API를 담당했습니다.

Gtihub: https://github.com/Numble3/Paws-Backend

 

활용한 라이브러리와 그 이유

ffmpeg라는 오픈소스 이미지, 동영상 압축 라이브러리를 사용했습니다.

https://www.ffmpeg.org/

 

FFmpeg

Converting video and audio has never been so easy. $ ffmpeg -i input.mp4 output.avi     News January 17th, 2022, FFmpeg 5.0 "Lorentz" FFmpeg 5.0 "Lorentz", a new major release, is now available! For this long-overdue release, a major effort underwent to

www.ffmpeg.org

HLS (HTTP Live Streaming) 기술을 사용하기 위해선 영상 파일을 분할해야 하기 때문입니다.

HLS를 선택한 이유는 여기를 참고해주세요

주요 로직과 그 이유

패키지 구조

백엔드에서는 패키지 구조를 도메인 단위로 나누었습니다. 도메인이 갖는 역할과 책임을 명확히 나누어야 변경 시 어느 파일을 건드려야할 지, 작성하는 코드는 어느 도메인으로 들어가야 할 지 알 수 있기 때문입니다.

 

각 도메인별로 패키지 구성은 다음과 같은 역할을 갖고 있습니다.

annotation: 도메인에서 사용하는 어노테이션

application: 도메인의 비즈니스 로직

controller: 도메인에 요청하기 위한 컨트롤러

domain: 도메인 엔티티 및 종속된 파생 클래스

infra: 특정 기술에 종속된 구현체

resolver: 스프링의 Argument Resolver를 관리

scheduler: 도메인에 종속된 스케줄러들의 모음

조회 속도

조회수, 좋아요 같은 기능은 짧은 시간에 트랜잭션이 많이 들어오기 때문에, 규모가 커질수록 가장 많은 영향을 받는 기능이라 판단했습니다. 논의 결과, 정합성보다는 조회 속도에 초점을 맞추어 Redis와 같은 인메모리 저장소에 요청들을 저장해두고, 스케줄러로 특정 시각마다 DB에 반영하도록 했습니다.

마찬가지로 동영상, 댓글 도메인 엔티티에서도 조회 속도를 높이기 위해 좋아요를 누른 여부를 관리하는 테이블에 카운트 쿼리를 날려 확인하지 않고, 엔티티 내부에 좋아요 수를 관리하는 프로퍼티를 두어 추가, 삭제마다 +-1을 수행하도록 했습니다.

단일 책임 원칙 지키기

검색 조건 필터링은 변경이 자주 발생할 수 있는 기능이라 판단했고, 컨트롤러마다 코드를 작성하는 것은 유지보수가 힘들다 판단했습니다. 이를 위해 스프링이 제공하는 기능 중 Argument Resolver로 요청이 들어오면 해당 요청에서 Query Parameter를 분석하여 값을 넣어주는 클래스를 만들어 단일 책임 원칙을 지키고자 했습니다.

@Override
  public Object VideoSearchResolveArgument(
      MethodParameter parameter,
      ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest,
      WebDataBinderFactory binderFactory) {
    final HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();

    List<NameValuePair> queryString =
        URLEncodedUtils.parse(request.getQueryString(), StandardCharsets.UTF_8);
    if (queryString == null) {
      return null;
    }
    String title = null;
    VideoCategory category = null;
    VideoSortCondition sortCondition = null;

    for (NameValuePair query : queryString) {
      if (query.getName().equals("title")) {
        title = query.getValue();
      } else if (query.getName().equals("category")) {
        category = VideoCategory.from(query.getValue());
      } else if (query.getName().equals("sort")) {
        sortCondition = VideoSortCondition.from(query.getValue());
      }
    }
    return new SearchCondition(title, category, sortCondition);
  }

의존성 분리

특정 기술에 의존하는 구현체는 비즈니스 로직과 섞여 코드의 복잡도를 높이고 테스트를 힘들게 한다 판단했습니다.

이를 위해 인터페이스로 행위와 구현을 분리하여, mocking을 수행하도록 했고, 비즈니스 로직에 기술적 침투가 일어나지 않도록 의도했습니다.

동영상 변환을 위한 convertUtils 인터페이스를 사용하는 서비스 레이어 코드

코드 내에서 고려한 특정 유저 행동과 그에 대한 대처

프로젝트를 진행하면서 고려한 특정 유저 행동은 다음과 같습니다.

JWT 토큰을 탈취당한 유저

세션 방식의 문제점은 부하가 몰리는 상황에서 Scale out에 유연하게 대처하기 힘들기 때문에 로그인 방식으로 JWT를 이용한 토큰 방식을 선택했습니다. 그런데, 토큰 방식의 문제점인 토큰을 탈취당하면 서버쪽에서 건드릴 수 없기 때문에 이를 위해 리프레쉬 토큰 전략을 선택했습니다.

먼저, 로그인 성공 시 리프레쉬 토큰을 발급받습니다. 해당 토큰으로 인가를 수행하는 액세스 토큰을 발급받아 처리하는데, 이 때 액세스 토큰의 만료 시간을 30분정도로 짧게 하여 탈취당해도 최대한 피해가 없게끔 의도했습니다.

리프레쉬 토큰을 분리한 이유는 해당 토큰을 발급받는 과정에서 서버가 보안 처리를 수행할 수 있기 때문입니다. 자세한 이유는 여기를 참고해주세요

 

프로젝트를 진행할 때 어려웠던 점 / 고민했던 부분과 해결방법

CORS 문제

서버의 API엔 문제가 없는데, 프론트에서 요청이 실패하는 문제가 있었습니다. 서버에선 요청을 받은 로그가 없었고, 프론트는 정상적인 요청을 보냄을 확인한 상황이었습니다. 구글링해보니, 도메인이 다른 출처의 리소스를 허용하지 않는 정책에 걸려 실패한 상황이었고, 이를 제대로 알지 못해 삽질을 했습니다.

해결 방법은 간단했습니다. 서버쪽에서 리소스 출처를 허용하도록 Access-Control-Allow-Origin 헤더에 값을 추가하였습니다.

HTTPS와 구글 크롬의 브라우저 정책 문제

최근 크롬의 보안 정책 변경으로 인해 HTTPS가 아닌 곳에서 받은 secure 옵션의 쿠키를 브라우저 내에 저장할 수 없었습니다. 당시 프로젝트 기간이 얼마 남지 않아 HTTPS로 변경하는데에 시간이 부족했고 리프레쉬 토큰을 쿠키에 저장했던 기존의 방식을 리프레쉬, 액세스 토큰을 모두 응답 결과로 리턴받도록 수정하였습니다.