처음 자바를 배웠던 것은 대학생 시절 객체지향 프로그래밍을 들었을 때였습니다. 자바라는 언어 자체를 배우는 과목은 아닌지라 언어 자체의 스펙을 깊이 있게 가르쳐주는 시간은 없었지만 많은 대학 과목들이 그렇듯이 과제를 제출하기 위해서는 따로 공부해야할 필요가 있었습니다.

학교 도서관에서 ‘자바의 정석’ 을 빌려 보았는데, Java 1.5 정도를 기준으로 쓰여 있던 것으로 기억합니다. 그 당시의 책에서 설명하는 자바 어노테이션 (Annotation) 은 말 그대로 코드의 메타데이터로서의 역할을 하는 ‘주석’ 이었습니다. @Override, @Deprecated 같은 IDE 나 컴파일러를 위한 일종의 힌트였죠.

시간이 흘러 저는 프로그래머가 되었고 스프링 기반의 웹 어플리케이션 개발을 하게 되었습니다. 그 당시 팀에서 진행하고 있던 스프링 프로젝트들은 XML 기반의 설정을 사용하는 레거시 프로젝트와 스프링 부트 2 를 사용하고 있는 비교적 최근에 시작한 프로젝트가 섞여 있었죠.

그나마 운 좋게도 저는 스프링 부트 기반의 프로젝트에 투입되었고 신입 개발자로서 주어지는 암묵적 유예기간 (?) 내에 스프링 기반 프로젝트를 이해해야 하는 업무가 주어졌습니다. 그때 스프링을 처음 공부하면서 저를 괴롭혔던 것은 다름아닌 스프링의 마법같은 어노테이션들과 자동 설정들이었습니다.

도대체 어디서 무슨일이 벌어지는거야?

‘프로젝트를 이해하는 가장 좋은 방법은 코드를 읽는것’ 이라는 당찬 목표는 메인 메서드 앞에서 바로 무너졌습니다. 메인 메서드에서는 스프링 프레임워크의 정적 메서드 호출 말고는 아무 것도 찾을 수 없었습니다. run 메서드와 @SpringBootApplication 어노테이션 선언으로 점프해 봐도 HTTP 요청은 어떻게 엔드포인트와 매핑되고 핸들링 되는지, 데이터베이스와 엔티티는 어떻게 연결되는지 알아 낼 수가 없었습니다.

@SpringBootApplication
public class WebApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebApplication.class, args);
    }
}

결국 스프링을 사용하기 위해서는 스프링의 문서를 찾아봐야 했고 spring-web, spring-data-jpa 등에서 ‘마법’ 처럼 처리해주는 것들이 무엇인지 알고 있어야 했습니다. 시간이 흐르고 스프링에 익숙해지고 난뒤 프레임워크 저편에서 이루어지는 ‘마법’ 에 대해 알게된 후에도 스프링과 친해졌다는 느낌은 잘 들지 않았습니다. 트러블 슈팅을 할 때에는 여전히 아직 잘 모르고 있는 스프링의 방대한 마법들을 문서와 구글링을 통해 알아내야 했습니다.

흑마법을 너무 가까이하지 말게나

스프링은 setter 없는 클래스 필드에 접근해 값을 세팅할 수도 있고, 메서드명만 선언된 인터페이스만으로도 데이터베이스에서 데이터를 쿼리하는 구체 클레스를 만들어 낼 수도 있습니다. 특정 어노테이션이 달린 클래스와 메서드에는 특정한 동작이나 추가 구현을 (개발자 모르게) 덧붙일수도 있죠. 처음에는 이런 스프링의 마법들이 너무 편리하고 신기했지만 개발하고 있는 프로젝트들이 저라는 개발자의 영향에서 너무나 벗어나기 쉬운 존재가 되었다고 느꼈습니다.

단순히 클래스나 필드명을 바꾸거나 패키지 경로를 바꾸는 것 만으로도 코드 상에서 확인할 수 있는 의존 관계나 동작이 바뀌지 않았음에도 불구하고 런타임 동작에 영향을 주기도 하고 새로운 스프링 생태계 기반의 라이브러리를 사용할 때 주의를 기울이지 않으면 빈 우선순위나 의존관계가 마음대로 뒤바뀌어 예측하지 못하는 일들이 벌어지곤 했습니다. 결국 일정 수준 이상의 통합 테스트가 강제되었고, 효율적으로 시스템 자원을 활용하는지 알기 어려운 코드가 되엇습니다. 또 문제가 발생할 때마다 정확히 왜 해결되는지 모르는 임시 대책성 솔루션들이 코드에 늘어나기도 했죠.

그게 정말 스프링 탓인가

혹자는 이러한 문제들이 제가 정확히 스프링을 이해하지 못하고 사용했기 때문이라고 말할지도 모르겠습니다. 저도 일정부분 개발자의 책임이 있다고 생각하지만 문서를 통해 동작을 파악해야만 하는, 코드를 통해 동작을 어렵게 만드는 어노테이션, 리플렉션 기반의 스프링 구현이 이러한 어려움을 심화시켰다고 생각합니다. (끔찍한 스프링 공식문서는 덤) 스프링이 구현하고자 하는 프레임워크의 철학은 너무나 멋지지만 자바라는 언어의 태생적인 한계로 인해 일종의 핵과 트릭으로 스프링이 구현되었기 때문이기도 하고요.

# https://fastapi.tiangolo.com

from typing import Union
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}

스프링 컨트롤러와 비슷한 방식으로 API 핸들러를 세팅하는 파이썬의 FastAPI 프레임워크를 살펴보면 스프링의 문제가 무엇인지 조금 명확하게 드러납니다. 언뜻 보면 스프링의 @RequestMapping 과 동일한 방식으로 핸들러가 세팅되고 있다고 생각할수도 있겠지만 위의 코드의 메서드 위에 달려 있는것은 자바의 어노테이션 (Annotation) 과는 전혀 다른 데코레이터 (Decorator) 라고 부르는 기능입니다.

데코레이터는 그 자체가 메서드를 파라미터로 받는 메서드로 파라미터로 받는 메서드 전/후에 추가적인 기능을 덧붙일 수 있는 고차 함수입니다. 때문에 코드 네비게이션으로 @get 데코레이터가 어떤 기능을 제공하는지 명확하게 살펴 볼 수 있고 FastAPI 메인 클래스까지 따라가면서 HTTP 요청이 어떻게 핸들링되고 파라미터가 어떻게 변환되어 전달되는지 확인할 수 있습니다. 많은것이 암시적이고 코드 외부의 정보를 알고 있어야만 제대로 활용할 수 있는 스프링과 대조적인 점이죠.

그래도 스프링

그래도 여전히 저는 현업에서 스프링을 사용합니다. 언급한 단점들에도 불구하고 스프링이 제공하는 기능은 너무나 강력하고 개발자가 비즈니스 로직 개발에만 집중할 수 있도록 도와주는 프레임워크인 것은 확실합니다. 닭과 달걀의 문제이긴 하지만 스프링 개발자가 가장 구하기 쉽다는 현실적인 문제도 있고요.

하지만 여전히 어노테이션과 리플렉션을 적극적으로 활용하는 스프링 프레임워크가 개발자에게 좋은 프레임워크인가에는 의문이 남습니다. 자바 개발자가 아닌 스프링 개발자라고 불러야 할 만큼 스프링 개발에는 높은 진입장벽이 있고 주의깊게 신경쓰지 않으면 프로젝트 전체가 스프링에 종속적인 코드가 되기 쉽기 때문입니다.

그래서 스프링 코어 외에 스프링 생태계의 라이브러리를 사용할 때는 직접 구현하는 것에는 명확한 장점이 있는지 항상 살피고 도메인 로직이 스프링 의존성으로부터 최대한 격리된 코드를 짜기 위해 노력하는 편입니다. 자바가 아닌 코틀린으로 새로 시작한다면 ktor 와 같은 경량 프레임워크로 시작하는것도 좋은 대안이 될 수 있을것 같습니다.