기나긴 겨울이 지나 어느덧 봄이왔다. 나란 사람은 여전히 생각이 복잡한 사람이지만, 글이라도 쓰면 복잡한 마음이 조금이나 해소될까, 지난날들을 되돌아보며 앞으로 나아가고자 한다.
그동안 바쁘다는 핑계로 글을 쓰지 않은지가 너무 오래되기도 했다...
(팀 전체적인 이야기보단 나의 이야기 위주 인 점 양해 바람.)
지난해 약 4개월간의 본과정을 마치고 아카데미에서의 마지막 관문인 인증 프로젝트 과정에 선발되어 12월 26일부터 3월 3일까지 팀 프로젝트를 수행했다.
TA님들이 인증 프로젝트 과정은 '상상 그 이상이다. 어디가서 이렇게 프로젝트를 하긴 쉽지 않을 것이다'라며 엄청난 성장을 할 수 있을 것이라 조언해주셨다.
두려움이 앞섰지만, 조금 더 제대로 생각하고 개발 할 수 있는 완성도 있는 프로젝트에 대한 경험이 지금이라 생각하고 기대감 또한 높았다.
프로젝트 결과는 다음에서 확인 가능합니다.
팀 프로젝트라면, 학교에서 캡스톤디자인이나 전공 과목에서의 간단한 개발 경험 뿐이라 여기서의 팀 프로젝트도 비슷하지 않을까 싶었다.
예상은 전혀 빗나갔다. 소프트웨어 공학론에서 볼법한 애자일 스크럼 형식의 협업을 진행하게 되었다. 팀 구성시 TA님들이 스크럼마스터를 하게되는 타 팀들과는 달리 우리 팀은 특이하게 학생들로만 구성되었고 이 중 누군가는 스크럼마스터의 역할을 수행해야만 했다.
모두가 처음이고 두려움이 앞섰지만, 결과가 어떨지를 떠나서 그냥 해보고싶었다. 프로젝트에서 팀원으로만 남는다면, 그저 팔로워가 될 것 같았고 의미있는 경험을 해보면서 뭔가라도 하나 더 배우고자 스크럼마스터 역할을 자청했다.
프로젝트 요구사항 및 기술 스택 가이드라인을 받아보니, Spring Cloud 기반 분산 서버환경에서의 온라인 서점 웹 서비스를 구축하는 프로젝트였다. 모놀리식 아키텍처에 익숙해있고, 이를 기반으로 학습했기에 모두가 '아 이거 할 수 있을까...?' 라는 걱정이 컸다. 어쨌든 프로젝트는 시작됐으니 우선 달리자.
온라인 서점이라면, Yes24, Aladin과 같이 우리가 평소에 많이 쓰는 서비스들이 떠올라 이들의 장점을 합치자는 의미에서 팀명을 YesAladin으로 선정했다.
NHN Dooray와 Google 스프레트시트를 통해 일정 관리 및 WBS를 작성하였고, 우리 팀의 스크럼 타임은 매일 오전 9시 반이었다. 매일 오전 스크럼 회의를 통해 각자 맡은 업무와 진행 상황, 논의해야 할 점, 기술적 공유 등에 대해 간단하게 소통 후 문제를 어떻게 해결해나가야 할 지 함께 고민하였다.
개인적인 생각으론, 스크럼 마스터라 해서 대단한 업무를 하는 것은 아니고, 이 프로젝트가 어떻게 하면 올바른 방향으로 나아갈지에 대해 큰그림을 그리고 팀원 사이의 중간다리 역할을 하는 것이라 생각한다.
팀원들 중 해보진 않았지만 배경지식으로 이 프로젝트를 어떻게 해야 하는지에 대해 아는 사람들도 있었고, 얕은 지식의 수준이지만 나 또한 거들어 요구사항 분석 시 MSA에 대한 이야기가 많이 나왔다.
아무래도 Spring Cloud 기반 구조와 MQ를 써야하는 요구사항 때문인지 흔히들 말하는 MSA 아키텍처를 생각했던 것 같다. 이 때문에 ERD 설계 시 유의미한 구조로 각 도메인을 나누고 이에 대한 DB와 서버를 전부 나눠서 문제를 풀어야하지 않을까 했다. 레이어드 아키텍처가 아닌 헥사고날 아키텍처에 관한 의견도 나왔고, Eureka와 Spring Cloud Config를 적용하고 이런 고민들과 인프라 구축을 하며 2주라는 시간을 썼던 것 같다.
오전 스크럼에 참석하며 피드백을 주신 책임님과 수석님께서 '작은 부분부터 점진적으로 개선하는 방향이 좋을 것이다', '헥사고날 아키텍처는 너무 과하다' 라는 의견으로 억제하지 않으셨다면 일정관리는 커녕 프로젝트가 산으로 갔을 것 같다.
처음 해보는 부분들이 많아 안그래도 진입장벽이 높은데, 어쨌든 프로젝트를 진행하다 개발은 커녕 설정 문제들을 해결하느라 시간을 허비해서 결국 Eureka 적용이나 기타 인프라 및 아키텍처에 관한 모든 설계를 밀어버리고 다시 시작하는 결정을 하게 되었다. 팀 구성원 전원이 구조를 하나라도 이해하지 못한다면 이 프로젝트는 실패할 것이라는 큰 교훈을 얻었다.
올바른 방향으로 나아가게 할 수 없었던 실수는 개인적으론 많이 힘들었지만, 도전과 실패에 대한 경험은 내게 많은 생각을 주었다.
어쨌든 가장 중요한 것은 8주라는 짧은 시간 안에 요구사항을 맞춰 일정을 진행하도록 하는게 나의 일이라는 것을 다시금 상기시켰다.
온라인 서점 서비스 구축에 관한 요구사항을 통해 개인적으로 해보고 싶었던 파트는 다음과 같았다.
팀원들에겐 JWT와 인증/인가라는 개념이 생소하게 다가왔고 우리 팀은 누군가 이 파트를 담당하게 된다면 프로젝트 전체적으로 해야 할 일들이 가장 고될 것이라 생각했다. 캡스톤디자인으로 웹서비스를 개발했던 경험을 토대로 JWT에 큰 어려움이 없었던 내가 담당하기로 결정했고 회원 도메인과 더불어 개발을 시작하게 되었다.
처음엔, JWT형식의 Access Token과 Refresh Token을 발급하여 DTO로 프론트 서버에 전달해 프론트 단에서 이를 자체적으로 사용하는 방식이라 생각했었다.
하지만, Vue, React와 같은 프론트엔드 프레임워크를 사용하는게 아닌 Spring Boot + Thymeleaf 기반의 프론트 서버를 구축해야 해서 이전에 개인적으로 경험했던 방식과는 전혀 다르게 해결해야만 했다.
각 서버는 RestTemplate을 사용해 Spring Cloud Gateway로 구축된 게이트웨이 서버를 통해 API를 요청/응답 해야 하는 구조이고, 게이트웨이 서버 자체에서도 추가적인 검증 로직이 필요했다.
인증/인가 서버는 별도의 서버이지만 Database에 직접적으로 연결하지 않고 Shop API 서버를 통해 유저 정보를 받아오는 방식으로 구성해야 했고, 프론트 서버의 Security Filter와 인증 서버 자체의 Security Filter의 flow을 고려해서 내부적으론 서버간 통신이 여러번 이루어 져야 한다는 점에서 굉장히 난해했다.
개발하면서 고려해야 할 점들이 생각보다 복잡하고 쉽지 않다는 점 때문에 인증/인가 개발이 쉽고 빠르게 할 수 있을 것이라 프로젝트 초기에 생각했던 나를 늘 반성하게 했다.
결과적으로 내가 해결한 방식은 요약하자면, 다음과 같다.
Front 서버에서 UsernamePasswordAuthenticationFilter
를 대체할 Security Filter를 커스터마이징하고, 이 필터를 통해 Form으로 입력된 LoginId와 Password를 받아 인증서버에 전달 후 인증 성공 시 AccessToken과 유효시간 및 로그인에 성공한 유저마다 구분하기 위해 자체 발급한 uuid를 HTTP Header를 통해 받아와 Redis로 이를 관리하도록 개발하였다.
OAuth2 소셜 로그인 기능도 Spring Security에서 제공하는 Filter를 사용할 순 있지만, 일정상의 문제 등을 이유로 해당 Provider와 API 통신을 통해 자체적으로 해결하도록 Controller와 Service를 구현하는 방식으로 개발하였다. 이 부분은 추 후 OAuth2 Filter에 대해 학습 후 적용해보고자 한다.
인증/인가 서버 프로젝트의 README에 작성했던 상세한 flow는 다음과 같다.
https://github.com/NHN-YesAladin/yesaladin_auth
Front Server에서의 사용자 요청을 받아 유효한 요청인지의 여부를 판별하고, JWT를 발급시켜주기 위해 UsernamePasswordAuthenticationFilter를 Customizing 하였습니다.
사용자 정보에 대한 Database는 Shop API Server에 종속되어 있어 내부적 flow는 다음과 같습니다.
AuthenticationProvider에 의해 책임을 받은 UserDetailsService를 Custom한 곳에서 RestTemplate으로 API 호출 후 인증 과정을 수행합니다. Spring Security를 적용한 서버는 Front Server, Shop API Server, Auth Server이고 Front Server를 제외한 각 서버는 Session 유지방식을 Stateless로 고정하였습니다.Front Server의 경우 Vue.js, React.js 등과 같은 Frontend Framework가 아닌 Spring Boot + Thymeleaf 기반의 서버이기 때문에 인증/인가 요청 이후 발급된 JWT를 직접적으로 Http Body에 넣어 넘겨주지 않고 회원마다 고유하게 발급된 uuid와 accessToken과 같은 정보만을 HTTP Header에 넣어 return 합니다. Front Server는 발급받은 JWT를 기반으로 Session 및 Cookie를 활용하는 방식으로 회원의 로그인을 유지하도록 설계하였습니다.
또한, Front Server의 scale out으로 인해 로그인을 유지하기 어렵다는 문제로 Redis를 공유 세션 저장소로 사용하였고, 사용자는 Auth Server로부터 브라우저에 Cookie로 발급된 uuid key를 기준으로 Redis Session에 접근하는 방식으로 로그인/로그아웃, 토큰 재발급의 기능을 수행합니다.
토큰 재발급의 경우, JWT의 accessToken, refreshToken을 토대로 accessToken의 유효한 타임을 기준으로 Front Server에 구현했던 Interceptor를 통해 사전에 재발급 해야 하는 시점인지 판별하고, 이에 해당하면 자동으로 Auth Server에 재발급 요청을 보내 응답받은 뒤, 다음 과정들(페이지 이동, Shop API 호출 등)을 수행하도록 합니다.
Shop API Server는 Front Server로부터 Authorization Header에 담긴 JWT 토큰 정보를 받아 이에 대한 인가 처리를 사전에 Auth Server로 위임합니다. Auth Server에서 해당 JWT 토큰의 유효성 검증이 완료되어 인가 된 경우, payload에 들어있는 사용자 식별 정보와 권한 정보를 추출하여 Shop API Server에 돌려줍니다. 이 정보를 바탕으로 Shop API Server 내에서 Spring Security를 통해 자체적으로 Authentication 을 생성하도록 처리하였으며, FilterSecurityInterceptor 및 method security를 적용하여 API 보안을 강화하였습니다.
게이트웨이 서버에서 JWT에 대해 유효한지 판단하며 블랙리스트를 관리하는 방식으로 Filter를 적용하고 검증에 통과한다면, 게이트웨이 서버는 추가적인 HTTP Header를 사용하여 payload에 들어있는 유의미한 정보를 뒷단에 위치한 Shop API 서버에 전달하고 Interceptor와 AOP를 사용하여 문제를 해결하고자 했지만, API 서버 자체의 보안 문제와 간단한 관리를 위해 Shop API 서버에 Spring Security를 추가하고 OncePerRequestFilter
를 커스터마이징 하여 매 요청마다 인증/인가 서버만이 JWT를 디코딩하고 이 정보를 기반으로 자체적인 인증 객체를 만들어 method security를 적용하는 등의 방법으로 해결하였다. 이를 구현하는데 어려움이 많았는데, 도움을 주신 홍대님께 감사함을 표하고싶다.
결과적으론 요구사항 전체를 개발할 순 없었다. 온라인 쇼핑몰이라면 기본적으로 필요한 상품, 주문/결제, 회원에 대해 개발하였고 쿠폰 서비스를 담당하는 팀원들의 노력으로 Kafka 도입 및 성능 개선기에 힘쓰고 최종 발표회에서 이를 주제로 발표하게 되었다. 중간에 한 팀원이 개인 일정으로 인해 불가피하게 팀을 이탈하는 상황 또한 있었지만, 해당 팀원이 충분히 노력해줬고 우리 모두가 공백을 없애기 위해 한마음 한뜻으로 노력했던 점들 때문에 공백에 대한 문제는 없었다. 오히려 신경쓰지 말고 잘 쉬고 오라는 훈훈한 분위기가 좋았다.
우리 팀은 Shop API 서버의 Test Coverage 80% 이상 달성을 목표로 하였고, 결과적으론 87.11%를 달성하며 프로젝트를 완료하였다.
팀원 모두가 시간이 더 많았더라면 다음과 같은 것들을 했을거라 회고한다.
프로젝트와는 별개로 늘 갖고있는 나의 숙제 또한 풀어나가야 할 것 같다.
촉박한 프로젝트 일정 상 위와 같은 문제들을 스스로 풀지 못하다보니 어느 순간부터 지금까지 번아웃에 빠져있다. 모든 구성원이 새벽잠을 못자며 똑같이 고생한 것은 맞지만, 내가 이들보다 뭐가 더 특별하다고 멘탈관리를 잘 못했나 싶다.
함께하는 동료들 덕에 힘들땐 힘들다 말할 수 있었고, 함께 문제를 풀어가는 것에 대해선 너무나도 감사하고 미안하다. 이만큼 좋은 동료들을 얻었다는 게 아직도 꿈만 같다.
첫 타석에 오르자마자 홈런을 치는 헛된 이상을 갖고 살지 않았나 싶고, 이제부터가 시작이고 앞으로가 더 중요하니 하루 빨리 정신차리고 다시 일어서야지. 프로젝트가 끝난 것과 최종 개인 평가의 결과가 전부가 아니니까.
배포된 서비스가 언제까지 유지될진 모르겠지만 못할 것 같았던 프로젝트가 결말이 났다는 점에 대해 너무나도 기쁘고 후련하다.
NHN 플레이뮤지엄에서 팀을 대표하여 프로젝트 발표를 했다는 점에서도 개인적으론 뜻깊었다. 언젠가 개발자로서 어느 컨퍼런스에 발표자로 서보는게 목표 중 하나인데, 그 목표를 향해 잘 나아가고 있지 않나 싶다.
우리 팀원 모두가 앞으로도 원하는 바를 성취하며 더 나아가길 진심으로 바란다. 나보다 더 낫다는 나의 말에 대해 늘 기만이라고들 답하던데, 겪어봐서 알겠지만 저 별거 없는 사람입니다...
참고로, 우리 프로젝트는 MSA는 아니다. Database는 하나로 모든 도메인이 테이블 별로 들어가 있고 도메인 별로 서버를 분리해서 이에 맞게 DB를 나눈게 아니기 때문이다. 게다가 특정 서버들을 별도로 분리했고, 쿠폰 서버와 Shop API 서버 간 트랜잭션 보장을 위해 kafka를 사용했지만, 실패나 예외 상황 시 복구하는 로직까지 작성한 것은 아니다.
문과 출신 대입 3수생에 집에 컴퓨터도 없던 컴맹이란 사람이 어찌저찌 컴퓨터공학과에 늦게서야 진학했고, 전공자임에도 불구하고 코딩을 늦게서야 시작했을 정도로 개발이라곤 아무것도 모르고 우연한 계기로 스스로 공부를 시작하던 풋내기가 대학교 마지막 학기를 NHN Academy에서 보내고 결국 졸업을 하게 됐다.
이젠 취준생의 신분으로 나아갈텐데, 앞으로 너무 조급해하지도 너무 늘어지지도 말고 지난 8개월 여간의 기억들을 토대로 부족한 점들을 메꿔가며 겉으로만 개발자가 아닌 진짜 개발자가 되기위해 노력해야겠다.
그동안 한 번도 해보지 않았지만 프로젝트에서 경험했던 '스케일 아웃, CI/CD 구축, 분산 서버 아키텍처 등'을 토대로 다시 한 번 꼭 해보고 싶고 내 것으로 만들고 싶은 화려한 것들도 많지만, 우선시 해야할 건 가장 중요한 본질인 기본을 제대로 쌓아야한다는 것이다. 알지만 답변을 제대로 할 수 없는 것들은 내 것이 아니라는 것을 충분히 느꼈기 때문이다.
좋은 동료들을 얻은 만큼 모두가 좋은 기회로 웃으면서 다시 재회하기를 바란다.
늘 좀 더 생각할 수 있도록 문제를 제시해주신 책임님, 수석님과 늘 뒤에서 든든하게 서포트 해주신 사무장님, 우리 교육생들을 아끼는 제자들이라 말씀해주시는 학장님, 부학장님 모두들 감사합니다. 앞으로도 잘 살아보겠습니다. 많이 배웠습니다.