들어가면서
4차 스프린트는 하루스터디 서비스 사용자들이 그동안 진행했던 스터디 기록을 모아볼 수 있는 기능을 제공하기 위한 로그인 기능을 구현하는데 집중했던 기간이었다. 과거에 진행했던 스터디 기록을 다시 조회할 수 없다는 것이 서비스 사용자들에게 가장 큰 불편함으로 다가올 것이라고 판단해서 최우선적으로 구현을 하기로 결정했다. 이와 별개로 운영중인 서비스에서 발생하는 로그들을 전혀 확인할 수 없는 문제가 있었는데 이를 해결하기 위해 로깅 시스템을 구축했어야 했다. 무엇 하나 쉽지 않았던 굵직했던 주제들이라 데드라인을 맞추기 위해 팀 내에서도 분업을 해서 정신없이 진행했었다. 이번 회고에서는 바쁘게 진행했던 작업들을 되짚어보면서 그 과정들을 기록해놓고자 한다.
4차 스프린트
Oauth2.0을 이용한 로그인 기능 구현
서문에서 언급했던 것처럼 이번 4차 스프린트에서 핵심적으로 구현한 신규 기능은 로그인이었다. 로그인 시스템을 직접 구축하기보다는 OAuth 프로토콜을 사용해서 소셜 로그인 기능을 도입하는 방향이 우리 서비스의 보안적인 측면에서 부담을 줄일 수 있으면서 기존 레퍼런스들을 통해 더 빠르게 구현할 수 있다고 판단했다.
Oauth 프로토콜은 사용자(Resource Owner)가 특정 사이트(Resource Server)에 저장된 정보(Resource)에 대한 접근 권한을 제 3의 웹 서비스나 어플리케이션(Client)에 개인정보를 제공하지 않고도 위임할 수 있도록 하기 위한 개방형 표준이다. Google 소셜 로그인 기능을 구현하려는 하루스터디 서비스 플로우에서 바라보면 위에서 언급된 주체들은 다음과 같다.
- Resource Owner : 하루스터디 서비스 사용자
- Resource : 하루스터디 서비스 사용자의 Google 프로필 정보
- Resource Server : Resource를 저장하고 있는 Google Server
- Client : 사용자의 Google 프로필 정보에 접근 권한을 얻어야하는 하루스터디 Server
그리고 Oauth 프로토콜에서는 인증,인가 작업을 수행하기 위한 4가지 플로우를 다음과 같이 제시한다.
- Authorization Code Grant
- Implicit Grant
- Resource Owner Credential Grant
- Client Credential Grant
이 중에서 웹 서비스에서 가장 일반적으로 사용하는 방식은 Authorization Code Grant 방식이다. 해당 방식을 이용한 Oauth 플로우를 도식화해보면 다음과 같을 것이다.
- ResourceOwner가 Client에게 Resource에 접근할 수 있는 권한을 위임하기 위해 인증 절차를 거친다. 인증이 완료되면 Authorization Code를 발급받는다.
- 인증을 완료하면 Resource에 대한 접근 권한을 Client에게 위임하기 위해 Authorization Code를 넘겨준다.
- Client는 Resource Owner에게 전달받은 Authorization Code를 가지고 Authorization Server에게 Resource에 접근할 수 있는 Access Token과 Refresh Token을 요청한다. 이후 발급받은 Access Token을 이용해 Resource Server에게 Resource를 요청한다.
- Client는 받아온 Resource를 이용해 Resource Owner에게 적절한 서비스를 제공한다.
하루스터디 Oauth 로그인 플로우
앞의 Oauth 플로우를 바탕으로 하루스터디의 로그인 기능을 설계했고 이를 도식화하면 다음과 같다. 아래 그림에서는 client가 하루스터디 프론트 페이지(혹은 서비스 사용자)라고 생각하고 보면 된다.
위의 이미지에서 주황색으로 표시된 과정들이 Oauth 프로토콜의 Authorization Code Grant 플로우에 따라 하루스터디 서버가 사용자 프로필 정보에 대한 인가를 부여받고 Google Server에 요청해서 받아오는 것에 해당한다. 성공적으로 사용자의 프로필 정보를 가져온 뒤 수행되는 빨간색 부분들은 하루스터디 서비스에서 자체적으로 수행하는 사용자 인가 확인 플로우다. 간단하게 말하면 적절한 사용자가 적절한 요청을 했는지 검증한다는 것이다. 이를 위해 서버에 들어온 요청이 로그인한 사용자의 것인지 판단하는데 사용될 Access Token과 Refresh Token을 자체적으로 발급한다. 이후 적절한 Access Token과 함께 들어온 요청은 정상처리를, 이외에는 예외처리를 해준다.
만약 Access Token이 만료된다면 Refresh Token과 함께 Access Token 재발급 요청을 보낸다. 그러면 서버에서는 Access Token과 Refresh Token을 모두 재발급해서 내려준다. 이는 Token이 탈취되었을 때의 위험성을 낮추기 위함인데 이렇게 설계한 근거는 다음과 같다.
- Access Token이 탈취되면 인가되지 않은 탈취자가 다른 사용자의 정보에 접근할 수 있게 된다. 이러한 위험성을 낮추기 위해 Access Token의 수명을 30분 정도로 매우 짧게 설정함으로써 탈취되었을 때 악용될 수 있는 여지를 최대한 없앨 수 있다.
- Refresh Token은 일반적으로 Access Token에 비해 수명이 매우 길게 설정된다. 따라서 Refresh Token이 탈취되면 RefreshToken 자체가 만료되기 전까지 탈취자가 Access Token을 마음대로 재발급 받아서 악용할 수 있게 된다. 이를 해결하기 위해 Refresh Token을 통한 Access Token 재발급이 이뤄질 때 Refresh Token도 함께 재발급하는 방식으로 탈취 시 위험성을 최대한 낮출 수 있다.
- 이러한 방식을 Reftesh Token Rotation(RTR)이라고 한다. 이를 통해 Refresh Token에 대한 탈취 상황 시 위험도를 낮출 수 있으나 사용되지 않은 RefreshToken을 탈취하거나 지속적으로 AccessToken만을 탈취하는 문제는 해결하기 힘들다.
위에서도 언급했다 시피 Token 방식을 사용하는 이상 Token이 탈취되었을 때 이를 악용하는 사례가 발생할 위험성을 0으로 만들 수는 없다. 하지만 Access Token의 수명을 30분 정도로 최대한 짧게 설정하고 재발급 요청마다 Refresh Token도 함께 갱신되도록 함으로써 각 Token이 탈취되었을 때 위험성을 상대적으로 낮출 수 있기 때문에 위와 같이 재발급 플로우를 설계했다.
로그인 기능 설계과정에서 겪은 어려움
프론트에서 API 리팩토링으로 인해 코드가 많이 난잡해져있던 상황에서 프론트 백엔드 모두 처음 구현하는 로그인 기능이다 보니 어떻게 설계해야하는지 전체적인 플로우를 같이 논의하는 과정에서 많이 힘들었다. 그래서 상대적으로 업무 부담감이 더 크게 와닿았을 프론트엔드를 더 배려하기 위해 Oauth 로그인 기능 플로우를 프론트와 백엔드로 나눠서 어떤 작업을 처리해야 하는지 정리하고 로그인 기능이 추가된 서비스 전체 플로우를 그려가며 어디에서 어떤 API가 호출 될 지를 미리 그려둔 다음 논의를 진행했다.
발행된 토큰들을 쿠키로 관리해야 하는지 세션 스토리지로 관리해야 하는지에 대한 논의도 팀적으로 이뤄졌었다. 이 논의에서 느낄 수 있었던 건 각 분야별 고유 영역에 대해서는 오히려 결정사항을 위임하는 것이 팀의 생산성을 보장하는 측면에서 더 좋을 수 있다는 것이었다. 백엔드 팀에서 아무리 어떤 방식으로 관리해야 프론트엔드가 더 구현하고 관리하기 편한 로직일지 고민해도 이 부분에 대해 결국 제일 잘 아는 건 프론트엔드 크루들일 것이기 때문이다. 협업을 하면서 같은 팀으로서 관심을 갖는 것과 고유 영역을 침하는 것을 잘 구분해서 일해야 팀이 잘 굴러갈 수 있다는 것을 깨달을 수 있었던 기회였다고 생각한다.
서비스에 인가 기능 추가하기
로그인 기능을 구현했으니 기존에 존재하던 API들에 대해 요청한 사용자의 인가가 적절한지 확인하는 기능을 추가하는 작업이 필요했다. 어떤 작업을 수행하는 API인지에 따라서 확인하는 인가 수준은 다르나 전체적인 프로젝트 수준에서 인가를 확인하는 과정을 간단하게 정리해보면 다음과 같았다.
- 로그인 이후 Client에서 들어오는 요청마다 Authorization 헤더로 access 토큰 값이 들어오면 Interceptor에서 1차적으로 Access 토큰을 꺼내서 만료여부를 확인한다. 이 때 토큰이 만료되거나 변조됨을 확인하면 예외처리 로직을 수행한다.
- Interceptor에서 기본적인 Access Token 검증 과정이 끝나면, ArgumentResolver를 통해 Access Token 소유자에 해당하는 member를 찾아서 컨트롤러의 매개변수로 받을 수 있도록 한다.
- 각 API마다 매핑되는 Controller의 메서드마다 요청 메세지 헤더에 있는 Access Token 소유자에 해당하는 member가 매개변수로 들어온다. 이제 요청으로 매핑된 member가 해당 요청을 수행하기 적절한 권한을 가졌는지를 검증하고 작업을 처리해주면 된다.
로깅 프레임워크를 이용한 로깅 기능 구현
4차 데모데이 전까지 만족해야 하는 최소 요구사항 중에는 로깅 프레임워크를 이용해서 로깅 시스템을 구축해야 한다는 것이 있었다. 개인적으로 가장 해보고 싶었던 부분이었기에 백엔드 팀 내에서 일정을 맞추기 위해 Task를 나눌 때 먼저 나서서 해보겠다고 지원했다. Spring Boot에서 기본적으로 채택하고 있는 로깅 프레임워크인 LogBack을 이용해 로깅 시스템을 구축했다. 해당 과정에서 어떤 데이터들을 로깅해야 할 지, HTTP 요청, 응답 메세지 로깅을 Interceptor에서 할 지, 아니면 Filter에서 할 지 등을 고민하고 결정해나가면서 정말 재미있게 개발할 수 있었다. 그리고 결정적으로 팀에서 실질적으로 겪고 있던 불편함을 해결하는데 일조했다는 점이 가장 의미있게 다가왔던 것 같다.
자세한 고민 및 해결과정은 다음 포스팅에 정리해뒀다.
이후 팀에서 사용중인 Slack에 알림을 보낼 수 있는 로거 구현을 시도해봤는데 해당 과정은 다음 포스팅에 정리해뒀다.
로그, 메트릭 데이터를 그라파나로 모니터링 대시보드 구축하기
최소 요구사항 중 또 다른 하나는 로그, 메트릭 데이터를 모니터링할 수 있는 대시보드를 구축하는 것이었다. 로그는 앞에서 구현했던 Logger로부터 저장된 로깅 데이터를 가리키는 말일텐데 메트릭이라는 말은 명확하게 어떤 데이터를 일컫는 단어인지 와닿지 않았던 기억이 난다.
매트릭(metric)은 지표라는 뜻의 단어이다. 그러면 서버 모니터링 대시보드에서 일반적으로 보여져야할 지표가 무엇인지를 생각해보면 될 것 같다. 스프링 부트에서는 Actuator라는 모듈을 통해 기본적인 서버 내부 지표들을 외부로 공개한다. 서버 메모리 사용량, CPU 사용량, GC 동작 정보, Hikari CP 현황 등 굉장히 다양한 정보들을 기본적으로 제공한다. 또한 추가 설정을 통해 이러한 정보들을 요청해서 받을 수 있게끔 API EndPoint 노출 여부도 결정할 수 있다.
다음과 같이 외부로 노출된 EndPoint로 요청하면 해당 메트릭 데이터를 응답으로 받을 수 있다.
{
"_links": {
"self": {
"href": "http://localhost:8080/actuator",
"templated": false
},
"beans": {
"href": "http://localhost:8080/actuator/beans",
"templated": false
},
"caches-cache": {
"href": "http://localhost:8080/actuator/caches/{cache}",
"templated": true
},
"caches": {
"href": "http://localhost:8080/actuator/caches",
"templated": false
},
"health": {
"href": "http://localhost:8080/actuator/health",
"templated": false
},
"health-path": {
"href": "http://localhost:8080/actuator/health/{*path}",
"templated": true
},
"info": {
"href": "http://localhost:8080/actuator/info",
"templated": false
},
"conditions": {
"href": "http://localhost:8080/actuator/conditions",
"templated": false
},
"configprops": {
"href": "http://localhost:8080/actuator/configprops",
"templated": false
},
"configprops-prefix": {
"href": "http://localhost:8080/actuator/configprops/{prefix}",
"templated": true
},
"env": {
"href": "http://localhost:8080/actuator/env",
"templated": false
},
"env-toMatch": {
"href": "http://localhost:8080/actuator/env/{toMatch}",
"templated": true
},
"loggers": {
"href": "http://localhost:8080/actuator/loggers",
"templated": false
},
"loggers-name": {
"href": "http://localhost:8080/actuator/loggers/{name}",
"templated": true
},
"heapdump": {
"href": "http://localhost:8080/actuator/heapdump",
"templated": false
},
"threaddump": {
"href": "http://localhost:8080/actuator/threaddump",
"templated": false
},
"prometheus": {
"href": "http://localhost:8080/actuator/prometheus",
"templated": false
},
"metrics-requiredMetricName": {
"href": "http://localhost:8080/actuator/metrics/{requiredMetricName}",
"templated": true
},
"metrics": {
"href": "http://localhost:8080/actuator/metrics",
"templated": false
},
"scheduledtasks": {
"href": "http://localhost:8080/actuator/scheduledtasks",
"templated": false
},
"mappings": {
"href": "http://localhost:8080/actuator/mappings",
"templated": false
}
}
}
이렇게 Actuator가 노출하는 EndPoint로 요청하면 우리가 원하는 메트릭을 받아올 수 있으나 요청하는 그 순간에 해당하는 메트릭만 받아올 수 있다. 즉 Actuator가 실시간으로 반복해서 메트릭을 수집하는 것은 아니라는 뜻이다. 서버의 상태를 모니터링하기 위해서는 요청 순간에 해당하는 메트릭만 수집하는게 아니라 실시간으로 지속적인 메트릭 수집을 해야하는데 이러한 작업을 수행하는 것이 바로 프로메테우스다. 프로메테우스는 지속적으로 수집한 메트릭을 내부 DB에 저장하는 작업을 수행하게 된다.
그리고 이렇게 프로메테우스에 지속적으로 수집되는 데이터들을 쉽게 모니터링할 수 있도록 대시보드 툴을 제공하는 것이 그라파나다. 우리가 직접 그래프나 시각화 툴을 만들 필요 없이 기존에 만들어져 있는 대시보드 템플릿 중 하나를 선택해 매우 빠르고 편하게 대시보드를 구축할 수 있다.
하지만 Actuator + 프로메테우스 조합으로는 로깅된 파일 데이터를 받아오는 기능을 제공하지 않는다는 문제가 있었다. 그래서 서버에 로깅된 파일 데이터를 받아와서 그라파나로 시각화할 수 있도록 로키와 프롬테일을 사용하기로 결정했다.
로키는 프로메테우스로부터 착안된 오픈소스 로그 집계 시스템으로 그라파나 랩스(Grafana Labs)팀에서 개발하고 있다. Like Prometheus, but for logs 라는 표어를 내걸고 있는 것처럼, 프로메테우스가 지원하지 않는 로그 데이터를 받아와서 저장하는데 그 목적을 두고 있다. 로키를 기반으로 구축할 수 있는 로깅 스택은 다음과 같은 3가지 구성요소로 구성할 수 있다.
- promtail : 로그를 실제로 수집하고 이를 Loki에게 전송하는 agent 역할을 수행한다.
- loki: promtail이 전송한 로그 데이터를 저장하고 조회 쿼리를 처리하는 책임을 지는 메인 서버 역할을 수행한다.
- Grafana : 로그 데이터를 조회하기 위해 쿼리를 날리고 조회 결과를 시각화하는 역할을 수행한다.
결과적으로 하루스터디 프로젝트 인프라에서 로그, 메트릭 데이터 모니터링 시스템을 구축한 설계는 다음과 같았다.
현재는 모니터링 도구들이 ec2 인스턴스 환경 내에 직접 설치되어 있는 상태이다. 이후 무중단 배포를 위한 서버 구조 변경 작업 시 이러한 부분들을 도커로 편하게 마이그레이션할 수 있는 방법도 학습해보고 적용해볼만한 부분이라는 생각이 들었다. 그리고 현재는 모니터링 환경을 구축하는데 의의를 뒀었는데 실제 서비스에서 이를 적극적으로 활용할 수 있는 방안으로는 무엇이 더 있을지 고민해봐야겠다.
회고 마무리
드디어 4차 스프린트까지 마무리되고 론칭 페스티벌까지 거치면서 하루스터디 서비스가 정식 론칭되었다. 2달 가까이 되는 기간을 정말 빠듯하게 달려서 완성시키기도 했고 처음으로 실 사용자가 있는 서비스를 만들어본 경험이 처음이다보니 감회가 남달랐던 것 같다. 다른 크루들이 직접 시연해보면서 긍정적인 평가를 해줄때마다 그동안 힘들게 기획했던 부분들도 다 이 순간을 위해서 존재했다는 걸 알 수 있었다.
이제 남은 부분은 레벨4를 지나며 기능 고도화와 성능 개선을 수행하는 것이다. 이렇게 서비스에 애착이 생긴만큼 레벨4에서도 더 나은 사용자 경험과 서비스를 위해 노력할 수 있을 것 같다는 생각이 들었다. 무사히 레벨3를 마친 것처럼 하루스터디가 레벨4도 끝까지 잘 넘어갈 수 있기를 기원하며 회고를 마쳐본다.
'우아한테크코스 > 회고' 카테고리의 다른 글
[우아한테크코스] LV3 - 3차 스프린트 회고 (0) | 2023.08.20 |
---|---|
[우아한테크코스] LV3 - 2차 스프린트 회고 (2) | 2023.08.13 |
[우아한테크코스] LV2 - 웹 장바구니 협업 미션 회고 (2) | 2023.08.13 |
[우아한테크코스] LV3 - 1차 스프린트 회고 (2) | 2023.07.09 |
[우아한테크코스] 중간 회고 (2) | 2023.07.02 |