지난 3주간 시나리오 기반 서버 구축을 진행했다.
https://github.com/mybloom/hh_concert_java
대기열 관련 백엔드 시스템 설계를 진행하고 변경에 유연한 코드를 위해 아키텍처와 테스트코드 작성하는 것을 배웠다.
그리고 운영을 위해 필요한 로깅과 동시성 처리도 진행했다.
가장 긴 시간을 할애한 것은 설계와 비즈니스 로직 작성이었다.
그러면서 이 시간에 변경에 유연하고 유지보수를 고려한 코드 작성하는 법을 익히고 싶었다.
그러기 위해서는 아키텍쳐 지식도 있어야 하고 좋은 코드를 많이 보면서 지식을 어떻게 코드화하는지도 익혀야했다.
1시간 남짓 설명을 통해 무엇을 공부해야하는지 파악하고 실제로 더 필요한 지식들은 스스로 익혀야 했었다.
나는 만들면서 배우는 아키텍처라는 책이 도움이 많이 되었고, 실제 코드화하는 작업은 QNA와 멘토링 시간에 영감을 받고 다른 사람들의 코드를 통해 배울 수 있었다.
공부하면서 기억에 남았던 것들을 간단히 정리해봅니다.
테스트코드를 이용한 개발
이번에 토이 프로젝트를 진행하면서 느낀 점은 동시성 테스트를 간단히 시도해보는 것만으로도 잘 알지 못하고 사용했거나 미처 생각하지 못했던 부분들을 알아갈 수 있다는 것이었다.
내 경우엔 DB에서 findById().orElseGet(makeEnitity) 같은 식의 작업을 했을 때 매번 한 번이 실패하여 코드에서 발생할 수 있는 문제를 찾을 수 있었다.
그리고 아래를 중점으로 테스트 코드를 작성했다. 2, 3번은 조금 섞어가면서 작성했다.
- 가장 먼저 컨트롤러 통합테스틀 만들어 API 요청/응답이 오는지 확인하는 코드를 작성
- 챙겨야 하는 비즈니스 로직이 있는 서비스의 통합테스트, 단위테스트를 작성
- 도메인 단위 테스트
- 동시성 테스트
아래는 각 레이어의 역할과 수행해야 할 테스트 코드를 정리한 내용입니다.
컨트롤러
역할
- 권한 검사
- 입력유효성 검사
- 입력을 유스케이스 입력 모델로 매핑
- 유스케이스 호출
- 유스케이스 출력을 HTTP로 매핑
- HTTP 응답을 반환
수행한 테스트
- WebMvcTest 단위테스트
- requestBody 입력유효성을 확인
- Controller만 로딩
- Service는 MockBean 으로 컨트롤러 로직 수행
- SpringBootTest와 MockMvc를 이용한 통합테스트 진행
- API의 요청과 응답을 확인
컨트롤러 테스트 참고 자료
컨트롤러를 테스트하기 위해서는 @WebMvcTest에서 많은 기능을 제공해준다.
https://docs.spring.io/spring-boot/api/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTest.html
- @Valid 검증
- Filter나 Interceptor 검증
- 커스텀 헤더를 추가하는 로직이 있는 경우에도 테스트할 수 있다.
유스케이스
역할
- 입력 받는다
- 비즈니스 규칙을 검증한다.
- 모델 상태를 조작한다.
- 출력을 반환한다.
수행한 테스트
- SpringBootTest를 이용한 통합테스트를 진행
- 주요 비즈니스 로직이 있을 경우 해당 내용이 잘 수행되어야 하는 케이스와 실패하는 케이스로 나눠서 작성
- 예) 가입된 사용자라도 충전이력이 없으면 포인트 데이터가 없다.
- ExtentionWith를 이용해 Mockito를 확장해 수행하는 단위테스트 진행
- 주로 흐름 제어가 있는 로직에 해당 테스트 코드 작성
- 유스케이스/서비스 레이어는 도메인에 대한 오케스트레이션을 해주는 부분이라 그 부분이 잘 되었는지 확인하는 테스트코드를 작성한다고 한다. 하지만 흐름 제어가 있는 부분은 직접 코드를 보는 것이 파악에 더 빠를 수 있다고 한다.
- 통합테스트를 이용한 동시성 테스트
- DB와 로직이 함께 있는 레이어이기 때문에 동시성 이슈가 발생할 곳을 찾아서 진행한다.
- 자바에서 멀티스레딩 프로그램을 할 수 있는 인터페이스를 이용한다. (ExecutorService,CompletableFuture 등)
도메인 모델
- DB와 연관되어 Enitity로 사용하기도 하고 POJO로 따로 만들기도 한다.
- 외부 주입이 필요없는 순수한 단위테스트를 진행할 수 있다.
클린 아키텍처를 가미한 레이어드 아키텍처
아키텍처를 공부하면서 패키지구조에 모두들 관심을 갖게 되었다.
왜일까? 패키지 구조가 아키텍처를 반영할 수 없으면 시간이 지남에 따라 코드는 목표 아키텍처에서 멀어지게 된다고 한다.
룰을 정하고 그 룰을 잘 표현할 수 있는 패키지구조를 통해 파악하기 쉽고 유지보수하기 좋은 코드를 만들어나가는 것이 아닌가 한다.
토이 프로젝트에서는 클린 아키텍처를 가미한 레이어드 아키텍처로 진행했다.
클린 아키텍처를 가미했다고 생각한 부분은 영속성 레이어는 애플리케이션 레이어에서 의존하게 되는데
인터페이스를 통해 의존을 역전시켜 애플리케이션은 어떤 것도 의존하지 않는 상태를 만든 것이었다.
클린 아키텍쳐에 대해서 얇은 책에서 본 코드로는 부족하여 github 레포를 추천받았다.
레이어를 gradle 멀티모듈을 사용해서 분리했고, 의존성을 gradle 모듈 간의 import로 제한하는 구조라고 하셨다.
https://github.com/grant-burgess/clean-architecture-example-java-spring-boot
오! 신기하고 재밌었다. 네이밍 하는 방법도 엿볼 수 있고 어렵지 않게 볼 수 있다.
책에서는 패키지간의 의존성을 접근 제한자를 통해서 한다고 했는데 멀티 모듈을 통해 모듈간 import 제한하는 구조가 제한자 신경쓰지 않고 편하고 합리적으로 보였다.
토큰 대기열 설계
대기열을 설계한다는 것은 처음으로 해보는 것이였다.
재밌었고 많은 사람과 리뷰 시간을 가지면 좋을 것 같다.
유념했던 부분 위주로 작성해본다.
- 대기열은 하나로 이뤄져야 하나? 대기열과 활성 풀(pool)이 따로 존재하는 것이 낫지 않을까?
- 둘러보는 사용자 말고, 좌석을 예약하고 결제까지 가는 과정에서 유효시간을 더 추가해줘야 하는 데 어떤 방법이 좋을까?
유효시간을 부여하는 과정이 어뷰징의 요소가 되지는 않을까? - 대기 순서는 어떻게 판단하는 것이 빠르게 판단할 수 있을까?
- 대기열을 기다리는 토큰(사용자)을 어떤 과정으로 통과하게 할까?
- 대기열 상태에 따른 관리는 어떻게 할까? 어떤 상태를 활성화 상태로 정의할까?
- 대기열 토큰의 만료시간은 생성했을 때와 활성 상태가 되었을 때 2개로 분리되는 것 같은데 어떻게 관리하면 좋을까?
위와 같은 고민들을 했고 혼자 고민도하고 여러 사람의 의견과 멘토님들의 이야기를 들으면서 하나씩 알아가게 되고 나름의 기준을 정하게 되었다.
그리고 백엔드 개발자로서 기준에 대한 것은 크게 2종류가 있다고 본다.
- 자원의 사용
- 빠른 처리
그런 관점에서 토큰의 만료시간에 대한 관리는 우리가 가진 서버의 자원에 따라 얼만큼 저장하고 있을지 판단해 볼 수 있다.
그리고 사용자가 대기열의 상태를 확인하는 방법은 쉽게 구현할 수 있도록 polling을 사용한다고 하는데 대기순서 파악과 활성 상태가 되었는지를 빠르게 처리해줄 수 있어야 한다.
나의 경우엔 대기순서를 마지막 활성 토큰의 offset을 이용해서 판단하도록 했는데 이 방법은 kafka같은 곳에서도 사용하는 방법이라고 하셨다.
예매 시스템 설계
그외로 예매 시스템 설계에서 스케줄과 좌석을 연결하는 방법이 사람마다 다양하게 설계하는 것을 볼 수 있었다.
좌석을 공간과 연결지어 같은 좌석이 추가되지 않는 마스터 테이블로 생각하거나,
스케쥴마다 연결되는 추가 되는 개념으로 보거나 둘 중의 하나 같았다.
나는 처음에는 전자로 설계했다가 개발을 빠르게 해야 하는 상황에서 후자로 변경했다.
그런데 실제는 마스터 테이블의 개념이고, 각 공연마다 달라지는 것은 RDB형태가 아니라 document 형태로 관리 될수도 있을 것 같았다.
여튼 이 과정에서 재미를 느꼈던 부분은 다양한 사람들의 생각을 볼 수 있었던 부분이고, 설계보다는 빠른 개발에 초점을 맞추면 다르게 판단할 수 있다는 것이였다.
3주간 고민이 많았지만 질문을 논리적으로 기술적으로 정리하지 못하는 경우도 있었는데 친절히 많은 것들을 알려주신 모든 분들께 감사함을 느낀다.
댓글