대한민국은 ‘스프링 공화국’이다?
한국에서 백엔드 개발자로 취업하고 싶다면 피할 수 없는 질문이 있다. “스프링(Spring) 할 줄 아세요?”
채용 사이트를 들어가 보면 10곳 중 9곳은 자격 요건에 ‘Spring Boot 경험자’를 명시한다. 오죽하면 ‘스프링 공화국’이라는 말이 있을 정도다. 대학생 때 나는 이 현상이 그저 유행인 줄 알았다.
“그냥 자바(Java)로 짜면 되지, 왜 굳이 무거운 스프링을 써야 해?” “노드(Node.js)나 파이썬(Django)이 더 쉽지 않나?”
하지만 실무에 들어와 대규모 프로젝트를 겪어보니 알게 되었다. 스프링은 단순한 유행이 아니라, 거대하고 복잡한 엔터프라이즈 시스템을 ‘안전하고 빠르게’ 만들기 위해 수많은 선배 개발자들이 다듬어온 ‘표준 작업대’라는 것을.
이번 시리즈는 그 표준 작업대 위에서, 무너지지 않는 성(Monolith)을 쌓는 방법에 대한 이야기다.
프레임워크: 내 마음대로 못하는 이유
본격적인 기술 이야기 전에, 짚고 넘어가야 할 것이 있다. 도대체 프레임워크(Framework)가 뭘까? 라이브러리(Library)와는 뭐가 다를까?
지난 ‘Re: Booting’ 시리즈에서 잠깐 언급했지만, 이 둘의 결정적 차이는 ‘누가 주도권을 쥐고 있는가’에 있다.
- 라이브러리(Library): ‘도구’다. 내가 필요할 때 망치를 꺼내 쓰고, 톱을 가져다 쓴다. 집을 지을 때 내 마음대로 지을 수 있다. (예: React, jQuery)
- 프레임워크(Framework): ‘틀(Frame)’이다. 이미 기둥과 벽이 세워진 모델하우스다. 나는 정해진 규칙에 따라 벽지 색을 고르거나 가구를 배치할 뿐, 기둥을 마음대로 뽑을 순 없다.
스프링은 프레임워크다. 그래서 스프링을 쓴다는 건, “내 코딩 스타일을 버리고 스프링이 시키는 대로 하겠습니다”라고 서약하는 것과 같다. 이 ‘시키는 대로’의 핵심이 바로 오늘 이야기할 제어의 역전(IoC)이다.
“제발 new 좀 그만 쓰세요”
이 서약의 첫 번째 규칙은 충격적이었다. 바로 자바의 기본 중의 기본인 new 연산자를 쓰지 말라는 것이다.
학부 시절, 객체가 필요하면 MemberService service = new MemberService();라고 쓰는 게 상식이었다. 내가 필요한 물건을 내가 직접 사서(생성) 쓰는 것, 너무나 당연하지 않은가?
하지만 입사 후 스프링 부트 프로젝트를 열었을 때, 코드 어디를 찾아봐도 new 키워드가 보이지 않았다. 대신 @Autowired나 @RequiredArgsConstructor 같은 낯선 어노테이션들이 그 자리를 차지하고 있었다.
“아니, 객체 생성을 안 했는데 어떻게 메서드를 호출해? 이게 돌아가긴 해?”
선임 개발자에게 물어봤을 때 돌아온 대답은 알쏭달쏭했다. “스프링 컨테이너가 알아서 넣어주니까(Inject) 우리가 만들 필요 없어요. 오히려 직접 new를 쓰면 나중에 유지보수하기 힘들어져요.”
나는 이해할 수 없었다. 내가 짠 코드의 주인이 나인데, 왜 객체 하나 마음대로 못 만들게 하는 걸까? 이것이 스프링의 독재처럼 느껴졌다.

주방장과 재료 담당자 (IoC & DI)
이 독재 같은 규칙이 왜 필요한지 이해하기 위해, 우리의 세계관을 ‘레스토랑 주방’으로 바꿔보자.
1. 전통적인 방식 (내가 주인)
나는 요리사(Class)다. 요리를 하려면 칼(Dependency)이 필요하다.
- 행동: 내가 직접 마트에 가서 칼을 사 온다.
Knife knife = new Knife(); - 문제: 만약 내가 ‘일식도’를 샀는데, 갑자기 메뉴가 중식으로 바뀌어서 ‘중식도’가 필요해진다면? 칼을 쥐고 있는 내 손(코드)을 뜯어고쳐야 한다. 도구가 바뀔 때마다 요리사도 영향을 받는다. (강한 결합)
2. 스프링의 방식 (스프링이 주인)
나는 여전히 요리사다. 하지만 이번엔 ‘재료 담당 매니저(Spring Container)’가 있다.
- 행동: 나는 그냥 “칼 주세요”라고 외치기만 한다. 어떤 브랜드의 칼인지는 신경 쓰지 않는다.
- DI (Dependency Injection): 매니저가 내 손에 칼을 쥐여준다(Inject). 오늘은 일식도를 줄 수도 있고, 내일은 중식도를 줄 수도 있다.
- 장점: 칼이 바뀌어도 나는 코드를 고칠 필요가 없다. 나는 그저 쥐여준 칼로 요리만 하면 된다. (느슨한 결합)
이처럼 “내가 필요한 자원을 내가 직접 관리하지 않고, 외부(매니저)에서 관리해서 나에게 넣어주는 것”. 이것이 바로 제어의 역전(IoC: Inversion of Control)이고, 그 구체적인 방법이 의존성 주입(DI)이다.

[Code Verification] 왜 new가 위험한가
말로만 하면 감이 안 온다. 코드로 직접 비교해보자. 우리가 ‘카드 결제 시스템’을 만든다고 가정해보자.
1. 나쁜 예: 직접 생성 (강한 결합)
public class PaymentService {
// 내가 직접 '삼성카드'를 선택하고 생성했다. (new 사용)
private final SamsungCard card = new SamsungCard();
public void pay() {
card.payment(); // 삼성카드로만 결제 가능
}
}
문제점: 어느 날 사장님이 “우리 수수료 싼 현대카드로 바꾸자!”라고 지시했다. 그러면 나는 PaymentService 코드를 열어서 SamsungCard를 HyundaiCard로 다 뜯어고쳐야 한다. 만약 이 코드가 100군데에 있다면? 야근 확정이다.
2. 좋은 예: 의존성 주입 (느슨한 결합)
public class PaymentService {
private final Card card; // 구체적인 카드사를 명시하지 않는다. (인터페이스 사용)
// 생성자 주입: "누군가 밖에서 카드를 넣어주겠지"라고 믿는다.
public PaymentService(Card card) {
this.card = card;
}
public void pay() {
card.payment();
}
}
이제 PaymentService는 어떤 카드가 들어오든 상관없다.
- 스프링 설정(Configuration)에서 “현대카드 넣어줘”라고 한 줄만 바꾸면,
PaymentService코드를 단 한 줄도 건드리지 않고 결제 수단을 바꿀 수 있다.
이것이 실무에서 말하는 “유지보수하기 좋은 코드(OCP 원칙)”의 핵심이다. 스프링 부트를 쓰는 이유는 바로 이 ‘유연함’ 때문이다.
스프링 컨테이너: 객체들의 호텔
그렇다면 이 객체들을 생성하고 넣어주는 ‘매니저’는 도대체 누구일까? 바로 스프링 컨테이너(Spring Container)다.
우리가 스프링 부트 서버를 켜는 순간(run), 내부에서는 엄청난 일이 벌어진다.
- 스캔(Component Scan): 스프링이
@Component,@Service,@Repository가 붙은 클래스들을 샅샅이 찾는다. - 등록(Bean Registration): 찾은 클래스들의 객체를 딱 하나씩(Singleton)만 만들어서 자기만의 창고(Container)에 저장한다. 이것을 ‘빈(Bean)’이라고 부른다.
- 주입(Auto Wiring): 객체들끼리 필요한 관계를 파악해서, 짝을 맞춰준다.
우리가 할 일은 그저 클래스 위에 “이거 관리해 주세요”라는 스티커(@Service)를 붙이는 것뿐이다. 나머지는 스프링이라는 거대한 호텔 지배인이 알아서 관리해 준다.
실무 조언: 생성자 주입을 써라
스프링에서 의존성을 주입받는 방법은 여러 가지가 있다. (@Autowired 필드 주입, Setter 주입, 생성자 주입) 하지만 실무에서는 무조건 ‘생성자 주입’을 권장한다. (Lombok의 @RequiredArgsConstructor를 쓰는 그 방식이다.)
@Service
@RequiredArgsConstructor // 이게 바로 생성자 주입을 자동으로 만들어준다.
public class OrderService {
// final을 붙일 수 있어서 안전하다.
private final PaymentService paymentService;
}
이유:
- 불변성(Final): 한 번 주입되면 바꿀 수 없다. 앱 도중에 부품이 바뀔 일은 없어야 한다.
- 순환 참조 방지: A가 B를 원하고, B가 A를 원하는 ‘무한 대기’ 상태를 서버 띄울 때 바로 잡아준다.
- 테스트 용이성: 스프링 없이 순수한 자바 코드로 단위 테스트를 짤 때, 내가 직접 가짜 객체(Mock)를 넣어줄 수 있다.
마치며: 마법사가 아닌 지휘자
처음에는 스프링이 내 코드를 맘대로 휘두르는 것 같아서 거부감을 느꼈다. 하지만 이제는 안다. 스프링은 나를 괴롭히는 독재자가 아니라, 복잡한 객체 관계를 정리해 주는 유능한 지휘자라는 것을.
덕분에 나는 “어떤 객체를 만들지” 고민하는 시간을 줄이고, “이 객체로 비즈니스 로직을 어떻게 짤지”에만 집중할 수 있게 되었다. 이것이 대한민국 개발자들이 스프링에 열광하는 이유일 것이다.
자, 이제 객체 관리의 권한을 스프링에게 넘겼다. 서버를 띄울 준비는 끝났다. 그런데 웹 애플리케이션은 혼자 도는 게 아니다. 화면(Frontend)과 대화를 해야 한다. PHP 시절에는 파일 하나에 HTML과 코드가 섞여 있어서 편했는데, 요즘은 왜 프론트엔드(Vue, React)와 백엔드를 나눠서 개발할까? 그리고 왜 그 둘을 붙이면 빨간색 CORS 에러가 뜨는 걸까?
다음 시간에는 웹 아키텍처의 변화(SSR vs CSR)와 그 사이를 가로막는 CORS의 정체를 파헤쳐 보자.