D2 CAMPUS FEST mini
개요
개발을 시작하기 앞서, 이번 프로젝트는 단순히 기능구현에만 초점을 맞추는데 그치지 않고, 보다 소프트웨어 엔지니어 측면에서 유지보수가 쉽고, 가독성이 좋은 코드를 작성하고자 팀원들과 많은 애기들을 나누었다. 때문에 과제를 받으면 바로 코드부터 작성하던 이전 프로젝트와는 달리, 이번 프로젝트는 설계부터 시작하여 전체적인 시스템 아키텍처에 대한 고민들 초점을 맞추고 진행했다. 이러한 고민들의 흔적과 노력들을 이번 보고서에 담고자 한다.
기술스펙
Back-End : Spring-Boot, Spring-Security, JWT, JPA, MySQL
Front-End : React, Css
Cloud : AWS - RDS, S3
Document : Swagger UI
Swaager UI를 통해 Rest API 문서작성을 자동화하여 Front와 협업을 보다 용이하게 진행하였다.
가독성과 생산성이 뛰어난 코드
세명의 개발자가 협업하고, 앞으로의 코드 일관성과 유지보수를 보다 쉽게 하고자 본격적인 코드 작성에 들어가기 앞서, 몇가지 규칙을 정했다.
- 메소드는 하나의 책임만 가지며, 복잡한 로직은 가능한 숨겨 신문 읽듯이 코드를 읽을 수 있어야 한다.
- 네이밍은 카멜네이밍을 적용하고, 변수나 메소드 이름은 명시적이고 의미가 담겨 있어야 한다.
-
객체의 setter는 남용하지 않고, 꼭 필요한 부분에만 설정한다.
- 문자열은 최대한 사용을 자제하고, 사용이 필요한 경우 Constant 변수로 관리한다.
DB 스키마 설계
타임라인을 빠르게 가져오기 위한 고민
단순히 타임라인 기능만을 구현하는 것은 그렇게 어렵지 않다. 가장 먼저 떠오른 방법은 관계테이블에서 친구를 찾고, 친구들이 쓴 글을 시간순으로 정렬해서 넘겨주는 것이다. 그러나 이 과정에서 관계테이블을 한번 조회해서 친구들을 찾고, 친구들의 기본키로 다시 게시물 테이블을 조회해야 한다. 최악에 상황에 시간복잡도는 관계테이블의 크기 N * 게시물 테이블의 크기 M 를 가지게 된다. 이를 개선하기 여러 고민을 했다. 아래는 실제로 고민한 방법들의 장단점을 비교한 내용이다.
- 그래프 DB
- 장점 : 아래 방법 중 타임라인에 대해최고의 성능을 가짐, 카카오스토리에서 사용중
- 단점 : ORM으로 코드를 짰기때문에 싹 갈아 엎어야함, 참고자료 부족 및 높은 러닝커브 예상
- MySQL 조인 쿼리 튜닝 + Redis 활용
- 장점 : 코드변경이 거의 없음, 참고할 만한 자료가 존재
- 단점 : Redis 나 타 캐시 저장소 사용경험이 없음
- 각 사용자마다 타임라인 테이블 구축 ( 타임라인 테이블은 배치 app이 만듬)
- 장점 : 메인서버 부하가 없음
- 단점 : 배치 App을 따로 만들어야함, 데이터베이스마다 가질 수 있는 테이블의 갯수가 제한, 테이블 생성에 대한 비용이 크다
위 방법 중 우리는 2번이 현재 상황과 가장 합리적이다 판단하여 2번으로 진행하기로 하였다. 세부로직은 아래와 같다.
- Timeline의 user_FK, board_FK를 인덱스로 만들어, O(1)만의 조회가 가능하도록 MySQL을 튜닝한다.
- 유저가 글을 작성하면, 해당 유저를 팔로워하고 있는 유저들에게 내가 쓴 글에 배달하는 방식으로, 글 작성시에 게시물 기본키와 유저들의 기본키를 가지는 Row를 추가한다.
- 유저 접속시, MySQL에서 타임라인 테이블에 user_FK가 일치하는 게시물들을 Redis에 저장한다.
- 이후 타임라인 정보 요청 시 Redis에서 캐시된 데이터들을 가지고 온다. 캐시된 데이터가 없다면 MySQL에서 요청한다.
위 로직에서의 단점은 쓰기시 발생하는 부하가 크고 추가적인 저장소가 발생한다. 대신 읽기시에 발생하는 부하가 적다. SNS서비스의 읽기와 쓰기 비중을 볼 때 읽기의 비중이 압도적으로 크기에 쓰기에 부하를 늘임으로 읽기의 부하를 줄이는 것은 합당해 보인다. 문제는 추가적인 저장 공간 발생인데, 이는 Cold Data를 위한 Cold storage를 이용함으로서 공간에 드는 비용을 최소화하고 시간적인 이점을 가져 갈 수 있을 것으로 보인다.
위 로직 중 Redis 캐시 구현중, 사용자가 글을 수정했을 때에 대한 캐시갱신 등에 이슈가 해결되지 않아 우선적으로 기술도입을 보류했다.
프로젝트 구조도
기술적인 부분과 시간적인 측면에서 한계와 제출방법에 대한 고민에 있어 분산처리에 대한 부분은 이번 프로젝트에서 다루지 못했다.
프로젝트 패키지 구조
api - ` Client의 요청을 받고 서비스를 호출, 이후 결과를 반환`
common - 프로젝트 내의 공통으로 쓰이는 유틸 관리
config - Spring Security, Web Confg 등 Configuration 파일 관리
constant - 문자열 상수 관리
dao - DB에 직접적으로 접근 하는 객체 관리
dto - front와 통신하기 위한 데이터 객체
exception - 예외 핸들러, 비즈니스 예외, 오류코드 관리
service - 프로젝트 내의 핵심로직 구현
vo - ` Entity 객체 관리`
RESTFUL한 서비스 개발을 위한 노력
Restful 한 서비스를 구현하기 위해서는 자원 요청을 위한 url이 명시적이고 의미가 담겨 있어야한다. 또한 요청에 대한 응답속에 내용뿐 아니라 Self-descriptiveness 해야한다.
-
명시적인 URL 설계
-
Self-descriptiveness 한 응답
/api/boards/timeline url에 요청한 후 받은 응답내용으로, 페이지에 대한 정보와 링크를 포함하고 있다.
시스템 안전성을 위한 노력
시스템이 안정적으로 동작하기 위해서는 올바른 Error 메시지를 출력해주고, 올바르지 않은 비즈니스 상황에 대해 예외처리를 제대로 뿜어 낼 수 있어야 한다. 때문에 우리는 예외를 직접 Handler로 처리하고, 올바르지 않은 비즈니스 상황에 대한 예외를 새로 만들어서 처리했다.
- 예외를 처리하는 예외 핸들러, ErrorExceptionHandler.java -
- 에러메시지를 관리할 ErrorCode.java -
- 실제로 비즈니스 로직을 처리한 예, MeberService.java -
시스템의 안정성을 높이는 또 하나의 예는 테스트 코드이다. 이번 프로젝트를 진행하면서 팀원 모두 처음으로 테스트 코드를 작성해보게 되었고, 초기에는 테스트 코드 작성에서 오는 번거로움 때문에 불평이 많았으나 익숙해지면서 테스트 코드 작성이 주는 이점을 조금씩 깨닫게 되었다. 단 테스트 코드를 작성하기 어려운 부분 주어진 시간 내에서 우선순위가 밀리게 되어 모든 부분을 커버하지 못했다. 아래는 테스트 커버리지다