5/25일 오프라인에서 진행된 "2024 Spring Camp"를 듣고 정리한 글입니다.
한국 스프링 사용자 모임(KSUG)에서 주최하는 스프링 컨퍼런스, 스프링 캠프에 다녀왔다.
인프런에서 티켓이 오픈되었고, 오픈채팅방 등에서 반응이 핫한걸 보고 서버시간 타임시커를 이용해 오픈 정각에 티켓팅을 달렸다 🏃 그렇게 구매한 소중한 티켓으로 다녀온 스프링캠프에서 특히 인상 깊었던 두 세션을 간단하게 정리해둔다.
(여담이지만 오픈 후 4-50초만에 티켓이 매진되더라..! 💸 스프링이나 자바/코틀린 행사가 더 많아졌으면 한다 ^.ㅠ)
1️⃣ 조성아님의 왜 나는 테스트를 작성하기 싫을까
테스트의 중요성이나 효과에 대해 알고 있음에도, 테스트를 짜는 건 굉장히 힘들고 피곤한 것처럼 느껴진다. 테스트는 어쩌다 피곤한 행위가 된 걸까?
우리는 테스트도 결국 코드고, 따라서 유지보수가 필요하다는 사실을 놓치고 있다. 테스트의 이득과 비용에 대한 고려 없이, 무조건 테스트 커버리지를 높이는 것은 당장은 안전해보여도 장기적으로 봤을 땐 비효율적일 수 있다.
개발을 할 때 트레이드 오프를 고려하는 것처럼, 테스트에서도 선택과 집중이 필요하다. 테스트를 짜는데 필요한 비용이 테스트로 인한 이득보다 클 때, 당연히 테스트에 대한 효과도 적어 보이고, 자연스레 피곤한 일이라고 느끼게 될 것이다.
그럼 테스트를 짤 때 고려해야 하는 비용은 무엇이 있을까? 연사자분은 아래 두 가지 비용을 소개해주셨다.
- 작성 비용 : 말 그대로 테스트를 작성하는데 드는 비용 (e.g., 테스트 범위, 난이도 등)
- 유지보수 비용 : 이 테스트를 계속 유지보수하는데 드는 비용 (e.g., 특정 도메인이 변경되었을 때, 테스트를 얼마나 수정해야 하는가?)
반대로 테스트로 얻는 이득은 확장 비용을 소개해주셨다.
- 확장 비용 : 기능을 확장하기 쉬워진다. (e.g., 기능을 수정해도 모든 테스트에 통과한다면 안심하고 배포를 한다던지..)
단, 테스트의 이득 (확장 비용)을 늘리기 위해선 구조적 차원의 코드 개선이 필요하기에 적지 않은 리소스가 필요하다. 따라서 테스트에 많은 기대를 할 수록 비용이 된다는 관점에서, 테스트 비용을 줄이는 시도를 해 볼 수 있다. 위에서 언급한 두 비용을 코드 레벨로 내려서 제거할 수 있는 비용에 무엇이 있는지 알아보자.
- given 절
- 작성하는 로직에 어떤 요청이 필요한지 작성하는 과정
- 굳이 필요하지 않은데 요청을 구체화하는 등, 테스트를 복잡하게 작성할 때 작성 비용이 증가한다.
- then 절
- 작성하는 로직에 어떤 결과가 나와야 하는지 작성하는 과정
- 하나의 테스트에서 많은 검증을 하려 할 때, 해당 테스트의 변경 포인트가 많아지므로 유지보수 비용이 증가한다.
즉, 최대한 간단한 경우의 수를 (given) 최대한 적게 검증 (then)하여 필요한 것만 테스트한다면 효과적으로 테스트 비용을 줄일 수 있다. 이는 단순히 테스트 비용을 낮추는 것에서 그치지 않고, 읽기 쉬운 테스트를 만들어 주는 효과도 있다. 컴팩트하게 작성된 테스트 코드는 그 자체로 기능 명세서의 역할을 해 줄 수 있으며, 굳이 코드를 실행하지 않아도 결과를 짐작할 수 있기에 협업하는 입장에서도 편리할 것이다. 테스트를 짜기 전, 이 테스트에서 제일 중요한게 무엇인지 생각해보고, 프로젝트와 나에게 적합한 비용을 조율해보자.
연사자분은 테스트 비용을 낮추는 방법 중 하나로 Fixture Monkey라는 오픈소스 라이브러리를 소개해주셨다. Fixture Monkey는 복잡한 테스트 객체를 자동으로 생성해주고, 랜덤 값을 세팅해주는 라이브러리로 코틀린과 자바 언어를 지원하고 있다. 객체와 인터페이스를 유연하게 생성할 수 있고, 플러그인을 통해 다양한 엔진을 사용할 수 있다고 하니 찾아보고 프로젝트에 도입하는 것도 좋을 것 같다.
- Fixture Monkey github : https://github.com/naver/fixture-monkey
2️⃣ 김용욱님의 실전! MSA 개발 가이드
요즘은 왠만한 규모 있는 서비스는 대부분 MSA 구조로 구성되어 있는 것 같다. 단순히 생각하면 그냥 서비스를 쪼갠거 아닌가 싶지만, 실제론 MSA 구조를 만들기 위해 모놀리틱 구조에선 없었던 수많은 문제를 마주해야 한다. DB를 분리하면서 생기는 문제들과, 해결 방법에 대해 알아보자.
API를 통해 데이터에 접근해도 속도가 괜찮을까?
데이터는 참조 빈도와 변경 빈도에 따라 아래 4종류로 분류할 수 있다.
결국 문제가 되는 건 참조 빈도가 높은 데이터다.
자주 참조하는데 자주 변하는 데이터는 다루기 까다롭다. 하지만 이런 유형의 데이터는 많지 않고, 대부분 잘 알려진 분야이기에 솔루션도 많다. 예를 들면 세션! 타임아웃 정보 등을 API가 호출 될 때마다 확인해야 하는 까다로운 아이지만, JWT 등을 이용해 대체할 수 있다.
자주 참조하는데 자주 변하지 않는 데이터는 매번 조회하면 (API를 호출하면) 확실히 비용이 크다. 하지만 튜닝할 여지도 많다. 연사자분이 소개해주신 튜닝 방식 4가지는 다음과 같다.
- 데이터 복제
- 공통 서비스에서 업무 서비스에서 필요한 데이터만 복제하여 끌어가는 방식.
- 업무 서비스 DB에 필요한 데이터가 있으니, API를 호출할 필요가 없다.
- 모델링 변경
- 각 업무에서 사용하는 속성은 서비스 내에, 모든 속성에서 사용하는 속성은 공통 서비스에 두는 방식.
- 서비스를 한 번 더 나누기에 데이터 관리에도 유용하고, 장애 대응에도 유리하다.
- 일괄 조회
- N+1을 방지하기 위해, 부가 속성에 있는 FK를 모아 하나의 API에서 필요한 모든 데이터를 조회하는 방식.
- 병렬 조회와 비슷해 보이지만, 병렬 조회는 순간적으로 큰 부하가 생길 수 있다는 점에서 안티패턴이다. 중요한 것은 실제 API call 횟수를 줄이는 것이라는 걸 기억하자.
- 로컬 캐시 사용
- 서버가 실행 될 때 한 번 로딩해서 메모리에 데이터를 올려두는 방식. 자바는 Ehcache 등을 사용할 수 있다.
- 네트워크 I/O와 디스크 I/O를 모두 줄여주기에 성능 향상에 큰 도움이 된다.
- 단 용량이 많아질 수록 GC에 불리하므로 적절한 양을 조율하는 것이 필요하다.
- 또한 서버 인스턴스가 많아질 수록, 필요한 데이터가 많아질 수록 데이터 동기화로 인한 비용이 커진다 (순단 등). 로컬 캐시는 Redis와 같은 글로벌 캐시보다 빠르지만, 데이터 동기화는 포기해야 한다는 문제점이 있다.
간단한 테스트를 통해 확인해보면, 네가지 방식의 성능은 다음의 순서로 좋다.
- 일괄 조회 + 로컬 캐시 > SQL join > 일괄 조회 > N+1
- 테스트에 사용된 코드 : https://github.com/wharup/microservices-example-sql-vs-api
트랜잭션 없이 데이터 정합성을 보장할 수 있을까?
API가 여러 서비스에 사용될 때, 네트워크 통신 구간에서 트랜잭션이 보장되지 않는다. 정확히는 ACID 중 원자성과 독립성을 보장할 수 없다. (일괄성은 애초에 어플리케이션의 영역이고, 지속성은 데이터 저장소의 특징이라 동일하게 유지된다.)
- 원자성이 보장되지 않기에 롤백 할 수 없다.
- 쓰기에 실패하면 커밋된 데이터를 API로 직접 삭제하고, 로컬에 생성된 데이터는 롤백하여 직접 처리해야 한다.
- 단, 삭제 API가 실패할 경우 데이터 정합성이 떨어지므로 API 재시도 또는 대사(대조 확인) 작업을 통해 정리해야 한다.
- API call에 실패했다면, 높은 확률로 대상 서버에 문제가 있어 통신이 안되는 상황이다. 따라서 재시도 횟수가 높아지면 스레드가 묶여 오히려 내 서비스가 요청을 못 받는 등으로 장애가 전파될 수 있다. retry 횟수를 적당히 설정해야 한다.
- 차라리 API를 재호출하기보다, 이벤트를 활용해 간접적으로 재시도하는 방식이 도움이 된다.
- 이벤트는 전달 보장이 되기에 트랜잭션을 편하게 보장할 수 있다.
- 단, 이벤트는 At least 1이기에 이벤트가 여러번 전달될 수 있다.
- 액션이 여러 번 발생해도 동일한 결과 나타나도록 (멱등성) 구현하는 것이 중요하다. 똑같은 데이터가 있으면 무시하거나 덮어쓰는 등의 처리를 해주어야 한다.
- 원자성을 보장하는 방법은 아래와 같이 분류할 수 있다.
- 긴 트랜잭션 나누기 : 실패해도 전체 취소가 필요 없거나, 취소할 수 없는 쓰기는 이벤트로 분리하자.
- 역할 분리 : 한 서비스에서 일괄 처리하는게 아니라, 서비스별로 적절히 작업을 나누고, 서비스 사이는 이벤트로 연결하자.
- 모델링 변경 : 데이터는 오너십을 가진 서비스에 배치하여 데이터 접근 비용을 낮추자.
- 서비스 경계 변경 : 너무 구현하기 힘들다면 서비스를 합치거나 경계를 변경하자.
- 독립성이 보장되지 않기에 트랜잭션 격리 레벨이 read uncommited로 유지된다.
- 서비스 간의 데이터가 순간적으로 일치하지 않을 수 있다.
- 따라서 기준 시간이 지난 후에 데이터에 조회하는 등 어플리케이션 레벨의 보완이 필요하다.
- 하지만 네트워크 사이에서 실시간으로 딱딱 맞아 떨어져야 하는 케이스는 별로 없기에, 큰 문제가 되는 케이스는 아니다.
- 데이터베이스의 동기화 메커니즘을 사용할 수 없다.
- DB에서 제공해주는 방식 (e.g., select for update)이 아니라 application lock (별도의 lock 테이블을 두는 방식)을 사용하여 해결할 수 있다.
- 서비스 간의 데이터가 순간적으로 일치하지 않을 수 있다.
이러한 패턴들을 활용해 분산 DB 환경에서 발생할 수 있는 문제를 해결할 수 있다. 다만, 방법이 다양하고, 상황에 따라 적합한 방법이 다르기 때문에 무작정 패턴을 정하는 것은 좋지 않다. 내 시스템에서 발생할 수 있는 문제가 무엇인지 고민하고, 그에 적합한 패턴을 적용하는 과정이 필요하다.
이렇게 인상깊었던 두 세션의 내용을 정리해봤는데, 필기해두었던 것을 다시 읽어보니 한 층 더 이해가 잘 되는 느낌이다 👍 1시간 정도의 시간으로 연사자분의 경험과 노하우를 쏙쏙 빼먹을 수 있다는게 컨퍼런스의 큰 장점 아닐까..! ㅎ.ㅎ
세션 내용도 좋았고, 경품으로 책도 한 권 받았고, 후원 기업의 개발자 상담도 받고, 간식도 야무지게 챙겨먹고 왔다. 티켓값이 전혀 아깝지 않은 만족스러운 행사였다. 언젠가 나도 좋은 주제로 스프링 캠프의 연사자로 참여해보고 싶다 💪
'ETC > 회고' 카테고리의 다른 글
[디프만] 15기 Server 파트 최종 합격 후기 (4) | 2024.06.11 |
---|---|
[우아한테크세미나] 개발자 원칙 후기 (2) | 2023.03.30 |