Project : RainMind 개발일지 - 2 [주요 설계와 선택 이유]
하다 보니 재밌다. 일단 기초적인 api들은 모두 완료했고, 테스트 코드 작성까지 완료하였다. 코드 작성하면서 들었던 의문이나 설계상 신경썼던 포인트들을 정리하고자 한다.
1) api
회원가입, 로그인, 로그아웃(+ redis 토큰 블랙리스트 등록), 초단기실황조회, 단기예보조회의 데이터를 DB에 저장, DB 저장된 데이터를 사용자가 시간에 맞게 조회할 수 있는 기능
2) 로그인/로그아웃 인증방식에 JWT(JSON Web Token) 인증 사용
이유 : HTTP stateless 특성과 맞물려, 우리 설계는 DB에 인증 정보를 저장하지 않으므로 클라이언트가 토큰을 통해 인증 정보를 제공하는 방식인 JWT가 적절.
- 추가 -> JWT 구조: header(해시 암호화 알고리즘 종류 + 토큰 유형) + payload(정보의 조각(= 클레임)들 모음) + signature(header, payload, 서버 자체 비밀 키를 합쳐서 인코딩한 암호화된 문자열)
3) URI vs URL ?
jwt 인증 필터 쪽 로직을 고민해보다가 uri url이라는 용어가 나와서 찾아보았다. URL은 uniform resource locator로, 네트워크 상 자원이 어디에 있는지 위치를 알려준다. URI는 uniform resource identifier로, 네트워크 상 자원을 식별하는 문자열이다. 즉 집중하는 포인트가 각각 위치/식별로 관점이 다르다. URL은 URI의 일종이라고 할 수 있다.
4) Spring 필터 종류
HTTP request message가 수신되면, 서버 쪽에서는 Tomcat이 HTTPServletRequest와 HTTPServletResponse 객체를 생성한다. 이때 HTTPServletRequest가 우리 서버 쪽으로 수신되는 HTTP 요청 내용을 담고 있다고 생각하면 되고, 이 요청은 바로 controller 쪽으로 들어오지 않고 여러 내부적인 filter chain을 거친다.
이 filter 종류에는 Servlet filter / Spring security filter가 있다. 어노테이션 @Component를 붙여 Bean으로 등록하는 filter가 Servlet filter이며, 커스텀 spring security filter를 만들고 싶으면 config 파일을 따로 만들어 filter를 만들어야 한다. 그래서 우리 코드의 SecurityConfig를 하나 만들어 우리가 만든 JWT 기반 커스텀 필터를 filter chain에 등록하는 것이다.
5) 세션 기반 인증 vs JWT 기반 인증
세션 기반 방법은, 클라이언트가 회원가입 등으로 인증 정보를 서버에 전송하면 서버가 세션을 생성 후 세션 id를 클라이언트에 전달하여 클라이언트는 그것을 통해 앞으로 요청하는 것이다. 서버가 이 세션 정보를 저장하고 있기 때문에, 보안은 좋지만 사용자가 많아지면 DB에 부담이 심해진다.
반면 JWT 기반 인증은, 토큰 자체에 인증 정보를 담아 클라이언트가 이 토큰을 이용해 요청해야하며, 서버는 이 토큰을 만들때 썼던 내부 비밀키를 이용해 토큰의 유효성을 검증한다. 서버에 부하가 심하진 않지만, 토큰이 탈취될 위험이 있고 서버가 임의로 토큰 무효화는 불가능하다는 점이 있다.
우리 구현에선, 사용자가 많아진다면 DB에 부하가 심해지기 때문에 JWT 기반 인증 방식을 선택하였다.
6) RestClient를 따로 둔 이유?
RestClient는 HTTP 외부 호출을 담당하는 코드들을 한곳에 모아놓고 쓸 수 있게 하는 인터페이스라고 생각하면 된다. 즉 실제 외부 사이트에 api를 전송하고 받아볼 수 있다. 그러나 이렇게 코드로 따로 뺀 이유는, 어쨌든 service 로직 쪽에서 데이터를 받고 가공해야 하는데 service 코드의 함수 개수가 많아지면 그때마다 처리하는 로직을 매 함수마다 따로 붙여야 하기 때문에 코드 길이가 길어지고 가독성도 떨어진다. 따라서 외부 api를 호출하는 코드를 따로 빼놓고, 서비스 쪽에서는 호출 조건만 그때그때 다르게 가공해서 공통된 코드를 호출하게 하는 방식을 채택했다.
7) Controller에서 반환 시, DTO 반환 vs ResponseEntity 반환?
DTO로 반환할 경우에는, Spring이 JSON으로 자동 직렬화 해주지만 response 상태는 200 OK가 디폴트이다(@ResponseStatus 사용하면 반환되는 상태 코드를 바꿀 수 있긴 하다). ResponseEntity로 만들 경우에는, response 상태 코드, 헤더, 바디를 커스텀해서 보낼 수 있다.
- JSON -> DTO 역직렬화 담당? Jackson Object Mapper가 이름이 같은건 넣고 다르면 넣지 않는 구조로 역직렬화 해준다. 따라서 우리 코드(KmaExternalFetchClient.kt)에서도 외부 api의 응답 구조에 맞게 DTO만 잘 설계해주면 예외 날 일 없이 데이터가 잘 들어온다.
8) 단기예보조회 호출 시 주의사항
단기예보조회 호출 후 그 정보들을 DB에 저장할 때, DB 정보를 갱신하기 위해 DB 내용을 전부 지웠다가 새로 정보를 채워넣는다. 즉 delete와 insert 연산이 모두 수행되기 때문에 반드시 service 계층에 Transactional 키워드를 붙여줘야 한다. 삭제는 되었는데 삽입이 안되면 롤백해야 하기 때문이다.
9) @Query 시 주의사항 repository 레벨에서 @Query를 사용할 경우, 기본적으로 executeQuery()로 동작한다. 그러므로 @Query 안에서 DELETE를 그대로 사용할 경우 에러가 호출된다. @Modifying을 사용하거나 deleteAllBy… 처럼 기본적으로 제공되는 메서드를 사용해야 한다.
10) 가장 중요한 질문… 왜 spring boot + kotlin 조합을 선택했는가?
spring boot의 경우, Tomcat을 자동으로 띄워주는 등 서버 개발자가 로직 구현에만 집중할 수 있는 환경이며, 개인 프로젝트라도 도커 등을 통해 운영 가능한 서버 환경을 빠르게 테스트해볼 수 있다.
kotlin의 경우, 코드량을 줄이면서도 null-safety 안정성을 높이며 복잡한 비즈니스 로직의 특성상 가독성과 유지보수가 좋아야 하므로, 지금과 같은 소규모 프로젝트에서 자바 대신 코틀린을 선택하였다.
11) 또 가장 중요한 질문… 왜 동작 테스트시 docker + mockmvc 조합을 선택했는가?
Docker의 경우, 개발자마다 운영환경(운영체제 등)/개발 및 테스트 환경이 다른 문제를 해결할 수 있다. 도커라는 독립된 컨테이너를 띄우기 때문이다. 따라서 환경 관계없이 내가 개발했던 그 환경을 다른 곳에서도 똑같이 작동시킬 수 있어 이식성이 좋아진다.
또한 테스트 코드 작성시 MockMvc를 사용하였는데, 이는 실제 HTTP 요청흐름을 재현 가능하게 하며, 서버를 실제로 띄우지 않아도 미리 테스트 코드에서 실제 동작을 테스트할 수 있게 하는 장점이 있다. 즉 서비스의 모든 시나리오들을 실제 데이터가 흘러가는(파이프라인) 것을 검증할 수 있다.
지금까지는 api들 구현 + 테스트 코드 작성을 통해 실제 내가 만든 서버의 동작의 정확성을 체크했다. 이제는 redis 알림 큐 + kubernetes 배포를 통해 업그레이드 해볼 생각이다.