조직문화


첫 3개월, 어떻게 살아남았냐고요? ‘개발과 수정 무한∞ 속에서’

한국신용데이터
2024-06-07
조회수 271



안녕하세요! 금융사업팀 신입 백엔드 엔지니어 hyena(헤나) 입니다.

어느덧 한국신용데이터(KCD)에 합류한 지 3개월이 지났네요. KCD를 처음 만난 날이 새록새록 기억 나는데요. 그 날은, 우아한테크코스라는 캠프에서의 리크루팅 데이였습니다. 당시 KCD 분들과 함께 이야기 나누면서 제가 ‘성장하기 좋은 환경’이라는 느낌을 받았어요. 특히 온보딩을 위한 여러 세션이 있다는 것이 매력적이었죠. 어떤 일들이 있었는지 다 말씀드리고 싶지만, 오늘은 제가 입사하자마자 수행한 ‘파일럿 프로젝트’로서 저의 생존 일기를 이야기해 보려 해요.

파일럿 프로젝트

파일럿 프로젝트를 시작하기 전에 팀 프로젝트 환경에 익숙해지기 위해 여러 도움을 받았는데요. 팀 내 데모 프로젝트를 별도의 gradle sub-module로 분리하여 구축하는 프로젝트를 진행해봤습니다. Spring + Kotlin 베이스로 되어 있어 언어에 익숙해지는게 필요했고 클린 아키텍처 기반의 팀 내 아키텍처 그리고 새롭게 접하는 인프라를 테라폼으로 구성해보면서 전체적인 구조를 파악했습니다.

그러고나니 팀 환경에 적응되고 자신감이 넘쳐흐르고 있었고 때마침 흥미로운 파일럿 프로젝트를 받았는데요. 주제는 바로 ‘접근제어’입니다.

할 수 있음.

접근제어 프로젝트를 받게 된 이유

제가 속한 금융사업팀은 2022년에 신설된 신생팀입니다. 기존 캐시노트(cashnote) core에 있던 금융서비스들을 MSA로 분리하면서 별도의 개발/운영 환경을 구축하게 됐습니다. 이러한 환경에서 신규 서비스들을 개발하게 되면서 폭발적인 성장이 있었고 자연스럽게 어드민도 core에서 분리하게 됐습니다. 그 과정에서 각 사용자와 조직별 접근 권한에 대한 제어 기능이 추가하게 되었고, 운 좋게 입사하자마자 개발에 함께 참여할 수 있게 되었습니다.

접근제어시스템의 필요성

접근제어시스템은 말 그대로 권한에 맞는 메뉴의 노출과 이벤트의 접근제어 역할을 할 수 있는 할 수 있는 시스템 환경을 말하는데요,

접근제어를 통해 안정적인 금융서비스를 제공하고 데이터의 기밀성, 무결성을 보장하여 보안 사고를 예방할 수 있기 때문에, 대부분의 핀테크 기업에서는 대부분 적용되어 있는 시스템이라고 합니다.

접근제어 요구사항

처음에는 단순히 Filter를 통해 권한 validation정도만 하면 되는 줄 알았는데 생각보다 해야될 작업들이 정말 많았습니다. 아래는 이번 프로젝트를 하면서 접근제어 기능에 필요한 요구사항을 정리해보았는데요, 인증/인가, 메뉴 제어, 권한 제어, 이벤트 제어, 사용자 제어 등의 다양한 기능들이 유기적으로 구성되어있어야 했습니다.

인증/인가 기능

  • LDAP을 통해 사용자 로그인(ID/PW)을 진행한다.
  • 요청 값에 토큰(JWT)으로 사용자를 확인한다.
  • 사용자가 권한이 없는 이벤트를 요청하면 에러를 응답한다.

메뉴 제어 기능

  • 메뉴 정보 히스토리를 보관한다.
  • 메뉴는 고유한 식별 값을 갖는다.
  • 식별 값으로 메뉴 정보를 알 수 있다.
  • 메뉴는 계층 구조를 갖는다.

권한 제어 기능

  • 권한 정보 히스토리를 보관한다.
  • 권한은 고유한 식별 값을 갖는다.
  • 권한은 접근 가능한 메뉴 정보를 갖는다.
  • 권한은 접근 가능한 이벤트 정보를 갖는다.

이벤트 제어 기능

  • 이벤트 정보 히스토리를 보관한다.
  • 이벤트는 고유한 식별 값을 갖는다.
  • 식별 값을 통해 이벤트 정보를 알 수 있다.

사용자 제어 기능

  • 사용자의 권한 기록을 보관한다.
  • 사용자는 권한을 N개 가질 수 있다.
  • 사용자는 접근 가능한 메뉴만 볼 수 있다.
  • 사용자의 접근 기록을 보관한다.

내용이 워낙 방대하다보니 이번에는 아래 모든 요구사항이 아닌 ‘인증/인가’ 부분을 구현하면서 설계한 구조와 느낀 점에 대해서 이야기해보려 합니다. 다음 이야기가 궁금하신 분들은 댓글 부탁드립니다~ 😁

접근제어 설계

위와 같은 요구사항을 어떻게 개발할 수 있을지 팀 동료들과 논의해본 결과, Spring Security와 HandlerInterceptor를 custom하는 방법이 있었는데요. Spring Security의 공식 문서가 잘 정리되어 있어 학습하기 좋고 관련된 정보들이 외부에 많이 나와있어 다양한 문제를 안정적으로 해결할 수 있다고 생각했고 접근제어 요구사항 중에서 사용자 로그인, 사용자 토큰 확인, 사용자 권한 확인 등의 기능을 해결하는데 Spring Security에서 제공하는 Filter를 잘 활용한다면 생각보다 쉽게 만들어갈 수 있을 것이라 판단하여 Spring Security기반으로 개발하기로 결정했습니다.

인증/인가 설계

기존에 만들어진 웹어드민에 LDAP을 이용한 사용자 로그인, 사용자 토큰 Validation이 이미 개발되어 있었기 때문에, 추가로 개발해야 하는 것은 사용자 권한 확인 기능이었습니다. 그래서 기존 기능들이 어떻게 구성되어 있었는지 Sequence diagram을 통해 설명하고 Spring Security로 옮겼던 작업에 대해 이야기 해보겠습니다.

첫 번째로 ‘사용자 로그인’ 기능입니다.

로그인 시에 사용자의 ID와 PW가 맞는지 확인해야 합니다. 서버는 사용자로부터 전달받은 ID와 PW를 LDAP 통신을 이용하여 AD 내에 사용자의 ID, PW와 일치하는지 확인합니다. 사용자 확인이 성공하면 토큰에 넣어줄 사용자 정보를 DB에 조회하여 가져옵니다. 이때 만약 사용자 정보가 DB에 존재하지 않는다면 최초 로그인으로 판단하고 DB에 신규 사용자 정보를 저장합니다. 사용자 조회 또는 저장이 끝나면 필요한 정보를 주입한 토큰을 발행하여 사용자에게 응답합니다.

두 번째는 ‘사용자 토큰 확인’ 기능입니다.

위에서 사용자 로그인 후에 토큰(JWT)을 응답 받았습니다. 이제 클라이언트(브라우저, 앱)는 서버에 이벤트를 요청할 때마다 토큰을 같이 송신합니다. 서버는 전달받은 토큰의 서명을 확인하고 파싱합니다. 그리고 DB에 사용자 정보를 확인한 후에 클라이언트로부터 요청받은 이벤트를 비즈니스로직에서 수행합니다.

세 번째는 ‘사용자 권한 확인’ 기능입니다.

클라이언트로부터 전달받은 사용자 토큰이 정상적이라면 이제 이벤트에 대한 접근 권한이 있는지 확인해야 합니다. 이벤트에 대한 정보는 HTTP의 URI그리고 HttpMethod를 이용해서 DB에 조회하여 이벤트 식별 코드를 알 수 있습니다. 그리고 이벤트 식별 코드를 이용해서 현재 사용자가 가지고 있는 권한들 중에서 이벤트 접근 권한이 있는지 확인합니다. 만약 이벤트를 접근할 수 없다면 서버에서 에러를 응답합니다. 접근 가능하다고 판단되면 이벤트를 정상적으로 수행합니다.

여기까지 인증/인가의 세 가지 기능에 대한 흐름을 봤습니다. 첫 번째 기능인 ‘사용자 로그인’은 로그인 요청에서만 필요한 로직입니다. 두 번째, 세 번째 기능은 대부분의 Admin 화면에서 특정 버튼이 트리거가 됩니다. 그렇기 때문에 절차적으로 사용자 권한을 확인하기 위해서는 사용자를 식별할 수 있는 토큰이 필요하기 때문에 ‘사용자 토큰 확인’이 ‘사용자 권한 확인’보다 먼저 수행되어야 합니다.

인증/인가 개발 with Spring Security

인증/인가에 필요한 세 가지 기능인 ‘사용자 로그인’, ‘사용자 토큰 확인’, ‘사용자 권한 확인’의 흐름을 위에서 보았는데요, 이제 인증/인가 기능을 Spring Security를 이용해서 세 가지 필터를 구현하여 시큐리티 필터에 추가해 보겠습니다.

위에서 설명한 세 가지 기능은 Filter로서 동작하도록 개발하였고, 각각의 인증, 인가가 실패할 경우에 Servlet안으로 유입되지 않기 때문에 Spring까지 진입하지 않고 곧바로 에러응답을 하도록 구현했습니다.

첫 번째로 ‘사용자 로그인’ 필터입니다.

AuthenticationFilter를 커스텀했고 사용자 조회 혹은 사용자 저장까지의 역할을 하고 있습니다. 그리고나서 LoginService에서 사용자 정보를 담은 토큰을 발행하고 클라이언트에게 토큰을 응답합니다. (가독성을 위해 최대한 sudo코드형태로 재구성하였으니 이점 양해바랍니다. )

두 번째는 ‘사용자 토큰 확인’ 필터입니다.

RememberMeAuthenticationFilter를 커스텀하여 cookie가 아닌 header를 이용하도록 했습니다. 토큰의 서명을 확인하고 사용자 정보를 추출합니다. 그리고 실제 DB에 저장된 사용자 정보와 일치하는지 검증합니다. 사용자 정보와 일치할 경우에는 현재 요청이 진행될 로직에서 사용할 수 있도록 사용자 정보를 메모리 저장소에 주입합니다.

세 번째는 ‘사용자 권한 확인’ 필터입니다.

요청에 대한 URI, HttpMethod를 추출하여 DB에 이벤트 정보를 조회합니다. 그리고 두 번째 필터에서 메모리 저장소에 넣은 사용자 정보를 가져와 현재 사용자가 이벤트 접근 권한이 있는지 검증합니다. 만약 접근 권한이 없을 경우에는 throw하여 에러를 응답하고 접근 권한이 있을 경우에는 정상적으로 다음 동작으로 넘어갑니다.

위에서의 세 가지 필터를 이용해서 인증/인가 요구사항을 모두 충족시킬 수 있었습니다!

코드 리뷰

세 가지 필터를 이용해서 인증/인가를 개발했고 이에 대한 팀 동료들의 코드 리뷰를 받을 수 있는 시간입니다. 우아한테크코스에서 동료들과 코드 리뷰를 할 때도 굉장히 재밌는 시간이었기에 사내에서의 코드 리뷰도 굉장히 기대하고 있었습니다. 얼마나 많이 배울 수 있을까 생각하기도 했지만 Spring Security를 잘 활용해서 구현했기에 코드 리뷰가 금방 끝날 줄 알았습니다.

자그마치 2시간 !!

굉장히 복잡하고 어려운 구조에 회의실이 술렁거렸습니다.

사실 제가 개발한 프로젝트를 바탕으로 다른 개발자에게 인수인계를 해주기로 했었는데, 구현 복잡도가 높아 인수인계받을 개발자가 잘 이해할 수 있을지와 ServletFilter에서 Spring의 Service레이어를 직접 호출하는 비즈니스 로직이 너무 많이 있기 때문에 이로 인해 발생할 수 있는 우려에 대해 이야기를 해주셨습니다.

‘처음에는 이게 무슨 문제가 되지’ 라고 생각했지만, 질문에 답변을 하면 할수록 점차 동료들의 질문에 제대로 대답하지 못하면서 미궁에 빠지고 있는 것이 느껴졌습니다. 분명히 구현한 각 필터는 역할에 충실하다고 생각했지만 점차 동료들의 피드백을 통해 무엇을 의미하고 있는지를 이해할 수 있게 되었습니다.

구현 복잡도

그렇기 때문에 이야기한 세 가지 필터 구현을 위해서는 많은 SpringSecurity 커스텀이 필요했습니다. Spring Security에서 제공하는 필터들을 하나씩만 간단하게 커스텀해서 3개만 구현한 것이 아니고 최종적으로 18개 클래스를 구현했었습니다. 실제로 필터 내부 동작을 요구사항에 맞추기 위해서 커스텀을 하였고각 클래스의 역할에 맞게 분리하다보니 나온 결과물이었습니다.

개인적으로 과하게 클래스를 많이 만든 것도 있지만 필터에 여러 로직들을 처리하려고 하다보니 클래스가 많아진 부분도 있습니다. 그러던 중 처음에 프로젝트를 시작할 때쯤 동료 중에 한분이 하신 말씀이 기억났네요.

“SpringSecurity는그대로 사용하기는 편리하지만, 커스텀하기가 매우 번거롭고 불편하기 때문에 작은 규모가 아니라면 그대로 사용하는 경우가 많지 않다.”

처음에는 그 말이 잘 와닿지 않았는데 다 개발하고 보니 그 말이 무슨 뜻인지 잘 알 수 있게 되었습니다.

개발할 때는 이 부분에 매몰되어 있다보니 이게 과한 커스텀인지 전혀 인지 하지 못했지만, 나중에 코드리뷰를 받고 리팩토링을 해보니, 제가 얼마나 오버엔지니어링을 했는지 느낄 수 있었습니다.

계층 규칙

위의 이미지는 Spring 프레임워크를 사용하는 사람이라면 누구나 알고 있는 아키텍처 입니다. SpringSecurity의 Filter는 Servlet Filter를 상속하여 구현하기 때문에 위의 빨간색 박스처럼 DispatcherServlet 앞에 있습니다. 그렇기 때문에 제가 구현한 필터들은 Spring Flow를 타지 않고 직접 @Service 레이어를 호출하였고, 특히 저희 팀에서 구축한 Application Architecture Flow 역시도 타지 않는 문제가 있었습니다.

물론 제가 개발한대로 ServletFilter에서 직접적으로 비즈니스 로직을 호출한다고 무언가 동작하지 않거나 문제가 발생하지는 않았지만, 그런 일들이 발생하지 않도록 많은 커스텀을 할 수 밖에 없었던 것이었습니다.

그로인해 코드리뷰하는 내내 동료들에게 코드를 설명하는 것이 어려울 수 밖에 없었습니다.

동료들의 코드리뷰를 받고 많은 고민을 하게 되었습니다. ‘이게 진짜 문제가 될까?’ 라는 생각부터 ‘난 왜 이런 생각을 안해봤지’란 반성도 했습니다. 사실 이런 것들이 문제가 될 것란 이야기를 들어보지 못한 것도 한 몫한 것 같습니다.

여러 고민 끝에 저는 프로젝트에서 Spring Security를 걷어내고 HandlerInterceptor기반으로 변경하였습니다. Filter와 HandlerInterceptor가 별 차이가 없어 보일 수 있지만, ServletFilter에서 수행하던 로직으로HandlerInterceptor으로 옮김으로 인해 Spring와 팀 Architecture에 구현되어 있는 여러가지가 자동으로 적용되도록 할 수 있었습니다.

사용자 로그인 필터에서는 계층을 무시하고 건너뛰고 있습니다. AD 접근 기능이 있는 Application Layer를 향하기도 하고 DB 접근을 위해 Data Access Layer로 가기도 합니다. 건너뛰게 된 이유를 생각해봤을 때 비즈니스 로직이 필터 내부에 있기 때문이었습니다.

“그러면 어느 부분을 비즈니스 로직이라고 할 수 있을까요?”

저는 LDAP을 이용해 AD 내에 접근하여 사용자 계정 정보를 검증하는 부분과 DB에 사용자를 조회하거나 사용자 정보를 저장하는 등의 행위가 접근제어 프로젝트에서 필요한 비즈니스 로직이라고 판단했습니다.

물론 Spring Security에서 인증 필터가 곧바로 DB에 접근하며 인증을 진행하기도 합니다. 하지만 필터 내부에서 예외가 발생하면 스프링의 도움으로 예외를 핸들링 할 수 없습니다.

이를 해결하기 위해서 추가적으로 예외 핸들링 필터를 개발해야 되는 추가적인 작업이 필요해집니다. 그리고 필터 내부에서 예외 발생 시에 팀 내 공통 기능으로 AOP를 이용한 로깅이 동작하지 않는 등 여러 가지 상황들이 생기게 되었습니다.

또한 팀에서 구성한 공통 기능이 적용되지 않는 것뿐만 아니라 계층 간의 규칙을 어기고 계층을 건너뛰는 행위는 점차 계층 구조를 망가트리기도 하였습니다. Spring Security에서 필터를 이용할 때 예외적으로 계층을 건너뛰어도 된다고 생각하게 되면서 사용자 권한 확인 필터에서도 계층을 건너뛰게 개발했고 점차 여러 예외 사항을 위한 필터들이 많아졌습니다. 이제 특정 기능에 대한 코드를 찾으려면 Application Layer 뿐만 아니라 Filter도 같이 확인해야 하며, 어떤 상황에서 Filter가 기능을 수행하는지 인지하고 있어야 합니다. 만약 Filter를 고려하지 않고 Application Layer에 기능을 개발하다 보면 예상하지 못할 오류가 발생할 수도 있게 되었던 것이었습니다.

결국 저는 HandlerInterceptor와 팀 자체에서 @Service계층에서 동작하도록 자체적으로 커스텀한 AOP기반 Filter 를 통해 이 문제를 해결할 수 있었습니다. ServletFilter에서 코드들을 HandlerInterceptor로 이관한 뒤로 눈에 띄게 복잡도를 낮출 수 있었고, Spring의 여러 기능들도 손쉽게 이용할 수 있어서 막상 변경하고 나니 추가개발 및 수정도 더 편리해진 것으로 저 스스로도 느낄 수 있었습니다.

이렇게 나머지 Servlet Filter들도 Application Layer의 팀 내 기능 필터로 수정하고 Spring Security를 완전히 제거하였습니다. 그 결과 그 많던 커스텀 Filter 클래스를 모두 제거하여 구조를 단순화 하고 계층 규칙을 지키면서 동료분들이 코드를 읽거나 흐름을 생각하실 때 훨씬 수월하게 진행할 수 있게 됐습니다.

정리해 봤을 때 Spring Security를 이용하면 팀에서 기존에 있던 기능을 이용하지 못하거나 중복해서 구현해야 하는 부분이 생겼습니다. Spring Security가 다양한 기능을 제공하여 편리하지만 현재 팀 아키텍처와는 맞지 않아 요구사항을 만족시키기 위해서 여러 커스텀이 필요했습니다. 그리고 ServletFilter가 아닌 HandlerInterceptor를 이용했을 때 복잡했던 커스텀과 팀에서 제공하고 있던 여러 기능을 재활용하여 보다 깔끔하게 해결할 수 있었습니다.

후기

처음에 프로젝트를 시작하면서 어떻게 진행하면 될지 감도 잡히지 않았는데, 무사히 프로젝트를 마칠 수 있었네요~ 개발하다 보면 순조롭게 진행되기도 하고 다시 처음부터 시작하기를 반복했는데요. 어떤 기능을 만들어야 할지 알아도 그것을 적절한 구조를 설계하고 개발한다는 게 쉬운 일이 아니었습니다…ㅠㅠ

그래도 이렇게 파일럿 프로젝트를 끝마칠 수 있었습니다. 접근제어가 어려운 작업이 아니라고 생각했지만, 여러 가지 상황을 겪으면서 배울게 많은 어려웠던 작업이었던거 같아요. (나만 어려워..?) 생각했던 일정보다 더 많은 시간이 필요했지만 접근 제어의 기반을 이해하고 개발할 수 있어서 너무 뿌듯하고 기분 좋았습니다.

제가 아직 말주변이 좋지 못해 이상한 말을 해도, 지저분한 코드를 보여드려도 언제나 본인의 일처럼 피드백을 주시는 동료들 덕분에 짧은 기간 동안 많이 성장할 수 있었습니다. 이 자리를 빌어 코드 리뷰를 진행해 주시고, 계속해서 피드백을 주시면서 제가 해낼 수 있게 도와주신 저희 금융사업팀 동료들께 감사드립니다. 추가로 이 글을 끝까지 읽어주신 많은 엔지니어 분들께도 감사드립니다.

다음에는 또 다른 실패 극복기로 찾아뵐게요 😀

👍 감사합니다 👍







0 0

월간 인기글