문제의 출발점
기능은 생각보다 빨리 붙는다. 화면도 나오고, API도 붙고, 파일 업로드도 되고, 관리자 기능도 돌아간다. 특히 AI 도움을 받으면 속도는 더 잘 나온다.
그런데 어느 순간부터 분위기가 바뀐다.
- 어떤 API는 점점 느려진다.
- 어떤 배치 작업은 돌릴 때마다 불안하다.
- 파일 처리 기능은 메모리를 많이 먹는다.
- 외부 API나 메일이 느려지면 전체 요청까지 흔들린다.
- 장애가 나면 어디부터 봐야 할지 막막하다.
나는 지금 백엔드를 만드는 법을 배우고 있는 걸까, 아니면 그냥 기능을 이어 붙이고 있는 걸까?
이 글은 그 질문에서 출발했다. 이 단계에서 필요한 건 더 많은 프레임워크가 아니라 운영에서 안 터지는 기준이다.
왜 이런 문제가 AI 개발에서 더 빨리 드러날까
AI는 동작하는 코드를 빠르게 만드는 데 강하다. CRUD 흐름, DTO, 서비스, 컨트롤러 뼈대, 반복 코드 생산성은 분명히 올라간다.
하지만 아직 사람이 직접 기준을 가져야 하는 영역은 다르다.
AI가 빠르게 도와주는 것
- CRUD 흐름
- DTO, 서비스, 컨트롤러 뼈대
- 기본적인 검증
- 반복적인 코드
여전히 사람이 기준을 가져야 하는 것
- 트랜잭션 경계
- 대량 데이터에서의 쿼리 비용
- 외부 I/O의 timeout과 retry
- 비동기 작업 포화 시 동작
- 대용량 파일 처리 방식
- Redis key 구조
- 운영 관측성
AI는 코드가 돌아가게 해줄 수는 있지만, 그 코드가 운영 가능한지 판단하는 기준은 아직 사람이 가져야 한다.
공부 순서는 화려한 것보다 위험한 것부터여야 한다
아키텍처, DDD, 클린 코드 같은 주제는 중요하다. 하지만 실제 서비스가 흔들리는 단계에서는 먼저 막아야 하는 위험이 따로 있다.
- 트랜잭션 경계
- SQL과 인덱스
- 외부 I/O 설계
- 대용량 파일 처리
- 비동기와 executor
- Redis와 캐시
- 관측성
- 실패 경로 테스트
이 순서는 화려하지 않다. 하지만 실제 서비스 품질을 가장 빨리 바꾼다. 특히 AI로 빠르게 만든 프로젝트일수록 기능의 빈칸보다 운영 기준의 빈칸이 먼저 문제를 만든다.
핵심 7개 주제
1. 트랜잭션 경계
많은 백엔드 문제는 비즈니스 로직보다 트랜잭션 경계에서 시작된다.
- DB 저장과 외부 API 호출이 같은 트랜잭션 안에 있다.
- 메일 발송, 파일 정리, 후처리가 commit 전에 실행된다.
- read-only 트랜잭션인데 오래 걸리는 파일/S3 작업이 들어 있다.
항상 이 세 가지를 물어야 한다.
- 이 작업은 commit 전에 꼭 필요할까?
- commit 이후로 미뤄도 될까?
- 실패하면 전체 rollback이어야 할까, 부분 성공이어도 될까?
2. SQL과 인덱스
많은 성능 문제는 코드가 비효율적이라서보다 쿼리가 커지는 방식이 나빠서 생긴다. 작은 데이터에서는 멀쩡해 보이지만 운영에서는 바로 드러난다.
- pagination 없는 목록 API
- 전량 조회 후 애플리케이션 메모리 정렬
offset pagination- N+1 조회
- 직렬 aggregate query
EXPLAIN ANALYZE, 인덱스, keyset pagination,
count, group by, order by 비용을 모르면
성능 문제를 늘 느낌으로만 이야기하게 된다.
3. 외부 I/O
메일, 스토리지, 외부 metadata API, 결제 API는 항상 느려질 수 있고 실패할 수 있다. 이 시점부터는 구현보다 실패 설계가 더 중요하다.
- timeout이 있는가
- retry가 있는가
- retry해도 안전한가
- 호출이 몰릴 때 막을 장치가 있는가
- 실패해도 핵심 상태는 지켜지는가
4. 대용량 파일 처리
파일 업로드, PDF preview, ZIP download, 대량 export 같은 기능은 평소에는 멀쩡해 보여도 큰 파일이 겹치면 heap, 디스크, 네트워크, 응답 시간이 동시에 흔들릴 수 있다.
- 이 파일을 메모리에 몇 번 올리는가
readAllBytes()를 써도 되는 크기인가- streaming으로 바꿀 수 있는가
- temp file이 더 안전한가
- 단일 파일 크기와 총 작업량 제한이 모두 있는가
5. 비동기와 executor
@Async나 background job은 시스템을 가볍게 만들 것 같지만,
실제로는 더 빨리 무너뜨릴 수도 있다.
- 무거운 작업이 포화 시 요청 스레드로 fallback 되는가
- 서로 다른 작업이 같은 executor를 공유하는가
- queue가 쌓이는 것을 운영에서 볼 수 있는가
- 비동기라 했지만 결국 요청 latency로 돌아오지 않는가
비동기의 진짜 목표는 백그라운드로 보내는 것이 아니라 핵심 요청 경로를 보호하는 것이다.
6. Redis와 캐시
Redis는 빠른 도구이지, 자동으로 안전한 도구는 아니다.
SCAN과 indexed access 차이- positive cache와 negative cache 차이
- cache stampede
- hot key
- single-flight
어떤 접근 패턴으로 읽고 쓸지 먼저 결정해야 한다.
7. 관측성
좋은 코드도 운영에서 숫자가 안 보이면 결국 감으로 대응하게 된다.
- request latency
- DB pool 대기
- executor queue 길이
- Redis latency
- 외부 API 호출 시간
- slow query
- 에러율
health check는 살아 있는지만 알려준다. 실제 운영에서는 어디가 포화됐는지 보는 숫자가 먼저 있어야 한다.
테스트도 종류가 다르다
기술부채가 쌓인 프로젝트에서는 unit test만으로는 부족하다. 동시성 충돌, 대량 처리, 실패 경로, 외부 I/O 지연, long-running task는 다른 종류의 테스트가 필요하다.
- unit test: 순수 로직 검증
- integration test: 계층 연결과 DB 동작 검증
- concurrency test: race condition 검증
- load test: 부하 상황 검증
중요한 건 테스트 개수가 아니라 어떤 위험을 막아주는 테스트인지다.
코드 리뷰 때 반복해서 던지는 10개 질문
- 이 쿼리는 데이터가 100배 커지면 어떻게 되나?
- 이 외부 호출은 timeout과 retry가 있는가?
- 이 코드는 트랜잭션 안에 있을 이유가 정말 있는가?
- 동시에 두 번 호출되면 안전한가?
- 실패 후 재시도하면 중복 상태를 만들지 않는가?
- 이 파일은 메모리에 몇 번 올라가나?
- 비동기 작업이 포화되면 어디로 밀리나?
- 공개 endpoint라면 abuse를 어떻게 막나?
- 장애가 났을 때 내가 볼 숫자가 있는가?
- 이 변경을 작은 PR로 나눌 수 없는가?
이 질문들에 답을 못 하면, 대체로 아직 운영 가능성을 충분히 검토한 코드는 아니다.
마무리
빠르게 만든 백엔드는 기능보다 운영에서 먼저 한계를 드러낸다. 그래서 어느 시점부터는 더 많은 코드를 만드는 능력보다, 어떤 코드를 경계해야 하는지 아는 능력이 더 중요해진다.
나는 그 기준이 결국 아래로 수렴한다고 생각한다.
- 트랜잭션
- SQL
- 외부 I/O
- 파일 처리
- 비동기
- Redis
- 관측성
기술부채를 줄이는 공부는 거창한 리팩토링에서 시작하지 않는다. "이 작업은 왜 여기 있지?", "이 쿼리는 왜 이렇게 가져오지?", "이 요청이 열 번 겹치면 어떻게 되지?" 같은 질문에서 시작된다.
좋은 백엔드는 복잡한 기술을 많이 쓴 백엔드가 아니다. 문제가 생겼을 때 어디서 왜 터지는지 설명할 수 있는 백엔드에 더 가깝다.