10주 동안 많이 배웠다. 설계 → 동시성 → 성능 → 회복력 → 이벤트 → 확장성 → 데이터 파이프라인까지, 루퍼스에서 배운 걸 실무에 바로 적용하면서 "일단 돌아가게만 만드는 코더"에서 "왜 이렇게 만들었는지 설명할 수 있는 엔지니어"가 되어가고 있다. 가장 크게 느낀 건, "은탄환은 없고, Trade-off를 이해하고 맥락에 맞게 선택하는 게 진짜 실력" 이라는 것이다.
TDD 에 대해서는 처음엔 귀찮지 않나? 싶었다. 근데 첫 주에 테스트를 작성하면서 생각이 바뀌었다. 테스트하기 어려운 코드는 설계가 잘못된 거였고, 테스트가 쉬운 코드는 구조가 명확한 거였다. 테스트가 내 설계의 거울이었던 거다. TestContainers로 실제 DB랑 비슷한 환경에서 테스트하고, Mock/Stub/Fake를 언제 써야 하는지 고민하면서 많이 배웠다.
2주차에는 "도메인을 모르면 코드를 아무리 잘 써도 소용없구나." 를 배웠다. 실무에서 KYC(고객확인) 시스템 문서를 읽는데, 도대체 무슨 말인지 하나도 모르겠는 거다. KYC, AML, EDD, CDD... 금융권 용어를 이해하고 나서야 "아, 이걸 만들어야 하는구나"가 보이기 시작했다.
3주차 도메인 모델링에서는 그렇게 공부한 도메인 지식을 코드로 표현하는 법을 배웠다. Entity와 VO를 나누고, Domain Service로 비즈니스 로직을 표현하니까 "코드가 비즈니스를 말하는" 느낌이 들었다.
이 3주를 통해 코드 짜기 전에 구조를 먼저 고민하고, 도메인을 이해하는 게 진짜 개발의 시작이라는 것을 알게 되었다.
4주차에서 처음으로 성능이라는 벽에 부딪혔다. 입사 첫 수습 평가였는데, 600만 건의 회원 데이터를 배치 처리해야 했다. "뭐 그냥 @Transactional 붙이고 findAll() 하면 되지 않나?" 싶었는데... 서비스가 느려지기 시작했다.
첫 번째 Trade-off 깨달음: READ_WRITE는 락을 잡아서 서비스에 영향을 주지만, READ_ONLY는 락이 없어서 안전하다. 조회만 하는 배치에서는 READ_ONLY가 적절했다.
5주차는 실무에서 SBS 가요대전 방청권 티케팅 시스템을 만들었는데, 초당 17,000건의 신청을 처리해야한다고 요구사항이 나왔다. DB만으로는 도저히 감당이 안 됐다. Redis를 도입하고, SETNX로 중복 신청을 막고, Write-Behind 패턴으로 Redis에 먼저 저장하고 DB에는 나중에 쓰는 방식으로 버텼다. 손이 떨렸지만, 공부한 모든 것을 총동원해야 했다.
두 번째 Trade-off 깨달음: Redis는 빠르지만 휘발성이니까, Write-Behind로 결국 DB에 저장해서 성능과 안정성을 둘 다 잡았다.
그리고 6주차... 7분간의 프로덕션 장애. 진짜 잊을 수 없는 경험이었다.
특정 서비스 API에 장애가 나자 서비스 전체가 멈췄다. 뉴스에도 나고, 고객 문의가 폭주했다. Circuit Breaker가 있었으면 이런 일을 방지할 수 있지 않았을까 생각했다.
그날 밤에 Resilience4j를 공부했다. Circuit Breaker, Timeout, Fallback... 외부 시스템은 언제든 실패한다는 걸 뼈저리게 느꼈다.
세 번째 Trade-off 깨달음: Circuit Breaker를 달면 복잡해지긴 하지만, 장애 전파를 막으려면 필수다. 특히 결제, 인증 같은 중요한 외부 시스템은 반드시 보호해야 한다.
책에서 배운 이론은 현실의 트래픽과 장애 앞에서 완전히 다르게 느껴진다. 성능과 안정성은 선택이 아니라 필수적으로 신경써야만 한다.
7주차부터 ApplicationEvent를 배우면서 "이벤트 기반으로 생각하면 코드가 진짜 심플해지는구나"를 경험했다.
8주차에 Kafka를 배우면서, 메시지 큐를 넘어 분산 로그 저장소의 중요성을 알게 되었다.
카프카에서는 메시지가 디스크에 영구 저장되고, Offset으로 언제든 추적할 수 있었다. Transactional Outbox 패턴을 적용하니까 "DB에 기록된 건 반드시 Kafka에도 전송"을 보장할 수 있었다.
실제로 고객이 "신청했는데 내역이 없어요"라고 문의한다면, Kafka Offset을 전수 조사해서 이벤트는 발행됐지만 Consumer가 실패했다는 걸 밝혀낼 수 있다. 일반 로그였다면 불가능할 추적이다.
네 번째 Trade-off 깨달음: ApplicationEvent는 심플하지만 분산 환경에서 한계가 있다. Kafka는 복잡하지만 이벤트 소싱, 감사 추적, 장애 복구의 마지막 보루다. 데이터 유실이 허용되지 않는 곳에서는 Kafka가 필수다.
9주차는에서는 SBS 가요대전 실시간 투표 랭킹 시스템을 접하게 되며 깨달았다.
"랭킹은 데이터 구조 문제가 아니라 시간 관리 문제구나."
처음엔 "Redis ZSET 쓰면 끝 아니야?" 싶었는데, 3일 후 확인하니 첫날 1위가 계속 1위였다. 초반에 많이 받은 아이돌이 계속 유리한 Long Tail Problem이었다.
Time Quantization 을 도입했다면 좀 더 재밌지 않았을까 싶다. 전체/일간/시간별로 키를 분리하면 "지금 핫한 아이돌"을 보여줄 수 있다. 근데 새 날이 시작되면 순위가 텅 비는 Cold Start Problem이 생길 수 있으니, Score Carry-Over 도 같이 구현하여 새로운 윈도우에 대한 초기화를 같이 해줘야 할 것 같다.
다섯 번째 Trade-off 깨달음: 어제 순위의 10%를 오늘 순위에 미리 반영한다면, 0%면 빈 순위가 노출되고, 100%면 Long Tail이 재발한다. 10%가 Cold Start Problem 을 방지할 사용자 경험과 공정성의 균형점일 것 같다.
마지막 주는 Spring Batch를 배우면서 대량 데이터 처리에 대해 배웠다.
수습과제에서 600만 회원의 정합성을 체크하는 배치를 만들어야 했다. "뭐 @Scheduled로 findAll() 하면 되지 않나?" 싶었는데 데이터 처리 속도가 처참했다.
배치 처리를 위해 Chunk-Oriented Processing을 적용했다. 500개씩 나눠서 읽고(ItemReader), 처리하고(ItemProcessor), 쓰는(ItemWriter) 방식으로 바꿨다.
근데 RepositoryItemReader의 페이징은 여전히 느렸다. OFFSET 6,000,000을 처리하려면 DB가 600만 건을 건너뛰어야 하여 성능 저하가 발생했다. JdbcCursorItemReader로 변경하여 성능 문제를 해결할 수 있었다.
여섯 번째 Trade-off 깨달음: 페이징은 구현이 쉽지만 대량 데이터에서 느리다. 커서는 JDBC 타입 설정이 필요하지만 성능이 일정하다. 100만 건 이상은 커서가 좋은 것 같다.
그리고 4주차에 배운 트랜잭션과 락의 개념이 무척 유용했다. 배치가 30분 동안 락을 잡고 있어서 서비스가 느려졌는데, READ_ONLY로 바꾸니까 락이 사라지고 처리 속도가 개선되었다.
신기한 건, 4주차에 배운 READ_ONLY를 10주차 배치 구현에 다시 쓰면서 "학습은 반복되고 심화되는구나"를 느꼈다는 거다. 같은 개념도 맥락에 따라 다르게 적용된다.
처음엔 "나는 개발자니까 코드만 잘 쓰면 되지 않나?" 싶었다. 근데 실무에서 인증 시스템을 유지하며 생각이 완전히 바뀌었다. KYC, AML, EDD, CDD... 금융 용어를 하나도 모르니까, "무엇을 만들어야 하는지" 모르게 되었다.
Entity와 VO를 나누고, Domain Service로 비즈니스 로직을 표현하는 것도 결국 "도메인을 코드로 말하는 것" 이었다. 그 이후로는 항상 문서를 먼저 읽고, 도메인 전문가랑 대화하며 Ubiquitous Language를 찾으려고 노력했다.
7분간의 프로덕션 장애는 충격적인 경험이었다. 뉴스에도 나고, 고객이 분노했다.
외부 시스템은 언제든 실패한다. Circuit Breaker, Timeout, Fallback... 이런 게 선택이 아니라 필수라는 걸 그날 뼈저리게 느꼈다. 이후로는 모든 외부 호출을 의심하고, 장애 전파를 막는 걸 최우선으로 설계하게 됐다.
ApplicationEvent로 이벤트를 분리한 후, Kafka를 배우면서 "분산 시스템에서는 이벤트가 영구 저장되고, 추적 가능해야 한다" 는 것을 알게 됐다. Transactional Outbox, Idempotent Consumer, DLQ 같은 패턴들이 왜 필요한지 체감했다.
이 전환점이 내 사고방식을 "하나의 서버"에서 "분산 시스템"으로 확장시켰다.
"Redis ZSET" 을 활용하며, Long Tail Problem을 마주쳤다.
누적 투표는 초반 인기가 계속 유리하다. 더 좋은 사용자 경험을 위해서는 시간을 어떻게 나누고, 얼마나 반영하느냐 를 고민해야 한다. "랭킹의 목적은 숫자가 아니라 팬들이 응원하는 재미"라는 걸 생각하게 됐다.
10주는 끝났지만 아직도 모르는 게 너무 많다. 분산 트랜잭션(Saga), 캐시 일관성, 대규모 데이터 샤딩, Kubernetes 기반 배포... 배울 게 산더미다.
하지만 이제는 어떤 자세로 공부를 할지 감각이 생기는 것 같다.
문제를 먼저 만나라. 이론만 공부하면 금방 잊는다. 실전 문제를 만나고, 해결하며 배워야 기억에 남는다.
Trade-off를 이해하라. 은탄환은 없다. 각 기술의 장단점을 알고, 맥락에 맞게 선택하는 게 진짜 실력이다.
작은 것부터 적용하라. Spring Batch를 배웠다고 모든 배치를 다시 짤 필요는 없다. READ_ONLY 하나만 적용해도 큰 효과가 있다.
왜 그런지 고민하라. "왜 커서가 페이징보다 빠른가?", "왜 Kafka는 메시지를 디스크에 저장하는가?" 의도를 이해해야 응용할 수 있다.
앞으로도 계속 고민하고, 적용하고, 실패하고, 배우겠다.