웹 브라우저가 보낸 HTTP 요청을 서버에서 어떻게 받아 처리하고 응답하는지, 그리고 그 과정을 스프링이 어떻게 도와주는지 살펴본다. 자바 웹 기술이 서블릿에서 시작해 스프링 부트까지 발전해 온 흐름을 이해하면, 스프링이 "무엇을 대신해 주는 프레임워크인지"가 분명해진다.
1. 웹 서버와 웹 애플리케이션 서버(WAS)
웹 서버 (Web Server)
HTTP 기반으로 동작한다.
정적 리소스(HTML, CSS, JS, 이미지, 영상)를 제공한다.
예) Nginx, Apache
웹 애플리케이션 서버 (WAS)
HTTP 기반으로 동작한다.
웹 서버 기능(정적 리소스 제공)을 포함한다.
프로그램 코드를 실행해서 애플리케이션 로직을 수행한다.
동적 HTML, HTTP API(JSON)
서블릿, JSP, 스프링 MVC
예) Tomcat, Jetty, Undertow
Spring Boot와 WAS
스프링 부트는 Tomcat을 내장하고 있어, 빌드 결과물(jar) 안에 WAS가 함께 들어간다. 그래서 별도의 서버 설치 없이 java -jar만으로 실행할 수 있다.
2. 서블릿(Servlet)
서블릿은 자바로 웹 요청을 처리하는 표준 기술이다. WAS가 요청·응답 객체를 만들어 서블릿을 호출한다.
HTTP 요청 시 서블릿의 흐름
WAS는
Request,Response객체를 만들어서 서블릿 객체를 호출한다.개발자는
Request객체에서 HTTP 요청 정보를 편리하게 꺼내서 사용한다.개발자는
Response객체에 HTTP 응답 정보를 편리하게 입력한다.WAS는
Response객체에 담긴 내용으로 HTTP 응답 메시지를 생성한다.
HttpServletRequest — 요청 메시지를 파싱하는 객체
영역 | 내용 |
|---|---|
START LINE | HTTP 메소드, URL, 쿼리 스트링, 스키마·프로토콜 |
헤더 | 모든 요청 헤더 |
바디 | form 파라미터 형식 조회, message body 데이터 직접 조회 |
HttpServletResponse — 응답 메시지를 만드는 객체
HTTP 응답 코드 지정
헤더 생성
바디 생성
3. HTTP 요청/응답 데이터
HTTP 요청 데이터 3가지
방식 | 설명 | 사용 예 |
|---|---|---|
GET — 쿼리 파라미터 | 메시지 바디 없이 URL의 쿼리 파라미터로 데이터 전달 | 검색, 필터, 페이징 |
POST — HTML Form |
| 회원가입, 상품주문 |
HTTP message body | JSON/XML/TEXT를 바디에 직접 담아 전달 (주로 JSON) | HTTP API (POST, PUT, PATCH) |
HTTP 응답 데이터 3가지
단순 텍스트
HTML (정적 리소스 또는 동적 생성)
API JSON
4. HTML, HTTP API, SSR, CSR
정적 리소스: 고정된 HTML, CSS, JS, 이미지, 영상 등을 그대로 제공한다.
동적 페이지: 템플릿 엔진으로 필요한 HTML을 동적으로 생성하여 전달한다.
HTTP API: HTML이 아니라 주로 데이터(JSON)를 전달한다. 다양한 시스템에서 호출한다.
SSR vs CSR
HTML 최종 결과를 서버에서 만들어 브라우저에 전달한다.
주로 정적인 화면에서 사용한다.
관련 기술: JSP, Thymeleaf
HTML 결과를 자바스크립트로 브라우저에서 동적으로 생성한다.
주로 동적인 화면에 사용한다. 웹을 앱처럼 부분부분 변경할 수 있다.
예) 구글 지도, Gmail, 구글 캘린더
관련 기술: React, Vue.js
이 강의의 방향
이 강의는 SSR(Thymeleaf) 을 중심으로 진행한다. 서버가 HTML을 완성해서 내려주는 방식이라, 스프링 MVC의 동작을 눈으로 따라가기 좋다.
5. 자바 웹 기술의 역사
timeline
title 자바 웹 기술의 발전
1997 : 서블릿 (HTML 생성이 어려움)
1999 : JSP (HTML은 편하지만 비즈니스 로직까지 떠안음)
2000년대 초 : 서블릿+JSP MVC 패턴
: MVC 프레임워크 춘추전국 (스트럿츠, 웹워크, 과거 스프링 MVC)
2010년대 : 어노테이션 기반 스프링 MVC (@Controller)
현재 : 스프링 부트 (서버 내장, 빌드·배포 단순화)서블릿(1997): HTML 생성이 어렵다.
JSP(1999): HTML 생성은 편리하지만, 비즈니스 로직까지 너무 많은 역할을 담당한다.
서블릿 + JSP 조합 MVC 패턴: 모델·뷰·컨트롤러로 역할을 나누어 개발한다.
MVC 프레임워크 춘추전국 시대(2000년대 초~2010년대 초): 스트럿츠, 웹워크, 과거 스프링 MVC 등.
어노테이션 기반 스프링 MVC 등장:
@Controller로 춘추전국 시대를 마무리한다.스프링 부트의 등장: WAS를 내장해 빌드 결과(
jar)에 서버를 포함 → 빌드·배포가 단순해진다.
6. 스프링의 탄생과 핵심 원리 — IoC / DI / AOP { #6-ioc-di-aop }
스프링을 "왜 쓰는가"는 결국 IoC·DI·AOP 세 가지로 설명된다. 그런데 이 원리들은 어느 날 갑자기 나온 게 아니라, 그 전 시대의 무겁고 복잡한 기술에 대한 반작용으로 태어났다. 그 배경을 먼저 보면 세 원리가 왜 그런 모습인지 자연스럽게 이해된다.
스프링은 왜 태어났나 — EJB와 로드 존슨
스프링이 등장하기 전, 자바 엔터프라이즈 애플리케이션의 표준은 EJB(Enterprise JavaBeans) 였다. EJB는 트랜잭션·보안·분산 처리처럼 어려운 일을 컨테이너가 대신 처리해 준다는 강력한 약속을 내걸었다.
문제는 그 대가가 너무 컸다는 점이다.
내 비즈니스 객체가 EJB가 정한 특정 인터페이스를 구현하고 컨테이너 클래스를 상속해야 했다 → 코드가 EJB 프레임워크에 강하게 종속된다.
설정용 XML 서술자가 방대했고, 작성할 코드가 많아 개발이 번거롭고 느렸다.
객체를 EJB 컨테이너 밖에서는 실행·테스트할 수 없었다 → 가벼운 단위 테스트가 사실상 불가능했다.
2002년, 로드 존슨(Rod Johnson) 은 저서 《Expert One-on-One J2EE Design and Development》에서 "이 모든 걸 EJB 없이, 평범한 자바 객체(POJO) 와 가벼운 컨테이너만으로 더 단순하게 할 수 있다"고 주장하며 직접 만든 프레임워크 코드를 함께 실었다. 이 코드가 곧 스프링 프레임워크의 씨앗이 되어 2004년 정식 출시(1.0)되었다. (이듬해 그의 다음 책 제목은 아예 《… without EJB》였다.)
POJO가 뭔가요?
POJO(Plain Old Java Object) 는 말 그대로 "평범한 옛날 자바 객체", 즉 특정 프레임워크나 기술에 종속되지 않은 순수한 자바 객체를 뜻한다. 특정 클래스를 상속하거나 특정 인터페이스를 구현하도록 강요받지 않고, 그냥 필드·생성자·메서드로만 이루어진 보통의 클래스다.
// POJO — 아무 기술에도 묶이지 않은 평범한 객체
public class Member {
private String name;
public String getName() { return name; }
}
// POJO가 아님 — EJB 프레임워크 클래스를 상속/구현해야만 동작
public class MemberBean extends SessionBean implements EntityBean {
// 프레임워크가 요구하는 메서드들을 억지로 구현 ...
}EJB처럼 "프레임워크에 묶인 무거운 객체"와 대비되는 말로, 2000년 무렵 마틴 파울러 등이 "평범한 객체에도 그럴듯한 이름을 붙이자"며 만들어낸 용어다. 스프링은 이 POJO를 그대로 두고 컨테이너가 관리해 주므로, 코드가 스프링에 종속되지 않는다.
스프링이 내놓은 해법은 이렇게 요약된다.
EJB (무거운 컨테이너) | 스프링 (가벼운 컨테이너) | |
|---|---|---|
내 코드 | 프레임워크 인터페이스·상속 강제 | POJO (평범한 자바 객체) |
객체 생성·연결 | 컨테이너에 종속 | IoC 컨테이너 + DI 로 외부에서 주입 |
트랜잭션 등 공통 기능 | 컨테이너가 침투적으로 제공 | AOP(프록시)로 코드 밖에서 적용 |
테스트 | 컨테이너 없이는 곤란 | 순수 자바로 빠르게 |
즉 EJB가 주던 이점(트랜잭션 등)은 그대로 누리되, 내 코드가 무거운 컨테이너에 묶이지 않도록 뒤집은 것이 스프링이다. 이어서 볼 IoC·DI·AOP가 바로 그 해법을 떠받치는 세 기둥이다.
스프링의 한마디 — POJO로 돌아가자
스프링의 철학은 "엔터프라이즈 기능을 쓰자고 내 코드를 프레임워크에 종속시키지 말고, 순수한 자바 객체를 그대로 두라"는 것이다. 그래서 우리가 만드는 서비스·리포지토리가 특별한 상속·구현 없이도 빈으로 동작한다.
IoC (Inversion of Control) — 제어의 역전
전통적인 자바 코드에서는 객체가 필요한 다른 객체를 직접 생성한다. 즉, 제어의 흐름을 내 코드가 쥐고 있다.
// 전통적 방식: 컨트롤러가 의존 객체를 직접 생성한다
public class OrderController {
// 구현체를 직접 new → OrderController가 OrderServiceImpl에 강하게 묶인다
private OrderService orderService = new OrderServiceImpl();
}이 방식의 문제는 강한 결합(tight coupling) 이다.
구현체를
OrderServiceImpl→OrderServiceImplV2로 바꾸려면 컨트롤러 코드를 고쳐야 한다.테스트할 때 가짜(mock) 객체를 끼워 넣기 어렵다.
객체를 누가 언제 만들고 없앨지를 일일이 코드로 관리해야 한다.
IoC는 이 제어권을 스프링 컨테이너(IoC 컨테이너) 에게 넘기는 것이다. 객체의 생성·연결·생명주기를 컨테이너가 대신 관리한다. 컨테이너가 관리하는 객체를 빈(Bean) 이라고 한다.
graph LR
A["내 코드<br/>(필요하다고 요청만)"] -->|객체 요청| B[스프링 IoC 컨테이너]
B -->|생성·관리| C["빈(Bean)들<br/>OrderService, Repository ..."]
B -->|주입| A제어의 '역전'이란
내가 new로 객체를 만들고 흐름을 제어하던 것을, 프레임워크가 대신 만들어 주고 필요한 시점에 내 코드를 호출하는 구조로 뒤집혔다는 뜻이다. ("Don't call us, we'll call you")
싱글톤 — 컨테이너는 빈을 하나만 만들어 공유한다
그렇다면 컨테이너는 빈을 몇 개 만들까? 웹 애플리케이션은 보통 같은 기능을 수많은 사용자가 동시에 요청한다. 요청이 올 때마다 OrderService·Repository를 new로 새로 만들면, 초당 수백·수천 개의 객체가 생성·폐기되어 메모리와 CPU가 낭비된다.
싱글톤(Singleton)이란
어떤 클래스의 인스턴스가 프로그램 전체에서 단 하나만 존재하도록 보장하는 것을 말한다. 'single'(하나) + '-ton'에서 온 말로, 객체를 아무리 여러 번 요청해도 항상 똑같은 하나의 인스턴스를 돌려준다. 설정값 관리자, 로그 기록기, 커넥션 풀처럼 여러 번 만들 필요가 없고 모두가 공유해도 되는 객체에 적합하다. 원래는 객체지향 디자인 패턴의 하나(GoF 싱글톤 패턴)이지만, 스프링에서는 개발자가 직접 구현하지 않아도 컨테이너가 이를 대신 보장해 준다.
그래서 스프링 컨테이너는 빈을 기본적으로 싱글톤(singleton) 으로 관리한다. 컨테이너당 빈을 딱 하나만 만들어 두고, 그 하나를 필요한 모든 곳에 공유해서 주입한다.
graph LR
R1[요청 A] --> S
R2[요청 B] --> S
R3[요청 C] --> S
S["MemberService<br/>(인스턴스 단 하나)"] --> R["MemberRepository<br/>(인스턴스 단 하나)"]@Service
public class MemberService { ... } // 평범한 클래스 — 싱글톤 코드는 한 줄도 없다
// 컨테이너는 MemberService 빈을 하나만 만든다 → 어디서 주입받아도 같은 객체
@Autowired MemberService a;
@Autowired MemberService b;
// a == b → true (동일한 인스턴스)직접 만드는 싱글톤 패턴 vs 스프링
전통적인 싱글톤 패턴은 클래스가 스스로 인스턴스를 하나로 강제한다.
public class SettingsManager {
private static final SettingsManager INSTANCE = new SettingsManager();
private SettingsManager() {} // 외부에서 new 차단
public static SettingsManager getInstance() { return INSTANCE; }
}하지만 이 방식은 문제가 많다.
싱글톤 유지용 코드(
private생성자,getInstance)가 클래스마다 반복된다.getInstance()로 구체 클래스에 직접 의존하게 되어, 인터페이스 기반 DI·구현체 교체·테스트가 어렵다.테스트에서 가짜(mock) 객체로 바꿔 끼우기 까다롭다.
스프링은 평범한 클래스를 그대로 두고 컨테이너가 싱글톤을 보장하므로 이 단점이 모두 사라진다. 그래서 스프링 컨테이너를 싱글톤 컨테이너라고도 부른다.
싱글톤이라 상태(state)를 가지면 안 된다
빈 하나를 여러 요청(스레드)이 동시에 공유한다. 따라서 빈에 요청마다 달라지는 값을 필드로 저장하면 안 된다. 한 사용자가 바꾼 값을 다른 사용자가 덮어쓰는 동시성 버그가 생긴다. 빈은 무상태(stateless) 로 설계하고, 요청별 데이터는 필드가 아니라 파라미터·지역변수로 다룬다.
기본은 싱글톤, 다른 스코프도 있다
빈 스코프의 기본값이 싱글톤일 뿐, 요청마다 새 객체가 필요하면 @Scope("prototype"), 웹 요청·세션 단위(request, session) 스코프도 쓸 수 있다. 다만 대부분의 서비스·리포지토리는 무상태라 싱글톤으로 충분하다.
DI (Dependency Injection) — 의존성 주입
IoC를 실제로 구현하는 방법이 DI다. 객체가 필요로 하는 의존 객체를 직접 만들지 않고, 외부(컨테이너)에서 넣어 준다.
@Controller
public class OrderController {
private final OrderService orderService; // 인터페이스에 의존
// 생성자 주입: 스프링이 OrderService 타입의 빈을 찾아 넣어 준다
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
}@Service // 이 구현체를 빈으로 등록
public class OrderServiceImpl implements OrderService {
// ...
}이렇게 하면 OrderController는 인터페이스(OrderService)에만 의존하고, 어떤 구현체가 들어올지는 컨테이너가 결정한다.
구현체를 바꿔도 컨트롤러 코드는 그대로다. (낮은 결합도)
테스트에서는 가짜 구현(mock)을 생성자로 넣어 주면 된다.
// 테스트: 진짜 서비스 대신 가짜 객체를 주입할 수 있다
OrderService fake = new FakeOrderService();
OrderController controller = new OrderController(fake);DI의 출발점은 '인터페이스에 의존하기'
구체 클래스가 아니라 인터페이스(역할) 에 의존하도록 설계해야 DI의 장점(교체 가능성·테스트 용이성)이 살아난다. 실제 빈 등록 방법(@Component, @Bean)과 주입 방식(생성자/필드/setter) 비교는 Spring MVC 장에서 더 자세히 다룬다.
AOP (Aspect Oriented Programming) — 관점 지향 프로그래밍
로깅, 실행 시간 측정, 트랜잭션, 보안 같은 기능은 여러 메소드에 공통으로 흩어져 나타난다. 이런 공통 관심사(cross-cutting concern) 를 핵심 비즈니스 로직과 분리하는 것이 AOP다.
// AOP 없이: 모든 메소드에 시간 측정 코드가 반복된다
public void order() {
long start = System.currentTimeMillis(); // (공통) 측정 시작
try {
// ===== 핵심 로직 =====
// ...
} finally {
long end = System.currentTimeMillis(); // (공통) 측정 종료
log.info("order() 실행 시간: {}ms", end - start);
}
}핵심 로직보다 공통 코드가 더 눈에 띄고, 메소드마다 복붙해야 한다. AOP를 쓰면 이 공통 코드를 한 곳(Aspect)에 모으고, 핵심 메소드는 순수하게 비즈니스 로직만 남긴다.
@Slf4j
@Aspect
@Component
public class TimeTraceAop {
// 어디에 적용할지(Pointcut): com.example 패키지 하위 모든 메소드
@Around("execution(* com.example..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
try {
return joinPoint.proceed(); // 실제 대상 메소드 실행
} finally {
long end = System.currentTimeMillis();
log.info("{} 실행 시간: {}ms", joinPoint.getSignature(), end - start);
}
}
}이제 핵심 메소드는 공통 코드 없이 깔끔해진다.
public void order() {
// ===== 핵심 로직만 남는다 =====
}AOP 용어 | 의미 |
|---|---|
Aspect | 공통 관심사를 모듈화한 것 (위 |
Advice | 실제로 적용할 부가 기능 (시간 측정 코드) |
Pointcut | 어디에 적용할지 지정하는 표현식 ( |
JoinPoint | Advice가 적용되는 지점 (각 메소드 호출) |
@Transactional 이 바로 AOP다
스프링에서 가장 많이 쓰는 AOP 예시가 @Transactional이다. 메소드에 이 어노테이션만 붙이면, 메소드 시작 시 트랜잭션을 열고 정상 종료 시 커밋, 예외 발생 시 롤백하는 공통 코드가 프록시를 통해 자동으로 둘러싸인다. 우리가 트랜잭션 코드를 직접 쓰지 않아도 되는 이유가 AOP다.
세 원리의 관계
IoC: 객체 관리의 제어권을 컨테이너에 넘긴다 (개념).
DI: IoC를 구현하는 방법 — 의존 객체를 외부에서 주입한다 (구현).
AOP: 공통 관심사를 핵심 로직과 분리한다 (보완).
이 셋이 맞물려 "결합도는 낮고, 중복은 적고, 테스트하기 쉬운" 코드를 만드는 것이 스프링의 지향점이다.
7. MVC 패턴
요청 처리를 역할에 따라 셋으로 나누는 패턴이다.
구성 요소 | 역할 |
|---|---|
Model | View에 출력할 데이터를 담는다. View가 비즈니스 로직이나 데이터 접근을 몰라도 되게 한다. |
View | 모델에 담긴 데이터로 화면(HTML)을 그리는 일에 집중한다. |
Controller | HTTP 요청을 받아 파라미터를 검증하고 비즈니스 로직을 실행한 뒤, 결과 데이터를 모델에 담는다. |
FrontController 패턴
프론트 컨트롤러 서블릿 하나로 클라이언트의 모든 요청을 받는다.
프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출한다.
공통 처리가 가능하다.
프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 직접 사용하지 않아도 된다.
스프링 웹 MVC의
DispatcherServlet이 바로 이 FrontController 패턴으로 구현되어 있다.
8. Spring MVC 구조
DispatcherServlet이 요청을 받아 처리하는 동작 순서다.
flowchart LR
A[클라이언트 요청] --> B[DispatcherServlet]
B --> C[핸들러 매핑]
C --> D[핸들러 어댑터]
D --> E[컨트롤러 핸들러]
E --> F[ModelAndView 반환]
F --> G[ViewResolver]
G --> H[View 렌더링]
H --> A핸들러 조회: 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
핸들러 어댑터 조회: 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
핸들러 어댑터 실행 → 핸들러 실행: 어댑터가 실제 핸들러(컨트롤러)를 실행한다.
ModelAndView 반환: 핸들러가 반환한 정보를
ModelAndView로 변환해서 반환한다.ViewResolver 호출: 뷰 리졸버를 찾아 실행한다.
View 반환: 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링을 담당하는 뷰 객체를 반환한다.
뷰 렌더링: 뷰를 통해 화면을 렌더링한다.
핵심 어노테이션
@Controller — 스프링이 자동으로 빈으로 등록한다(내부에
@Component가 있어 컴포넌트 스캔 대상). 어노테이션 기반 컨트롤러로 인식된다.@RequestMapping — 요청 정보를 매핑한다. 해당 URL이 호출되면 이 메소드가 실행된다. 어노테이션 기반이라 메소드 이름은 임의로 지어도 된다.
9. 로깅(Logging)
운영 환경에서는 System.out.println 대신 로그 라이브러리를 사용한다.
로그 라이브러리
Logback, Log4j, Log4j2 등 여러 라이브러리가 있는데, 이를 통합해 인터페이스로 제공하는 것이 SLF4J다.
SLF4J는 인터페이스이고, 그 구현체로 Logback 등을 선택한다.
스프링 부트는 기본으로 Logback을 제공하며 대부분 이를 사용한다.
로그 사용의 장점
쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고 출력 모양을 조정할 수 있다.
로그 레벨에 따라 개발 서버에서는 모든 로그를, 운영 서버에서는 일부만 출력하도록 조절할 수 있다.
콘솔뿐 아니라 파일·네트워크 등 별도 위치에 남길 수 있다. 파일은 일별·용량별 분할도 가능하다.
성능도
System.out보다 좋다.
로그 레벨
TRACE > DEBUG > INFO > WARN > ERROR로그 레벨 설정 (application.properties)
# 전체 로그레벨 설정 (기본 info)
logging.level.root=info
# hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=debug로그 선언
// 1) 직접 선언
private final Logger log = LoggerFactory.getLogger(getClass());
// 2) static 으로 선언
private static final Logger log = LoggerFactory.getLogger(Xxx.class);
// 3) Lombok 사용 (권장)
@Slf4j
public class MyController {
public void hello() {
log.info("info log = {}", data);
}
}Lombok @Slf4j
클래스 위에 @Slf4j만 붙이면 log 변수가 자동 생성된다. 가장 간편한 방법이다.
정리
정적 리소스는 웹 서버, 코드 실행이 필요한 동적 처리는 WAS가 담당한다.
자바 웹 기술은 서블릿 → JSP → MVC 프레임워크 → 스프링 MVC → 스프링 부트로 발전했다.
스프링 MVC의 중심에는 FrontController 패턴인
DispatcherServlet이 있다.컨트롤러는
@Controller,@RequestMapping으로 만들고, 로깅은@Slf4j+ SLF4J로 한다.다음 장에서 실제로 프로젝트를 생성하고 이 구조를 코드로 확인한다.