SSAFY 2학기 첫 번째 프로젝트 회고록
들어가며
Disclaimer
-
Last Update: 2024/08/19
- SSAFY는 2학기 첫 프로젝트에 공통 프로젝트라는 이름이 붙였는데요. 이번 글은 해당 과정이 아니라 프로젝트에서 느낀점을 위주로 서술된 회고록입니다.
- SSAFY 과정 내부에서만 알 수 있는 내용은 최대한 서술하지 않았습니다.
- 겪고 얻은 것들과 아쉬운 점에 대해서 기술하는 글입니다. (꽤 깁니다.)
겪고 얻다
갑작스러운 Spring Security 기존 코드 개선
겪다
-
배경
팀원들의 취업으로 인한 인원 부족으로, 기획 주차에 기존 팀이 해체되었습니다. 상세히 밝히기는 어렵지만, 팀장으로서 (저를 포함한) 남은 인원들이 정말로 필요로 하는 곳에 갈 수 있도록 적극적으로 행동했습니다. 다행히도 좋은 결과를 얻었습니다.
-
업무
기존 팀에서 학습하고 코드를 작성하던 채팅 기능 대신, 새로이 합류한 팀에서는 우선 Spring Security를 이용해 작성된 기존 회원 관리 코드 개선을 맡게 되었습니다.
얻다
-
레거시 코드를 대하는 태도
레거시 코드를 정의하는 여러 방식이 있지만, 저는 일방적인 대화가 이루어지는 코드로 정의하고 싶습니다. 당연하게도, 언제나 말을 거는 사람은 레거시 코드를 읽는 사람입니다.
SSAFY나 여타 직업 훈련 기관에서는 쉽게 겪을 수 없지만, 실제로는 빈번한 일 중 하나가 레거시 코드를 개선하는 일이라고 생각합니다. 비록 완전히 갈아엎는 정도는 아니더라도요.
이번에 맡은 Spring Security는 저도 처음 이용해보기도 하지만, 이전에 작성하시고 SSAFY를 취업 퇴소하신 분께서도 처음 이용해보셨던 프레임워크입니다.
저는 팀원과 대화하면서, 기존 코드가 일단 인증 및 인가 흐름을 위해 작성되고 설정되었음을 파악하게 되었습니다. 그 덕에 흐름은 유지하되 잘못된 설정 등 미진한 부분만 빠르게 고쳐 회원 기능을 개선할 수 있었습니다.
기존 코드를 개선하기 전에는 프로젝트 내에서의 중요도를 살펴볼 필요가 있다는 것을 실감할 수 있었는데요. 이는 당연히 현재 팀원들과의 대화와 함께, 기존에 개발자가 코드를 왜 그렇게 작성해야 했을까? 라는 질문에 대한 고민이 필요하다는 뜻이기도 합니다.
-
Spring Security 이해: 5와 6에서의 차이 포함
정확히는 Spring Security 5버전 이후, 5.4 버전 ~ 6 버전에서의 차이라고 봐도 되겠습니다. 전반적인 인증이나 인가 흐름에 대해서는 따로 글로 작성할 것이므로, 이번에는 간단하게 소개해보겠습니다.
우선, Spring Security 5.4부터는 기존에 Config 설정을 위해 이용하던
WebSecurityConfigurerAdapter
가 deprecated되었습니다. 대신SecurityFilterChain
빈을 직접 등록하고 띄우는 방식을 이용합니다.또한, 보안 정책을 구성하는
HttpSecurity
에서 메서드 체이닝이 개선되고, CSRF 및 CORS도 더욱 직관적으로 이용할 수 있도록 변화했습니다.인가 과정에서도
AccessDecisionManager
및AccessDecisionVoter
대신AuthorizationManager
가 이용되면서 유연성이 더욱 강화되었습니다.게다가 Spring Security를 다루며 Spring MVC부터의 기본 개념인 Filter는 물론, CORS(Cross-Origin Resource Sharing)과 같이 통신과 인증에 대한 지식도 쌓을 수 있었습니다.
테스트 코드 작성 with Spring Security
겪다
-
배경
기존 회원 관리 코드를 따라, 개선 후에도 로그인은 Controller이 아니라 Filter 단에서 처리가 되었습니다. 하지만 프론트에서도 Swagger를 이용할 수 없었으며, 채택한 git branch 전략이 github flow 였기도 해 미진하게 나마 테스트 코드를 작성하고자 했습니다. 비록 처음 테스트 코드를 작성해보지만, 좋은 기회로 받아들였습니다.
-
업무
로그인 테스트에서는 MockMVC를 활용해 웹 계층에서의 HTTP 요청과 응답을 모킹하며 로그인 기능을 검증했습니다. 또한, JUnit과 Mockito 프레임워크를 학습하고, 단위 테스트를 위해
@MockBean
어노테이션 및when().thenReturn()
를 이용할 수 있었습니다.
얻다
-
Spring Security 의존성 이해 및 테스트 의의
Spring Security를 적용한 테스트 코드를 작성하며, Spring Security 내부의 의존성을 확실하게 이해할 수 있었습니다. 특히,
AuthenticationManager
가 어떻게UserDetailsService
를 이용하는가에 대해서나,Authentication
인터페이스의 역할과 전달에 대해서도 말입니다. 이는 제가 Spring Security를 이용하면서 단위 테스트를 진행하려고 노력했기 때문이기도 합니다.또한, 단위 테스트 코드에서 나오는 결과를 통해 현재 코드에서 어떤 점이 잘못되었는지 살펴볼 수 있는 계기가 되기도 했습니다. 대표적으로 저는 테스트 코드를 작성한 덕에,
CustomUserDetailsService
가 이용되지 않음을 확인할 수 있었습니다.동시에 테스트 코드는 기존 코드가 얼마나 잘 작성되어 있는지 확인하는 기회라고도 느꼈습니다. 비록 이번에는 크게 경험하지 못했지만, 단위 테스트임에도 너무 많은 의존성을 주입해야하는 경우도 생각해볼 수 있겠습니다. 이런 경우에는 기존 메서드를 나누어야 할 지도 모릅니다.
-
테스트 및 관련 프레임워크 이해
Spring Security에서 어떻게 단위 테스트를 진행할 수 있을까 고민하면서, 통합 테스트와의 차이를 경험할 수 있었습니다. 특히 검색 결과로 나오는 Spring Security를 이용한 테스트는 대부분 통합 테스트였습니다. 심지어는 단위 테스트를 원하는 stackoverflow 질문에도 그냥 통합 테스트를 진행하라는 답변이나 댓글이 달리기도 했었는데요. 통합 테스트가 아니라 단위 테스트를 작성하면서, 저는 확실히 의존성이 단위 테스트에서 차지하는 위치를 알 수 있었습니다.
이는 곧 JUnit이나 Mockito와 같은 프레임워크가 등장하게 된 배경이나, 프레임워크 내에서 자주 쓰이는 메서드들이 왜 중요한지 돌이켜보는 계기가 되기도 했습니다.
verify()
를 통해서 스터빙된 메서드에 대해 검증하거나,MockMVC
를 활용해 HTTP 요청 및 응답 자체를 모킹하거나,@MockBean
이나@Mock
과 같은 Mockito 어노테이션, JUnit에서 왜@Test
어노테이션을 이용하는지를 알 수 있었습니다. Spring Security와 관련해서도@WithMockUser
는 물론, 이에서 나아가 아예@WithCustomMockUser
어노테이션을 직접 작성해보기도 했습니다.물론 테스트 프레임워크를 처음 이용해보았기에 앞으로 학습해 나가야 할 점이 많습니다. 그럼에도 의존성이 복잡한 Spring Security에 기반한 테스트 코드를 작성하며 Spring Security는 물론 테스트 코드에 관해서도 학습할 수 있는 기회였습니다.
Redis 및 RabbitMQ 등 외부 서비스 이해
겪다
-
배경
이번 프로젝트에서는 여러 외부 서비스가 사용되었는데요. Redis는 refresh 토큰을 관리하기 위한 사용자 데이터 캐싱이나, 게시글 좋아요나 조회수 등의 쓰기 작업에 대한 배치 처리를 위해 이용했습니다. RabbitMQ는 직접 작성하지는 않았지만, 서비스 내 라이브 코딩 기능에서 응시자와 면접관 사이의 코드 공유 등 실시간 데이터 처리에 쓰였습니다.
비록 프로젝트 일정 상 외부 서비스보다 인프라에 우선 순위를 두긴 했지만, 다양한 외부 서비스의 동작과 설정을 이해해야 배포를 진행할 수 있는 상황이기도 했습니다.
-
업무
우선적으로 Redis에 관한 학습과 조사를 중점을 두고 진행했습니다. 이는 기존 회원 관리 코드에서도 Redis가 쓰였기 때문입니다. 이후 Redis의 캐싱 전략, 지연 처리, 자료 구조, 데이터 영속화와 장애 복구 등을 폭 넓게 학습하며 전략을 세울 수 있었는데요. 그러나 일단 서비스 배포를 우선순위로 두고, 이후 Redis 전략을 심화시켜 보자는 판단에 학습한 내용을 많이 구현하지 못했습니다.
얻다
-
Redis와 그 설계 전략에 대한 기본적인 이해
Redis 공식 문서와 기타 참고 자료를 읽으면서, 인메모리 데이터베이스로서의 Redis가 얼마나 많은 방면에서 활용할 수 있는지를 우선 파악했습니다. 특히, 반복적이거나 악의적인 게시글 조회나 좋아요를 통해 DB에 쓰기가 자주 발생하면 부하가 발생할 수 있다는 점이 문제적이었습니다. 따라서 Redis에만 반영하고, 특정 시간 간격을 두고 배치 처리를 통해 DB에 쓰기 연산을 진행하도록 설계했습니다. 게다가 게시글 조회수나 좋아요 데이터는 서비스의 핵심 기능이 아니면서도 정확한 반영이 중요하지 않았으므로, Redis에서 지원하는 Hyperloglog 자료구조를 이용했습니다.
또한, refresh 토큰을 관리함에 있어서도 Redis를 이용하여 빠른 접근 속도와 추후의 스케일링을 고려할 수 있었습니다. 이는 인증 토큰이 사용자 간에는 공유되지 않는 데이터이자, 상태를 유지할 필요가 적으며, TTL(Time-to-Live)를 통해 쉽게 관리할 수 있다는 장점을 살린 전략이었습니다. 특히 안전하게 로그아웃해 토큰 재사용을 막으면서도, 특정 토큰을 차단할 필요가 없다고 판단했기에 Refresh Token Rotation 전략을 이용했습니다.
추후 아쉬운 점을 이야기하면서 말하겠지만, 사실 ‘기본적인 이해’라고 쓴 것은 우선순위에 밀려 메모리 관리나 여타 전략을 시행해보지 못했던 까닭이 큽니다. 하지만 확실히 얻은 것은, 어떠한 기능을 작성하거나 데이터를 다룰 때 중요한 것은 기술 자체가 아니라 서비스 측면에서 바라보아야 한다는 점이겠습니다.
Infra 담당하기: Jenkins, CI/CD, AWS EC2, Docker, NginX
겪다
-
배경
프로젝트 마무리 2주를 남기고, 프론트엔드와 백엔드 모두에서 핵심 기능들이 완성되어 감에 따라 급하게나마 인프라에 대한 학습과 설계, 배포를 진행했습니다. 새로 학습해야 할 기술도 많고, 처음으로 배포까지 진행해야 하는 상황이었습니다. 다만 Linux의 기초적인 파일 시스템이나 Shell Script는 완전히 문외한인 편은 아니었으므로, 러닝 커브 자체가 아주 크지만은 않았습니다.
-
업무
배포 과정에 필요한 모든 업무를 도맡아 했습니다. Jenkins-Gitlab 연동, Jenkins & Shell Script를 이용한 docker-compose 이미지 생성 및 CI/CD, 기초적인 AWS EC2 관리와 환경 변수 설정, 배포 환경 분리를 위한 docker-compose 및 설정 파일 작성, 리버스 프록시 이용을 위한 NginX 설정 작성, SSL 발급 및 등을 경험해보았습니다.
얻다
-
전반적인 배포 과정과 환경에 대한 이해
사실 SSAFY 내에서 진행하는 프로젝트만으로, 다수의 서비스 이용자를 가진 환경을 체험하기는 어렵습니다. 하지만 위와 같은 업무를 맡게 되면서, 실제 배포 환경과 개발 환경을 분리하면서 다양한 오류를 맞닥뜨렸는데요. 예를 들면 다음과 같은 상황도 있었습니다.
docker에 띄워진 컨테이너를 이용함에 있어서 host를
127.0.0.1
로 두어서 연동 오류를 해결했습니다. 우선, docker-compose를 개발 환경에서 작성해 준 동료가 있어 참고한 덕에 그나마 시행착오를 줄일 수 있었음을 밝히고 싶습니다. 문제는 MySQL과 Spring Boot 어플리케이션을 연동하며, 베포용 application-prod.yaml에서 jdbcURl을 docker 컨테이너 이름인 db로 뒀더니 인식하지 못했다는 오류가 발생했습니다.이는 EC2 인스턴스에서 바로 Spring Boot 서버는 띄우는 반면, MySQL이나 Redis 등은 docker 컨테이너 이미지로 띄워질 때 배포 환경에서의 차이를 잘 이해하지 못했던 까닭입니다. docker는 기본적으로 호스트 시스템과 분리된 네임스페이스에서 자체적인 가상 인터페이스와 IP 주소를 가지는데요. 그래서 실제로는 docker 브리지 네트워크 내에서
172.x.x.x
와 같은 프라이빗 IP를 할당받습니다. 대신, 도커는 NAT(Network Address Translation)을 이용해 호스트 네트워크와의 트래픽을 관리해127.0.0.1:port
로 들어오는 요청을 컨테이너의172.x.x.x:port
로 매핑해 전달합니다.처음부터 배포 환경을 고려하지 못해 발생한 비효율도 있습니다. 우선, 단일 인스턴스 환경이기에 NginX를 로드 밸런서로는 따로 이용하지 않았음을 밝힙니다. 대신 NginX를 리버스 프록시로 이용하면서 어플리케이션 서버와의 분리 및 URL 리다이렉트/리라이트를 중점으로 이용했는데요. 클라이언트의 요청을 우선적으로 NginX에서 받으며, 포트 번호를 숨기고 백엔드 서버를 외부 네트워크에서 분리할 수 있었습니다. 하지만, 포트 번호를 제외하고 서비스를 제공하려는 과정에서 클라이언트 요청 경로와 서버 요청 경로가 겹치는 상황이 발생해 추후 백엔드 요청 경로에
/api/v1/
을 접두사로 추가해야만 했습니다.이렇듯 Infra 업무를 맡으면서 저는 배포 과정과 환경에 대해서 기본적인 이해도가 생겼다고 자신할 수 있게 되었습니다. 비록 트러블 슈팅이 아니더라도, 배포 환경에서 쓰이는 개념과 관련 원리에 대해 주기적으로 작성해보겠습니다.
-
NginX와 Websocket: 네트워크 지식의 필요성
NginX는 인프라 업무를 맡으며 가장 재밌었으면서도 세부적으로는 다루지는 못한 기술 중 하나인데요. 특히 기억에 남는 것은 NginX를 통해 클라이언트에서의 웹소켓 요청을 서버로 전달하는 일이었습니다. 개발 환경과 다르게 Kurento에서 이용되는 WebSocket이나, STOMP 프로토콜에서 이용되는 WebSocket 요청이 전혀 서버로 도달하지 않았는데요. 그 때문에 클라이언트 측에서 웹소켓이 항상 빠르게 닫히는 오류가 발생했습니다.
일차적으로는 NginX에 웹소켓 통신 관련 요청에서는 만료 시간도 지정하고, Upgrade 헤더와 Connection 헤더 설정을 통해 HTTP Upgrade 역시 진행했는데요. 물론, 오류는 여전히 해결되지 않았습니다. 문제는 WebSocket 요청이 HTTP Upgrade를 거친다는 점에도 불구하고, 관련된 경로가 CORS 설정과 리버스 프록시 설정에서 누락되어 있었다는 점이었습니다. CORS에 대해 Sprign Security를 거치며 사전 학습해두었기 때문에, 다행히 해결할 수 있었던 오류였습니다.
사실 오류보다도, Infra 업무를 맡으며 어떻게든 서비스를 띄워야 하는 동시에 여러 프로토콜을 필수로 학습해야 했던 상황이 더욱 기억에 남습니다. 이전에 살펴보았던 docker 오류 경험은 물론, 웹소켓의 HTTP Upgrade를 단순히 한 줄의 코드가 아니라 기반 지식으로서 마주하면서 오히려 네트워크 지식이 얼마나 중요한지를 깨닫는 시간을 가질 수 있었습니다.
-
로그에 대한 관심 및 중요성
오류를 파악하기 위해 도커 내에서든 EC2 환경 내에서든 빈번하게 로그를 살펴보았는데요. 이는 개발 과정에서 로그가 얼마나 중요하고, 로그가 어떻게 쓰이는지에 대한 관심이 생기는 계기가 되기도 했습니다. 로그는 문제 발생 시에 가장 처음 참고할 수 있으면서도, 성능이나 보안 모니터링에서도 중요한 역할을 합니다. 물론, 민감한 정보나 로그 레벨에 있어서는 적절하게 다루어야 하겠지만요.
한 마디로, Infra 업무를 맡은 덕분에 개발 과정에서 로그가 차지하는 위치와 중요성에서 관심이 생겼다고 할 수 있겠습니다.
돌이켜보다
아쉬운 점
-
시큐어 코딩과 보안
솔팅이나 환경 변수 처리, 입력값 검증, 코드 인젝션 방지 등을 통해 시큐어 코딩 혹은 보안 측면에서 더 섬세한 프로젝트가 되었으면 좋겠습니다. 특히, 이번 프로젝트에서는 급하게 서비스를 띄운다고 포기했던 부분이 많습니다.
Spring Boot 배포용 설정을 위해 환경 변수와 관련해서도 dotenv 라이브러리를 이용했으나, dotenv 라이브러리도 충분히 취약점이 발생할 수 있다고 생각합니다. 또한,
ProcessBuilder
클래스를 이용했던 라이브 코딩 관련 코드 컴파일 및 제출 기능에서도 취약점이 있음을 인지했으나 대처하지 못했습니다.다음 프로젝트에서는 더욱 섬세하게 접근해, 취약점을 먼저 고려하고 대처하는 기회를 가져보고 싶습니다. 개인적인 욕심으로는, 처리율 제한 장치나 로그와 관련된 정비에 관해 깊게 다루어보고 싶기도 합니다.
-
JPA에 대한 심화적인 이해
JPA에서 바로 DB로 영속화를 진행하는 대신, EntityManager를 이용해 시점에 대한 고려를 더 진행해봐도 좋을 것 같았습니다. 동시에 이는 곧 꼭 JPA를 써야하는가? 바로 영속화를 진행한다면 SQL Mapper나 JDBC Template을 이용해도 되지 않을까? 하는 고민에서 출발해도 좋겠습니다. 언제나 은탄환은 없으니까요.
게다가 서비스 측면에 대한 고려와 함께 JPA가 제공하는 컨텍스트를 이해하고 어떻게 1차 캐시나 2차 캐시가 잘 이용될 수 있는지를 살펴볼 수 있는 기회를 가져봐도 좋겠습니다.
-
Redis 설계 전략 실행, 메모리 관리 및 모니터링
다음에는 페일 오버를 고려한 Redis 설계 전략을 짜보려고 합니다. 물론, Redis도 서비스에서 필요해야만 채택되겠지만요. Redis의 대체재인 오픈 소스를 고려해도 좋겠습니다. 그러나 무엇보다도 인메모리 데이터베이스에서 중요한 점은 메모리 관리와 모니터링이라고 생각합니다. 아무리 레디스와 관련된 코드를 작성했다고 하더라도, 실제 메모리 관리와 모니터링 없이는 반쪽 짜리라고 생각합니다.
특히 Redis는 O(N)에 해당하는 실행문에서 위험 요소가 존재하므로, 서비스 차원에서 SPOF(Single Point of Failure)가 되기 쉽다고 생각합니다. 따라서 백업 전략과 페일 오버 관리도 중요하다고 생각합니다. Redis Centinel 등이 기술 부채를 남기는 오버 테크놀로지는 아닌지 고려해 보는 것도 필수입니다.
-
아키텍처 설계와 외부 서비스 이해
기획 이후 코드 작성 단계가 진행되고 있는 팀이어서, 따로 아키텍처 설계에 참여할 시간은 없었습니다. 하지만 다음 프로젝트에서는 각 외부 서비스의 장단점과 서비스 차원에서의 도입 필요성을 이해하고, 효율적이면서도 장애를 고려하는 아키텍처를 고민해보려고 합니다. 물론 세부적인 전략도 팀원들과 함께 논의하고자 합니다.
Infra 업무를 맡아보니, 아키텍처가 배포 과정에서의 문제, 나아가 포트 전략이나 프록시 서버까지도 고려해야 배포 과정에서의 문제점이 줄어들 것이라는 생각이 들었던 까닭이 큽니다.
-
CI/CD와 테스트 코드 작성
짧은 시간 속에서, Spring Security를 적용한 상황에서의 간단한 단위 테스트 코드를 작성한 것만으로도 충분히 좋은 경험이었습니다. 하지만 프로젝트가 github flow를 채택했다면, 저는 테스트 코드에 조금 더 신경을 썼어야 했다는 아쉬움이 남습니다. 이는 사실 기획과 기능이 일정 내 팀원들의 능력을 고려하지 못했기 때문이기도 합니다.
하지만 저는 CI/CD가 (특히 github flow를 이용 중이라면) 테스트 코드를 중심으로 이루어질 필요가 있다고 생각합니다. 특히 처음 프로젝트 코드를 작성하는 과정을 넘어, 배포를 고려하는 과정에서는 더욱 그러합니다. TDD(Test Driven Development)를 진행하자는 뜻은 아닙니다. 특히 TDD에서는 시간 소모나 테스트를 위한 설계, 숙련도 차이로 인한 품질 문제가 발생할 수 있으니까요. 이 모두는 SSAFY 프로젝트에서 달성하기 어려운 과업이기도 합니다.
대신 저는 테스트를 통해 사전에 방지할 수 있는 에러 탐색과 디버깅 시간 감소, 안정적인 배포라는 측면에서 테스트 코드가 필요함을 소구해보려고 합니다. 특히 이는 git flow처럼 release 브랜치가 따로 존재하지 않는 github flow나 trunk-based development라면 그 중요성이 더욱 커진다고 믿습니다.
팀원들에게
새로 들어왔음에도, 믿고 여러 업무를 맡겨 준 모든 팀원에게 감사의 말을 전하고 싶습니다. 비록 짧은 시간이라 완벽히 해내지 못한 업무들도 많지만, 이번 프로젝트에서는 “어떻게든 해냈다”와 함께 “어떻게든 띄워냈다”라는 점에서 모두에게 감사함과 칭찬을 전하고 싶습니다. 특히 급한 일정에서도 발표 자료와 UCC를 만들어 주신 팀원에게도 다시 한 번 감사의 말씀을 드립니다.
모두 이번 프로젝트를 자양분 삼아, 다음 프로젝트에서는 더 완성도 있는 기획과 기능을 달성해 더욱 큰 뿌듯함을 느낄 수 있도록 바라겠습니다! 저 역시 Backend와 Infra는 물론, Frontend 동료들의 API 통신이나 JS 로직과 관련해서 디버깅을 함께 진행하며 많은 것을 배웠습니다!