
0. 들어가기 전에
지난번 백엔드 기초 글(백엔드 프로그래밍 편: 백엔드의 기초 파악하기)의 마무리에서, 나는 web.xml이나 매핑, 필터와 리스너, JSTL과 태그 라이브러리, MVC와 프론트 컨트롤러 패턴, 배포(WAR와 컨텍스트 경로) 같은 것들을 다음에 천천히 다룰 것 같다고 예고했다. 그런데 다루고자 하려니까 생각보다 더 빠르게 스프링을 접하게 된 것이다. 따라서 이번에는 프레임워크 개념과 DI(의존성 주입)까지 먼저 다루게 되었다.
그래서 이번 글의 위치를 이렇게 잡고 싶다. 예고했던 항목들이 사라진 것이 아니라, 도구 이름과 설정 방식이 스프링 쪽으로 옮겨진 상태에서 비슷한 문제를 다시 만나게 될 가능성이 크다. 예를 들어 MVC와 프론트 컨트롤러는 스프링에서 DispatcherServlet과 컨트롤러 흐름으로 다시 등장하고, 매핑은 어노테이션 기반으로 읽히는 비중이 커진다. web.xml 중심 이야기는 스프링 부트 환경에서는 자동 설정과 자바 설정, 어노테이션 쪽 설명이 더 두드러질 수 있다. JSTL과 태그는 뷰 기술 선택에 따라 다른 템플릿 쪽으로 이어질 수도 있다. 배포와 컨텍스트 경로는 여전히 중요하지만, 로컬에서 부트로 먼저 굴리고 나중에 WAR로 묶는 순서가 될 수도 있다.
또 한 가지, 나는 JDBC 편에서 JDBC가 JPA나 MyBatis의 바닥에 깔린다고 정리했었다. 스프링도 비슷한 방식으로 이해할 수 있다. 스프링은 웹 요청을 처리하는 방식과 객체를 만들고 연결하는 방식을 한데 묶어 주는 층이 강하고, 그 과정에서 데이터 접근도 같은 애플리케이션 구조 안에서 연결된다. 다만 이번 글에서 DB 연동을 끝까지 파고들지는 않는다. 이번 편의 중심은 프레임워크와 DI의 이론적 뼈대이고, 실무적으로는 왜 이렇게 조립하라고 하는지를 잡는 데 목적이 있다.
그리고 백엔드 기초 글에서 말했듯이, 나는 처음에는 실무 중요 포인트만 짧게 적는 방향으로 시작했고, 앞으로는 각 항목마다 실무 TIP 부분을 조금씩 채워나가겠다고 했다. 이번 글도 완전한 실전편이라기보다는, 그 연장선에서 실무에서 자주 나오는 판단 기준을 몇 군데 끼워 넣는 형태로 썼다. 다음 편부터는 MVC 흐름이나 설정이 더 잡히면 실무 노트 비중을 조금씩 늘릴 수 있을 것 같다.
1. 프레임워크의 정의
먼저 프레임워크라는 말부터 조금 길게 잡아보고 싶다. 개발자 커뮤니티에서 프레임워크는 흔히 “라이브러리보다 더 큰 것” 정도로만 설명되기도 하는데, 그런 설명만으로는 부족한 경우가 많다. 더 정확히 말하면 프레임워크는 보통 다음을 동시에 포함한다. 첫째, 애플리케이션의 큰 실행 흐름이나 구조를 미리 제시한다. 둘째, 개발자는 그 구조 안에서 훅(hook)으로 불리는 지점에 자신의 코드를 끼워 넣는다. 셋째, 반복되는 공통 문제(생명주기, 설정, 횡단 관심사 등)를 표준화해서 팀의 유지보수 비용을 낮추려는 목적이 크다.
이때 라이브러리와의 대비는 이렇게 잡는 편이 실무에 잘 맞는다. 라이브러리는 내가 필요할 때 가져다 쓰는 도구 모음에 가깝고, 호출의 주도권은 보통 내 코드 쪽에 있다. 반면 프레임워크는 큰 틀과 실행 흐름의 뼈대를 먼저 제공하고, 내 코드는 그 틀 안에서 “부품”처럼 동작하는 경우가 많다. 물론 경계가 항상 완전히 뚜렷한 것은 아니고, 실무에서는 둘을 섞어 쓰는 프로젝트도 흔하다. 그러나 학습 단계에서 이 구분을 한 번 잡아두면 이후에 왜 이렇게 제한적으로 코딩하게 되지라는 느낌이 설명된다.

그리고 프레임워크라는 말이 곧 마법을 뜻하지는 않는다. 프레임워크는 보통 공통으로 반복되는 뼈대를 표준화하고, 개발자는 그 위에서 비즈니스 규칙과 도메인 로직에 집중하게 만든다. 그래서 스프링을 처음 접하면 규칙이 많아 보이는데, 그 규칙은 종종 팀 단위로 유지보수 비용을 줄이기 위한 합의이기도 하다.
여기서 프레임워크가 자주 동반하는 개념이 제어의 역전이다. 역전이라는 말이 거창하게 들리지만, 실제로는 아주 현실적인 문제에서 출발한다. 예를 들어 예전에 서블릿을 배울 때도, 우리가 직접 소켓을 열고 HTTP를 파싱하지 않았다. 컨테이너가 요청을 받고 서블릿의 라이프사이클을 관리했다. 그것도 일종의 제어 역전이다. 즉 “내가 모든 호출을 끝까지 직접 조립한다”에서 “플랫폼이 공통을 맡고 내 코드는 그 안에서 호출된다”로 옮겨가는 감각이다.

그런데 프레임워크가 항상 정답은 아니다. 프레임워크는 보통 다음을 전제로 한다. 팀이 같은 구조를 공유할 수 있고, 문제가 생겼을 때 원인을 같은 맥락에서 찾을 수 있다. 반대로 프레임워크가 과하게 느껴질 때는 보통 이런 상황이다. 요구사항이 너무 단순해서 뼈대가 오히려 부담이 된다. 또는 팀의 규칙이 프레임워크의 관례와 충돌한다. 지금은 전부를 판단할 필요는 없다. 다만 프레임워크는 “협업 비용을 줄이기 위한 도구”라는 관점을 가져두면 이후 덜 답답해진다.
실무 TIP
프레임워크는 종종 편해서 쓰는 것으로만 보이기 쉽다. 그런데 팀 단위로는 팀이 합의한 구조를 강제하는 도구에 가까운 면도 있다. 그래서 스프링을 쓴다는 것은 단순히 어노테이션을 쓴다는 뜻이 아니라, 프로젝트가 객체를 조립하고 요청을 처리하는 방식을 공통 언어로 맞춘다는 뜻이 될 때가 많다.2. 스프링의 설계 및 작용
이제 스프링을 “프레임워크 하나”로만 이해하면 초반에 막히는 경우가 있다. 스프링은 역사적으로도 모듈성을 강하게 가져가는 편이고, 실무적으로는 “스프링이라는 이름 아래에 여러 기술이 같이 묶여 있다”는 느낌으로 접근하는 것이 더 안전하다. 다만 이번 글에서 스프링을 전부 끝까지 파헤치지는 않겠다. 지금 단계에서 필요한 건 이것이다. 스프링이 애플리케이션에서 어떤 역할을 맡기 위해 설계되었고, 그 결과로 개발자가 무엇을 덜 신경 쓰게 되는가이다.
스프링의 중심축 중 하나는 객체의 생성과 연결을 애플리케이션 코드 곳곳에 흩뿌리지 않고, 컨테이너가 담당하는 쪽으로 옮기는 데 있다. 이때 그 연결을 의존성 주입(DI)이라고 부르는 경우가 많다. 그리고 그 객체들을 스프링이 관리하는 단위로 빈(Bean)이라고 부르는 설명을 자주 듣는다. 즉 스프링은 “협력 객체들이 서로를 어떻게 참조할지”를 프레임워크가 조립하는 구조를 취하는 경우가 많다.

또 하나의 중요한 작용은, 웹 애플리케이션에서 흔히 요구되는 공통 관심사를 애플리케이션 구조 안에서 다루기 쉽게 만든다는 점이다. 예를 들어 MVC의 흐름, 요청을 어떤 핸들러로 보낼지, 예외를 어떻게 처리할지 같은 것들은 결국 스프링 MVC와 주변 구성요소로 이어진다. 이번 글에서는 MVC를 끝까지 설명하지 않지만, “스프링은 웹 요청 처리의 틀을 제공했다”는 사실만 기억해도 다음 편에서 이어 읽기 쉽다.
그리고 스프링은 마법처럼 WAS를 없애지 않는다. 백엔드 기초 글에서 말했듯이 웹이면 결국 서블릿 API와 WAS의 세계와 연결된다. 스프링 MVC에서도 DispatcherServlet 같은 서블릿 기반의 요소가 등장한다는 설명을 나중에 듣게 된다. 다만 개발자가 매일 의식하는 추상화 단계가 올라갈 뿐이다.
데이터 접근 측면에서도 JDBC 편에서 말했듯이, 상위 기술은 아래층을 완전히 사라지게 하지 않는다. 스프링에서 데이터 접근을 다룰 때도 결국 연결과 실행, 트랜잭션 경계 같은 문제가 다시 나온다. 그래서 나는 스프링을 “DB를 대신해 주는 기술”이 아니라, “애플리케이션의 구조와 흐름을 정리하는 데 강한 도구”로 먼저 잡는 편이 이해에 도움이 된다고 생각한다.

실무 TIP
스프링을 처음 접하면 어노테이션만 많아진다고 느낄 수 있다. 그런데 팀 단위로는 “어떤 역할을 어떤 클래스에 둘지”가 더 중요해진다. @Service, @Repository, @Controller 같은 스테레오타입은 기능이 완전히 달라서 이름이 다른 것이 아니라, 책임의 의도를 드러내기 위한 경우가 많다. 지금은 이름을 외우는 것보다, “이 클래스는 애플리케이션에서 어떤 책임을 맡는가”를 먼저 말할 수 있게 만드는 쪽이 중요하다.
3. IoC와 DI는 같은 말일까?
여기서부터는 용어를 조금 엄밀하게 잡아보고 싶다. IoC(Inversion of Control, 제어의 역전)와 DI(Dependency Injection, 의존성 주입)는 책마다 같은 장에 나오고, 강의에서도 한 묶음으로 설명되는 경우가 많다. 그런데 엄밀히 말하면 같은 단어는 아니다.
IoC는 제어권이 뒤집힌다는 넓은 원리에 가깝다. 예를 들어 예전에 서블릿을 배울 때도, 우리가 직접 소켓을 열고 HTTP를 파싱하지 않았다. 컨테이너가 요청을 받고 서블릿의 라이프사이클을 관리했다. 그것도 일종의 제어 역전이다. 즉 IoC는 스프링만의 독점 개념이라기보다, 프레임워크가 자주 가져가는 큰 패턴이다.
DI는 그중에서도 의존 관계를 어떻게 연결할 것인가에 초점을 둔, 더 구체적인 기법이다. 내가 필요로 하는 객체(의존성)를 내가 직접 생성하지 않고, 바깥에서 주입받는 방식을 말한다. 스프링에서는 이런 주입을 도와주는 컨테이너가 있고, 그 컨테이너가 관리하는 객체를 빈(Bean)이라고 부르는 경우가 많다.
여기서 의존성이라는 말이 헷갈릴 수 있다. 나쁜 뜻이 아니다. A가 B를 사용해야만 제 기능을 완성한다면, A는 B에 의존한다고 말한다. 문제는 의존 자체가 아니라, 의존이 구체 구현에 고정되면 변경과 테스트가 어려워진다는 점이다. 그래서 DI 이야기는 종종 인터페이스와 구현 분리 이야기와 같이 나온다.
그리고 한 단계만 더 나아가면, 객체지향 설계에서 자주 등장하는 의존성 역전 원칙(DIP)과도 연결된다는 설명을 듣는 경우가 많다. 핵심은 이렇게 요약할 수 있다. 상위 수준의 정책이 하위 수준의 구체 구현에 직접 매달리지 않도록, 추상화에 의존하라는 쪽에 가깝다. 다만 이번 글에서 SOLID를 끝까지 전개하지는 않겠다. 지금 단계에서 필요한 건 이것이다. DI는 “연결을 누가 맺느냐”의 문제이고, DIP는 “무엇에 의존하는 것이 더 안전한가”의 문제다. 둘이 같이 등장하는 이유는 실무에서 결합을 줄이는 방향으로 자주 같이 움직이기 때문이다.

실무 TIP
DI를 처음 배울 때 가장 큰 오해는 이것이다. 인터페이스를 쓰면 DI인가? 그렇지 않다. 인터페이스는 결합을 느슨하게 만드는 도구일 뿐이고, DI는 그 의존성을 누가 언제 연결해 주느냐의 문제다. 인터페이스 없이도 주입은 가능하고, 인터페이스가 있어도 여전히 구체 클래스에 고정되면 이점이 줄어든다.
4. 스프링 컨테이너와 빈: 관리의 중요성
스프링을 배우면 ApplicationContext 같은 이름을 만난다. 이름이 길고 무섭게 느껴질 수 있는데, 직관적으로는 이렇게 이해해도 된다. 객체를 만들고, 생명주기를 관리하고, 서로 연결해 주는 일을 담당하는 쪽이 컨테이너에 가깝다.
빈은 스프링 컨테이너가 관리하는 객체로 이해하면 편하다. 그리고 기본적으로 많은 빈은 싱글톤 스코프로 관리된다는 설명을 자주 듣는다. 여기서 싱글톤이라는 말이 디자인 패턴 책의 싱글톤과 완전히 같은 뉘앙스는 아닐 수 있지만, 학습 초반에는 이렇게 받아들여도 실무 감각에 도움이 된다. 애플리케이션에서 자주 쓰는 객체를 요청마다 새로 다 만들지 않고 공유할 수 있어 자원 측면에서 유리한 경우가 많다.
다만 스코프 이야기는 여기서 끝까지 파고들 필요는 없다. 중요한 건 이 한 가지다. new의 위치가 바뀌면 객체의 생명주기와 공유 범위도 함께 바뀐다. 그래서 스프링을 쓰면 디버깅할 때 이 객체가 언제 만들어졌고, 누가 같은 인스턴스를 보고 있지 같은 질문이 생긴다.
또 하나 짚고 싶다. 스프링에는 @Component, @Service, @Repository, @Controller 같은 스테레오타입 어노테이션이 등장한다. 이름이 다르게 붙는 이유는 “역할의 의도”를 드러내기 위한 경우가 많다. 예를 들어 Repository는 데이터 접근 계층의 의도를, Service는 애플리케이션 서비스 계층의 의도를 강조하는 식이다. 기능이 완전히 달라서가 아니라, 팀이 코드를 읽을 때 빠르게 맥락을 잡게 하려는 목적이 크다. 지금은 어노테이션의 이름을 외우는 것보다, “이 클래스는 애플리케이션에서 어떤 책임을 맡는가”를 먼저 말할 수 있게 만드는 쪽이 중요하다.

실무 TIP
스프링을 처음 접하면 그냥 @Autowired만 붙이면 되지 같은 느낌이 들 수 있다. 그런데 운영 이슈가 생기면 결국 컨테이너가 관리하는 그래프가 문제로 돌아온다. 예를 들어 테스트에서 특정 구현을 바꿔 끼우고 싶을 때, 생성자 주입이면 대체가 쉬운 편이다. 반면 숨은 의존성이 많으면 작은 변경이 예상 밖의 클래스까지 번지기도 한다.
5. 의존성 주입 방식: 생성자, 세터, 필드
DI를 구현하는 대표적인 방법은 생성자 주입, 세터 주입, 필드 주입으로 나누어 설명하는 경우가 많다. 각각은 코드 형태가 다르고, 팀 컨벤션 논쟁이 생기기도 한다.
생성자 주입은 객체가 만들어질 때 필요한 의존성이 함께 채워지도록 강제하기 쉽다는 장점이 있다. 필수 의존성이 누락되면 객체 생성 단계에서 실패하기 때문에, 잘못된 상태의 객체가 런타임 중간까지 살아남는 문제를 줄이는 데 도움이 된다.
세터 주입은 선택적 의존성을 넣기 쉽게 보일 수 있지만, 그만큼 객체가 반쯤 만들어진 상태로 존재할 시간이 길어질 수 있다. 필드 주입은 작성은 빠르게 느껴질 수 있지만, 테스트에서 의존성을 직접 채워 넣기 불편해지는 경우가 있고, 불변성 측면에서 불리할 수 있다.
그래서 많은 가이드가 생성자 주입을 권장한다고 말한다. 물론 프로젝트마다 예외는 있다. 다만 왜 권장이 나왔는지를 알고 선택하는 것과, 그냥 따라 치는 것은 다르다.
예시는 개념 확인용으로만 짧게 적겠다. 실제 프로젝트 패키지나 설정은 수업 환경에 맞게 바꾸면 된다.
@Service
public class OrderService {
private final PaymentClient paymentClient;
public OrderService(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
public void placeOrder() {
paymentClient.pay();
}
}
위 코드에서 OrderService는 PaymentClient를 생성자를 통해 받는다. OrderService 내부에서 new PaymentClient()를 하지 않는다는 점이 핵심이다. PaymentClient의 구체 구현이 무엇이든, 조립은 컨테이너 쪽에서 맞춰 줄 수 있다.
@Component
public class PaymentClient {
public void pay() {
// ...
}
}
실무 TIP
레거시 코드나 예제에서 @Autowired를 필드에 붙인 형태를 볼 수 있다. 그것이 항상 틀렸다는 뜻은 아니지만, 팀이 점점 생성자 주입으로 수렴하는 이유는 테스트와 명시성 때문인 경우가 많다. 신규 코드부터 습관을 잡아두면 나중에 덜 아프다는 말이 자주 나온다.

추가 실무 TIP
테스트 코드를 아직 많이 쓰지 않았더라도, DI를 이해하는 가장 빠른 방법 중 하나는 “내가 이 클래스를 단독으로 new 해서 테스트할 수 있는가?”를 스스로에게 묻는 것이다. 생성자 주입은 그 질문에 답을 쉽게 만들어 준다. 반면 필드 주입은 리플렉션이나 테스트 프레임워크의 도움 없이는 불편해질 수 있다.
6. 마무리하며
이번 글은 프레임워크의 정의에서 출발해서, 스프링이 애플리케이션에서 어떤 설계를 전제로 어떤 작용을 하는지를 넓게 잡고, 그다음에 IoC와 DI, 컨테이너와 빈, 주입 방식으로 좁혀 정리했다. 백엔드 기초 글에서 예고했던 web.xml과 JSTL, WAR 이야기는 사라진 것이 아니라, 스프링이라는 도구를 통해 같은 문제를 다른 방식으로 만나게 될 가능성이 크다.
그리고 JDBC 편에서 말했듯이, 상위 기술은 결국 아래층을 완전히 사라지게 하지 않는다. 스프링도 결국 JVM 위에서 돌아가고, 웹이면 서블릿 API와 WAS의 세계와 연결된다. 다만 개발자가 매일 의식하는 추상화 단계가 올라갈 뿐이다.
다음 편에서는 아마도 DispatcherServlet 중심의 요청 흐름, 혹은 @Controller와 뷰 연결, 혹은 스프링 부트의 실행 구조 같은 주제로 이어가게 될 것 같다. 아직 수업 진도에 맞춰 조정할 예정이다. 그리고 백엔드 기초 글에서 약속했던 것처럼, 실무 노트와 TIP는 항목이 늘어날수록 조금씩 두꺼워지게 쓰고 싶다.
이미지 출처
※ 모든 이미지는 직접 제작하거나 저작권 문제가 없는 이미지로 제작되었습니다.
'부트캠프 일지 > 멀티캠퍼스 TIL' 카테고리의 다른 글
| [Java 풀스택 개발자] 백엔드 프로그래밍 편: 백엔드의 기초 파악하기 (0) | 2026.03.24 |
|---|---|
| [Java 풀스택 개발자] 시큐어 코딩편: 개발자가 알아야 할 웹 보안 (0) | 2026.03.17 |
| [Java 풀스택 개발자] 알고리즘 편#2: 알고리즘 방법론 (0) | 2026.03.09 |
| [Java 풀스택 개발자] 알고리즘 편: 무엇이 좋은 알고리즘일까? (0) | 2026.03.03 |
| [Java 풀스택 개발자] JDBC 편 : Java와 DB의 Connection, JDBC (0) | 2026.02.24 |
