Spine을 만들며 다시 본 웹 프레임워크의 본질

현대 웹 프레임워크는 뛰어난 생산성을 제공한다. Spring이나 NestJS를 사용하다 “기능을 만들기 어렵다”라고 느낀 적은 거의 없다. 문제는 구현 이후였다. 프로젝트가 커질수록, 도메인이 복잡해질수록 머릿속에 같은 질문이 반복됐다. “이 로직은 대체 언제 실행되는 걸까?”
컨트롤러 중심이 아닌 ‘요청이 어떤 규칙으로 실행되는지’를 기준으로, 웹 프레임워크를 다시 본 경험을 기록한다.
처음 로직의 실행에 대한 질문을 떠올렸을 때는 단순히 ‘익숙하지 않아서 그런 것’이라고 생각했다. 프레임워크를 더 깊이 이해하면 해결될 문제라고 여겼다.
그러나 질문은 사라지지 않았다. Controller 메서드는 분명 내가 작성했다. 하지만 요청의 흐름을 따라가다 보면 어느 순간부터 확신이 없어졌다. 어노테이션, 미들웨어, 인터셉터, AOP 각각의 역할은 알고 있지만, 이 요청이 어떤 순서로 실행되는지 코드 구조만 보고 설명하기는 어려웠다. 결국 실행의 흐름을 코드가 아니라 디버깅, 문서, 그리고 팀의 암묵적 지식에 의존하며 이해하고 있었다.
이때부터 내 관심은 기능 구현에서 벗어나기 시작했다. “어떻게 잘 만들까”보다 “이 요청은 어떤 규칙에 따라 실행되고 있는 걸까”라는 질문이 더 중요해진 것이다.
이 불편함은 단순한 느낌이 아니라, 프로젝트가 커질수록 반복해서 나타나는 구조적인 문제였다. 예를 들어, 주문 확정 API 하나만 보더라도 요구사항은 아래와 같이 점점 복잡해진다.
이를 다루는 Controller 코드의 예시다.
@RestController
@RequiredArgsConstructor
public class OrderController {
@PostMapping("/orders/{id}/confirm")
@Transactional
public OrderResponse confirm(
@PathVariable Long id,
@AuthenticationPrincipal UserPrincipal user
) {
return orderService.confirm(id, user.getId());
}
}
겉으로 봤을 때는 단순해 보이는 코드다. 하지만 실제 실행은 복잡한 단계를 거친다.
1) Servlet Filter 단계
아직 트랜잭션은 시작되지 않는다
2) HandlerInterceptor.preHandle()
3) Argument Resolver
PathVariable, AuthenticationPrincipal 해석
예외 발생이 가능하지만, 책임 위치는 불명확하다
4) TranscationInterceptor
이 시점에서야 트랜잭션이 시작된다
5) Controller에서 Servcie 호출
orderService.confirm(id, userId);이때, Service 내부에서는 또 다른 일이 벌어진다.
6) Interceptor.postHandle()
불편함 1: 이 로직은 트랜잭션 안인가? 밖인가?
권한 검사 중 DB 업데이트가 있거나, 이벤트 리스너에서 예외가 발생하고, 롤백이 불가했다. 또, AOP 위치를 모르면 설명이 불가능했다.
불편함 2: 이 검증 로직은 어디에 두는 게 맞는가?
Filter? Interceptor? Controller? Service? 팀원마다 답이 달랐다. 결국 문서보다 경험자가 더 강한 권한을 가졌다.
불편함 3: 이 요청의 실행 순서를 한 마디로 설명할 수 있는 사람이 없다
신규 인원이 들어와 이 API의 요청 흐름을 물어보면, 대답은 항상 얼버무려졌다. “인증 먼저 되고, 그다음 인터셉터 타고, 트랜잭션은 이쯤에서 열리고….” 정확한 순서를 아무도 확신하지 못하기 때문이다.
이 지점에서 깨달았다. 이건 특정 프레임워크의 숙련도 문제가 아니라, 실행 흐름이 코드 구조로 드러나 있지 않기 때문에 생기는 문제라는 것을.
나는 문제를 다시 정의했다. 이는 Spring이 부족해서도, NestJS가 불완전해서도 아니다. (오히려 두 프레임워크 모두 너무 많은 일을 잘해주고 있다.) 문제는 다른 데 있었다. “요청이 어떻게 실행되는지에 대한 설명이 코드 구조 안에 남아 있지 않다는 점”이었다.
Filer, Interceptor, AOP, Controller, Service 각각은 분명한 역할을 가지고 있다. 하지만 이들이 어떤 순서와 규칙으로 엮여 있는지는 코드만 보고 알기 어렵다. 결국 실행 흐름은 문서나 경험, 혹은 디버깅을 통해서만 복원된다. 이 지점에서 관점을 조금 바꿔보기로 했다.
기존에는 웹 프레임워크를 이런 관점으로 바라봤다.
하지만 실제로 더 중요한 질문은 이것이었다. “이 요청은 어떤 규칙에 따라 실행되는가?”
인자 생성은 언제 일어나고, 횡단 관심사는 어디에서 개입하며, 비즈니스 로직은 정확히 어느 시점에 호출되는가. 이 질문에 대한 답이 코드 구조로 드러나 있지 않다면, 프레임워크를 아무리 잘 알아도 실행을 온전히 통제하기는 어렵다.
이 사고는 또 다른 질문으로 이어졌다. “그렇다면, 실행 순서를 누가 알고 있어야 할까?”
Spring이나 NestJS에서는 이 역할이 여러 구성 요소에 나뉘어 있다. 각 컴포넌트는 자신이 언제 호출되는지에 대해 깊이 이해하지 않는다. 대신 프레임워크 내부 규칙에 맡긴다.
반대로 이렇게 가정해 보면 어떨까? 요청의 실행 순서를 아는 주체가 하나인 것이다. 인자 생성, 실행 전 처리, 비즈니스 로직 호출, 실행 후 처리, 응답 완성. 이 모든 단계를 하나의 실행 모델로 알고 있는 주체가 있다면, 실행 흐름은 더 이상 암묵적 지식이 아니라 구조가 된다.
이 생각의 끝에서, 기존 프레임워크를 ‘이해하는 것’만으로는 이 불편함을 끝낼 수 없겠다는 결론에 이르렀다. 그래서, 직접 이 관점을 전제로 한 구조를 만들어보기로 했다.
웹 프레임워크를 기능의 묶음이 아니라, 요청을 실행하는 규칙의 집합으로 정의하는 것. 이 실행 모델 중심의 관점이 Spine이라는 프레임워크의 출발점이 되었다.
Spine는 다음을 명확한 전제로 둔다.
아직은 작은 프레임워크지만, 이 구조 위에서는 요청이 어떻게 실행되는지를 코드 구조만 보고도 설명할 수 있다. 그리고 이는 내가 Spine을 만들며 가장 분명하게 달라졌다고 느낀 부분이다.
실행을 하나의 모델로 보겠다고 마음먹은 다음, 바로 프레임워크를 만들 수 있었던 것은 아니다. 오히려 그때부터 더 많은 선택을 해야 했다. 실행 흐름을 명시적으로 드러내려면, 기존 프레임워크에서 당연하게 받아들여지던 많은 것들을 다시 결정해야 했기 때문이다.
가장 먼저 내린 결론은 이것이었다. “요청의 실행 순서를 아는 주체는 반드시 하나여야 한다.”
Spring이나 NestJS에서는 실행 흐름이 여러 컴포넌트에 나뉘어 있다. Filter는 Filter대로, Interceptor는 Interceptor대로, AOP는 AOP대로 동작한다. 각각은 잘 설계되어 있지만, 전체 실행 순서를 단일한 구조로 표현하지는 않는다.
Spine에서는 이 책임을 하나의 컴포넌트로 모으기로 했다. 요청이 들어오면 어떤 순서로 처리되는지, 어디에서 확장이 일어나는지, 비즈니스 로직은 정확히 언제 호출되는지를 오직 하나의 실행 파이프라인이 알고 있도록 했다.
이 선택은 이후 모든 설계의 기준이 되었다.
다음으로 부딪힌 문제는 “컨트롤러 호출”이었다. 일반적인 프레임워크에서는 컨트롤러 메서드를 호출하는 행위와 그 호출을 둘러싼 실행 판단이 강하게 엮여 있다. 인자는 어디서 만들어지는가, 인터셉터는 언제 실행되는가, 예외는 어디서 처리되는가, 이 모든 것이 “컨트롤러 호출”에 묶여 있다.
Spine에서는 이 둘을 분리했다.
이렇게 나누고 나니, 실행 구조가 훨씬 또렷해졌다. “무엇을 호출하는가”와 “언제, 어떤 순서로 실행되는가”가 명확히 갈라졌기 때문이다.
Controller를 어떻게 다룰지도 중요한 선택이었다. Spine에서 Controller는 다음 책임을 가지지 않는다.
그 대신 Controller가 표현하는 것은 단 하나다. “이 요청이 어떤 유즈 케이스를 실행하는가?”
그래서 Controller 메서드의 시그니처 자체를 계약으로 보게 되었다. 파라미터는 “어디선가 알아서 주입되는 값”이 아니라, 반드시 ArgumentResolver를 통해 생성되는 값이다. 이러한 구조에서는 “이 값이 어디에서 오는가?”라는 질문에 항상 코드로 답할 수 있다.
한편 Spine에서 실행 흐름에 개입할 수 있는 공식적인 확장 포인트는 Interceptor 하나뿐이다.
이 선택은 의도적이었다. 실행 모델 자체가 흔들리기 시작하면, 다시 실행 흐름은 분산되기 때문이다.
Spine의 구조는 처음부터 편리함을 추구하지 않았다. 오히려 의도적으로 불편하게 설계한 부분이 많다.
기존 프레임워크에는 “일단 붙이고 나중에 정리하자”는 선택지가 있다. 하지만 Spine에는 그런 접근이 거의 통하지 않는다. 실행 흐름에 개입하려면, 그 책임이 어느 단계에 속하는지부터 결정해야 한다.
자동으로 해결되던 일이 명시적인 코드로 드러나고, 구현보다 아키텍처적인 판단이 필요한 구조. 기본 프레임워크에 익숙한 개발자일수록 “이건 너무 번거로운 거 아닌가?”라는 생각이 자연스럽게 들 수 있다. 그뿐만 아니라 Spine에 숙련되지 않은 상태라면, 초기 프로토타입 단계에서는 Spring이나 NestJS보다 느리다고 느껴진다.
하지만 구조를 고민하는 일은 한 번이면 된다. 시간이 지나면서 체감이 달라지기 시작할 것이다. Spine에서는 실행 흐름에 대한 고민이 기능마다 반복하지 않는다. Pipeline이라는 실행 모델이 고정되어 있기 때문이다.
이 규칙이 한 번 정해지고 나면, 새로운 기능을 추가할 때마다 “이 로직을 어디에 넣어야 하지?”라는 질문을 다시 하지 않아도 좋다. 고민의 비용을 앞에서 치르고, 그 이후에는 구조가 그 결정을 대신해 준다.
명시적인 실행 구조가 가져온 가장 큰 변화는 디버깅 방식이었다. 이전에는 문제가 발생하면 “어디서 실행된 걸까?”를 먼저 추적해야 했다. 브레이크포인트를 여러 군데 찍고, 로그를 따라가며 실행 흐름을 머릿속으로 복원했다.
Spine에서는 접근 방식이 다르다. 문제는 항상 특정 실행 단계에서 발생한다. Interceptor 이전인지, 이후인지, 비즈니스 로직 호출 전인지, 후인지, 실행 흐름이 구조로 고정되어 있기 때문에, 문제를 좁히는 속도가 눈에 띄게 빨라진다.
이 차이는 팀 규모가 커질수록 더 크게 느껴질 것이다. 신규 인력이 들어왔을 때, Spine의 실행 흐름은 그림 한 장으로 설명할 수 있다. “요청이 이러한 순서로 실행된다”라는 설명이 개발자마다 다르게 해석되지 않는다.
경험 많은 사람이 구조를 설명해 주지 않아도, 코드 구조 자체가 실행 모델을 드러내는 형태다. 그래야 암묵적 이해에 의존하던 영역이 줄어든다.
현재 Spine가 주는 불편함은 대부분 명시성에서 비롯된 것이었다. 자동으로 처리되던 일을 직접 결정하고, 코드로 드러내야 하기 때문이다. 이 과정은 분명 비용처럼 느껴진다.
하지만 이 비용은 누적되지 않는다. 한 번 구조를 세우고 나면, 시스템이 커져도 실행 흐름은 그대로 유지된다. 이 점에서 명시적 구조는 단기 생산성을 조금 양보하고 얻는 장기 안정성에 가깝다.
그래서 Spine는 Spring과 NestJS를 대체하는 것이 목표일까? 이 질문에 대한 답은 분명하다. Spine은 기존 프레임워크를 밀어내기 위해 만들어진 것이 아니다.
나는 오히려 Spine을 만들며, Spring과 NestJS가 왜 그러한 선택을 했는지 더 잘 이해하게 되었다.
두 프레임워크는 대규모 팀과 다양한 요구사항을 전제로 설계되었다. 많은 결정을 프레임워크가 대신 내려주고, 개발자는 정해진 방식 안에서 빠르게 기능을 구현할 수 있다. 이 선택은 분명 강력한 방식이며, 실제로도 잘 작동한다.
그럼에도 Spine은 전혀 다른 지점에서 출발했다. 관심사는 기능의 개수가 아닌 “요청이 어떤 규칙에 따라 실행되는가?”다. 그래서 실행 흐름을 하나의 모델로 고정하고, 그 위에서만 개입이 가능하도록 구조를 단순화했다.
이 선택이 프레임워크의 성격을 완전히 다르게 만든다. Spine에서는 기능이 늘어날수록 실행 흐름이 복잡해지기보다, 오히려 더 또렷해진다. 인증, 트랜잭션, 로깅 같은 문제는 “어디에 붙일 수 있는가”가 아니라 “어느 실행 단계의 책임인가”로 정리된다.
말했듯이 이 구조가 초기에는 불편할 수 있다. 하지만 도메인이 커지고, 요청 하나가 여러 책임을 거치게 될수록 실행 흐름을 통제할 수 있다는 감각은 분명한 장점이 된다. 그래서 더욱 Spine은 대체재라기보다 다른 출발점을 가진 프레임워크에 가깝다. 실행 흐름이 하나의 모델로 고정된 구조를 기준으로 삼고 나면, Spring이나 NestJS의 실행 방식이 더 이상 막연하지 않을 것이다.
어디에서 실행이 분산되고, 왜 그런 추상화가 필요했는지, 그 모두가 비교와 판단의 대상으로 드러난다.
장기적으로는 Spine 위에서도 세션 관리, 트랜잭션, 메시징 같은 공통 문제를 다루는 에코시스템을 만들어가려고 한다. 다만, 그 방식은 자동화가 아니라 명시적인 실행 모델 위에 구조적으로 시스템을 얹는 방향이 될 것이다.
이 글로 내가 만든 프레임워크가 기존의 것들보다 더 낫다고 말하려는 것이 아니다. 대신 웹 프레임워크를 “요청은 어떻게 실행되고 있는가”라는 관점으로 다시 바라보게 된 계기와 그래서 무엇이 달라졌는지 적은 기록이다. Spine은 그 작은 질문을 끝까지 밀어붙여 만들어낸 결과물이다.
이러한 관점의 변화는, 지금 사용하고 있는 프레임워크를 조금 다른 시선으로 보게 할 출발점이 될 수 있다고 굳게 믿는다.

Spine은 아직 작은 프레임워크입니다. 하지만 요청과 실행을 하나의 모델로 고정하고, 그 위에 구조적으로 확장해 나갈 최소한의 기반은 갖췄다고 생각합니다. 이 관점이 의미 있다고 느껴진다면, 프로젝트를 살펴보고 이슈로 의견을 남기거나 기여로 함께해 주세요. 깃허브 스타도 큰 힘이 됩니다.
실행 흐름을 명시적으로 다루는 실험이 실제 생태계로 성장할 수 있을지는, 결국 이 문제의식에 공감하는 사람들의 참여에 달려 있습니다. 지금도 열심히 Spine을 개발하고 있으며, 다양한 공통 문제를 실행 모델 위에서 풀어가는 방향으로 계속 확장해 나갈 예정입니다.
이 글이 웹 프레임워크를 바라보는 여러분의 시선을 조금이라도 바꿨다면, Spine은 그 질문을 실제 코드와 구조로 바꾸어내는 하나의 답변이 될 것입니다.
©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.