"요청을 합친다"는 말은 서로 다른 세 가지를 가리킨다. 무엇이 줄어드는지가 다르다.
① 싱글플라이트 같은 키 동시 중복 조회를 1건으로 서버 내부 → DB 조회 횟수↓
② 배치 로딩 같은 틱의 여러 키를 1쿼리로 서버 내부 → 쿼리 왕복↓
③ 엔드포인트 통합 클라의 여러 번 호출을 1번으로 클라↔서버 → 네트워크 왕복↓
① 싱글플라이트 (request coalescing)
캐시가 만료된 순간 같은 키로 1,000개 요청이 들어오면 DB에 1,000번 간다(캐시 스탬피드).
private final ConcurrentHashMap<String, CompletableFuture<Data>> inFlight = new ConcurrentHashMap<>();
Data load(String key) {
return inFlight.computeIfAbsent(key, k ->
CompletableFuture.supplyAsync(() -> fetchFromDb(k))
.whenComplete((v, e) -> inFlight.remove(k)))
.join();
}
캐시와 다르다 — 캐시는 끝난 결과를 저장하고, 싱글플라이트는 진행 중인 작업을 공유한다. 둘은 함께 쓴다.
② 배치 로딩 (DataLoader)
목록 100건 그리며 각 작성자를 따로 조회 → 1 + 100 쿼리 (N+1)
↓ 같은 이벤트 루프 틱에 키를 모은다
SELECT * FROM users WHERE id IN (1..100) // 1회
GraphQL 리졸버에서 특히 중요하지만 REST에서도 같은 문제다. JPA라면 fetch join·@BatchSize가 같은 문제를 다른 방식으로 푼다.
③ 엔드포인트 통합·BFF
- Before — GET /products/{id} + /stock + /users/me/coupons = 왕복 3회
- After — GET /product-page/{id} = 왕복 1회
효과가 서버 내부에 그치지 않는 것이 앞의 둘과 다르다 — 인증·암복호화 게이트웨이, TLS 핸드셰이크, 모바일 네트워크 지연이 호출 횟수에 비례한다. 실제 라이브 커머스 사례에서 API 3개를 1개로 합쳐 피크 트래픽 약 50% 감소.
BFF(Backend for Frontend) 는 이를 구조로 만든 것으로, 화면이 필요로 하는 모양대로 조립해 주는 계층을 클라이언트별로 둔다.
합치면 잃는 것
캐시 단위가 커진다
- 재고(자주 변함) + 상품정보(거의 안 변함) 를 한 응답에 묶으면
- 전체를 짧은 TTL 에 맞춰야 한다 → 캐시 효율 하락
화면과 API 가 묶인다 (BFF)
- 화면이 바뀔 때마다 서버를 고쳐야 하고 클라이언트가 늘면 BFF 도 는다
- GraphQL 은 클라가 필드를 고르게 해서 같은 문제를 다르게 푼다
변경 주기가 크게 다른 것은 나눠 두는 편이 낫다.
면접 함정
- ❌ 세 층을 뭉뚱그려 "요청을 합쳤습니다" → 무엇이 줄었는지(DB 조회·쿼리 왕복·네트워크 왕복)를 말할 수 있어야 한다.
- ❌ 싱글플라이트를 캐시로 착각 → 저장이 아니라 공유다.
- ❌ 무조건 통합 → 캐시 효율과 변경 주기를 먼저 본다.