Make the middle layer a thin hop, not a boundary that breaks on every upstream change.
Built a reverse proxy with a reflection-based RPC dispatcher consolidating 40 middleware endpoints into one, deployed across 8 sites — freeing 1 FTE of middleware development by eliminating redeployments.
- Problem: 1:1 API mirroring — every legacy change forced middleware redeploy
- Concern: change isolation, only 3 engineers, didn't scale
- Fix: thin gateway — frontend passes namespace/class/method as params
- Fix: legacy parses + dispatches via reflection; end-to-end JSON, no transform
- Result: 30 mirrored endpoints became 1 forwarder; zero incidents in 12mo
- Result: stayed at 3 engineers, avoided a multi-month 4th-hire approval
// thin gateway: forward by plant code, no business logic
public Object route(String plantCode, Payload body) {
return legacy.forward(body);
}
// legacy WMS: parse params, dispatch by reflection
Object dispatch(String pkg, String cls, String method, Object[] args) throws Exception {
Class<?> target = Class.forName(pkg + "." + cls); // reflection
Method m = target.getMethod(method, typesOf(args));
return m.invoke(beanFor(target), args); // ~1-2ms/call
}
// one forwarding endpoint, not 30 mirrored ones;
// dispatch lives in legacy: only it can reach the DB- ·The senior mapped 30 endpoints 1-to-1; every legacy change broke the middleware.
- ·My concern was change isolation — with 3 engineers it wouldn't scale.
- ·I proposed a thin gateway: middleware just forwards, the legacy parses and dispatches with reflection.
- ·His expertise still mattered — I gave him gateway logging and monitoring.
- ·Result: zero incidents in 12 months, stayed at 3 engineers, no 4th hire.
- Opendoor: a thin edge/forwarding layer that doesn't break on every upstream change.
- Proves: gateway/dispatcher design, change isolation, small-team leverage.
- Use it for API-gateway / BFF design.
- Client now names legacy class/method directly — the frontend is coupled to legacy internals, so renames/refactors break callers silently.
- No transform layer means no contract: a changed arg order or type surfaces as a runtime 500, not a compile error.
- Reflection dispatch keyed on client-supplied strings leans on network perimeter for safety; one allowlist gap exposes arbitrary method invocation.
- One forwarder for all calls hides per-endpoint visibility — metrics, rate limits, and auth become coarse unless you parse the payload back open.
- Add consumer-driven contract tests (Pact) so legacy method/arg changes fail CI instead of in production.
- Whitelist dispatchable namespaces/classes in a registry, not raw reflection — cuts the arbitrary-invocation surface and enables per-method authz.
- Cache resolved MethodInfo/handles per namespace+class+method to drop reflection lookup cost under load.
- Emit per-method metrics by tagging spans with namespace.class.method, so the single endpoint still gives per-call latency and error rates.
Understand it step by step (한국어로 이해 → 영어로 말하기)
- 1
앱(폰) → 중간 서버 → 옛 창고 시스템, 이렇게 3단 구조다. 옛 시스템만 DB에 닿을 수 있다.
// 3-tier: app -> middleware -> legacy WMS (only legacy reaches the DB) public Object route(String plantCode, Payload body) { return legacy.forward(body); // dispatch + DB access live in the legacy }🗣 영어로 말해“The app calls a middle server, which calls the old warehouse system.”
checking microphone…
- 2
선배는 옛 시스템 기능 30개를 중간 서버에 똑같이 30개로 다시 만들었다. 한 개에 하나씩 짝지은 거울 구조다.
⚖️ Trade-off: 옛 시스템에서 하나만 바뀌어도 중간 서버의 짝꿍도 매번 같이 고쳐야 한다.
🔧 도구:1:1 API mapping
// 1:1 mirroring: one middleware endpoint per legacy method Object getLot(args) { return legacy.getLot(args); } Object saveLot(args) { return legacy.saveLot(args); } Object closeOrder(args){ return legacy.closeOrder(args); } // every legacy change forces a matching middleware redeploy🗣 영어로 말해“The senior mirrored 30 endpoints one-to-one, so every change broke the middle layer.”
checking microphone…
- 3
사람이 3명뿐인데 옛 시스템이 바뀔 때마다 중간 서버를 또 배포해야 한다. 일이 끝없이 늘어나서 감당이 안 된다.
⚖️ Trade-off: 거울 구조는 변경이 한쪽에서 다른 쪽으로 다 번진다. 격리가 안 된다.
✅ Fix: 그래서 중간 서버가 '내용을 아는' 구조 자체를 버리기로 했다.
🗣 영어로 말해“With only three engineers, redeploying on every change just doesn't scale.”
checking microphone…
- 4
해결: 중간 서버는 그냥 통과만 시킨다. 앱이 '어느 클래스, 어느 함수를 불러줘'를 글자로 같이 보낸다.
✅ Fix: 중간 서버는 내용을 안 봐도 된다. 공장 코드만 보고 옛 시스템으로 그대로 넘긴다.
🔧 도구:thin gateway / pass-through forwarder
// thin forwarder: middleware passes the call through, no per-method logic Object route(String plantCode, Payload p) { return legacy.forward(plantCode, p); // app names what to call inside p }🗣 영어로 말해“I made the middle layer a thin forwarder — the app just names what to call.”
checking microphone…
- 5
옛 시스템이 그 글자(클래스/함수 이름)를 읽고, 리플렉션으로 그때그때 맞는 함수를 찾아서 실행한다. 끝까지 JSON 그대로라 변환이 없다.
⚖️ Trade-off: 이제 앱이 옛 시스템 내부 함수 이름을 직접 부른다 → 옛 시스템이 이름을 바꾸면 앱이 조용히 깨진다. 중간에 변환층이 없으니 인자 순서·타입이 틀리면 컴파일 에러가 아니라 실행 중 500 에러로 터진다.
✅ Fix: 거울 30개가 통과 1개로 줄었다. 옛 시스템이 바뀌어도 중간 서버는 손 안 댄다.
🔧 도구:reflection dispatchend-to-end JSON
// legacy WMS: parse params, dispatch dynamically by reflection Object dispatch(String namespace, String cls, String method, Object[] args) { Object target = load(namespace, cls); // reflection lookup Method m = target.getClass().getMethod(method); return m.invoke(target, args); // ~1-2ms/call, JSON in/out }🗣 영어로 말해“The old system reads those names and dispatches by reflection — end-to-end JSON, no transform.”
checking microphone…
- 6
위험: 함수 이름을 앱이 글자로 정해서 보내니, 막으면 아무 함수나 부를 수 있는 구멍이 된다.
⚖️ Trade-off: 글자로 함수를 부르는 건 공격에 약하다. 허용 목록 한 칸만 빠져도 위험하다.
✅ Fix: 닫힌 망 + IP 허용 목록으로 막고, 게이트웨이 인증을 한 겹 더 뒀다. 한 겹이 뚫려도 다른 겹이 막는 식(다중 방어).
🔧 도구:IP allowlistclosed networkgateway authentication
// defense in depth: closed network + IP allowlist + gateway auth if (!ipAllowlist.contains(remoteIp)) return reject(403); if (!gatewayAuth.verify(token)) return reject(401); return dispatch(namespace, cls, method, args);
🗣 영어로 말해“The risk is exposing internal methods, so I added a closed network plus gateway auth — defense in depth.”
checking microphone…
- 7
통과 하나로 합치니 함수별로 따로 보던 지표·속도·에러를 못 본다. 그래서 선배에게 게이트웨이 로깅·모니터링을 맡겼다.
⚖️ Trade-off: 통로가 하나라 어느 함수가 느린지·터졌는지 따로 안 보인다.
✅ Fix: 선배의 강점을 살려 로깅·모니터링을 맡겼다. 그의 전문성도 그대로 쓰였다.
🔧 도구:span tags: namespace.class.method
// tag spans so the single forwarder still gives per-method visibility span.setTag("rpc", namespace + "." + cls + "." + method); log.info("dispatch {}.{}.{} latencyMs={}", namespace, cls, method, ms);🗣 영어로 말해“His expertise still mattered — I gave him the gateway's logging and monitoring.”
checking microphone…
- 8
결과: 30개가 통과 1개로 줄었고, 12개월간 사고 0건. 사람도 3명 그대로, 4번째 채용을 안 해도 됐다.
🗣 영어로 말해“Result: zero incidents in twelve months, stayed at three engineers, no fourth hire.”
checking microphone…
8단계 영어를 다 말하면 → 이 메커니즘 전체를 영어로 설명할 수 있게 된다.