[TIL 24일 차] Spring MVC: 비즈니스 로직
오늘의 학습
1. 웹 애플리케이션의 기초
1-02. 서블릿(Servlet)의 이해
자바로 작성되어 서블릿 컨테이너(ex: 톰캣)에서 실행되는 서버 측 웹 컴포넌트로, 클라이언트의 HTTP 요청(Request)을 처리하여 HTTP 응답(Response)을 생성
자바 진영에서 웹 애플리케이션을 구현하기 위한 가장 기초적인 표준이며, Java EE (현 Jakarta EE)의 일부로 정의되어 있음.
- 등장 배경
- 초기 웹 서버는 정적인 HTML 파일만 제공할 수 있었고, 동적인 웹 페이지를 만들기 위해 Perl이나 Shell 기반의 CGI(Common Gateway Interface)가 주로 사용되었지만, CGI는 한계가 있음
- 요청마다 새로운 프로세스를 생성하여 성능 저하 발생
- 복잡한 상태 관리와 요청 분기 처리의 어려움
- 프로세스 간 자원 공유의 부재
- 이러한 문제를 해결하기 위해 Java 기반의 고성능 웹 서버 컴포넌트로서 Servlet이 등장
- 프로세스가 아닌 스레드 기반 처리로 고성능 제공
- 객체지향 방식으로 코드 구조화 가능
- 세션, 쿠키, 리다이렉션 등 HTTP 기능을 손쉽게 사용 가능
- 다양한 WAS(Web Application Server)에서 표준적으로 작동 (Tomcat, Jetty 등)
- 초기 웹 서버는 정적인 HTML 파일만 제공할 수 있었고, 동적인 웹 페이지를 만들기 위해 Perl이나 Shell 기반의 CGI(Common Gateway Interface)가 주로 사용되었지만, CGI는 한계가 있음
- 작동 흐름
[브라우저] → [HTTP 요청] → [서블릿 컨테이너] → [서블릿 클래스의 doGet/doPost()] 실행 → [응답 생성] - Spring MVC는 결국 내부적으로 DispatcherServlet이라는 서블릿을 통해 모든 요청을 처리함
- 서블릿의 생명주기(Lifecycle)
- 서블릿은 클라이언트의 요청이 있을 때마다 새롭게 생성되는 것이 아니라, 서블릿 컨테이너가 그 생명주기를 제어
- 컨테이너는 서블릿 객체를 단 한 번 생성하고, 여러 요청을 반복해서 처리하도록 유지
- 단계
- 생성자 호출
init()➡️ 최초 요청 시 1회 호출로, 초기화service()➡️ 클라이언트 요청이 들어올 때마다 분기 수행시킴doGet()/doPost()➡️service()내부에서 HTTP 메서드에 따라 분기 실행destroy()➡️ WAS 종료 시 1회 실행되어, 메모리 효율을 위해 자원 정리
- 서블릿 컨테이너의 역할
- 서블릿이 실행될 수 있는 환경과 HTTP 통신을 위한 핵심 기능을 제공하는 서버 측 컴포넌트
- Java EE 스펙을 따르는 웹 애플리케이션 서버라면 반드시 서블릿 컨테이너를 포함하고 있으며, Spring Boot에서도 톰캣을 내장해 이를 자동으로 제공
- 종류 : Tomcat, Hetty, UnderTow, Resin 등
- 요청부터 응답까지의 흐름
[브라우저] → (HTTP 요청) ↓ [Connector (Socket)] ↓ [Request 객체 생성] ↓ [Context 및 URL 매핑 탐색] ↓ [Wrapper에서 해당 서블릿 호출 → service() → doGet()/doPost()] ↓ [Response 객체 작성] ↓ [Socket을 통해 응답 전송] ↓ [브라우저 화면 출력]
2. MVC 아키텍처의 이해
2-01. Spring MVC
Spring Framework에서 웹 계층을 담당하는 모듈로, spring-webmvc에 포함된 웹 프레임워크이다.
서블릿(Servlet) API를 기반으로 동작하며, 클라이언트의 HTTP 요청을 받아 Model, View, Controller로 분리하여 처리하는 MVC 패턴을 구현한다.
➡️ 개발자는 서블릿을 직접 작성하지 않아도 애너테이션 기반으로 컨트롤러를 통해 요청 처리, 데이터 바인딩, 응답 생성을 편리하게 구현
- 데이터 바인딩 : HTTP 요청으로 들어온 값들을 자바 객체의 필드에 자동으로 매핑하는 과정
2-02. MVC 패턴
Model - View - Controller의 약자
- Model : 데이터와 비즈니스 로직 처리
- 클라이언트의 요청을 처리한 결과 데이터를 담고 있는 영역
- Service Layer에서 만들어진 객체가 곧 Model 데이터가 된다.
- View : 사용자에게 보여지는 화면 처리
- Model 데이터를 기반으로 사용자에게 시각적으로 보여지는 결과를 생성
- 종류: HTML, JSON, 문서(PDF, Excel 등)
- View를 분리하면 백엔드와 프론트엔드 작업을 동시에 할 수 있어 효율적
- Controller : 요청 처리 및 흐름 제어
- 클라이언트의 요청을 직접적으로 수신하는 엔드포인트
- 요청을 받고 비즈니스 로직을 실행한 후, Model을 View에 전달
- Controller에서는 반드시 DTO 또는 Entity 객체만 반환
2-03. MVC 패턴의 장점
대규모 애플리케이션을 보다 명확하게 구조화할 수 있게 해주는 대표적인 아키텍처 패턴
하나의 애플리케이션을 세 가지 책임 영역으로 분리하여 관리함으로써 유지보수성과 확장성을 높이는 데 큰 장점
- 관심사의 분리 (Separation of Concerns)
- 서로 다른 목적과 책임을 가진 코드를 분리하여 관리하는 원칙
- Model - View - Controller로 나눠서 각 계층은 자신의 책임만 집중
- View는 화면 표시만 담당하고,
- Controller는 흐름 제어만 수행하며,
- Model은 데이터 처리 및 비즈니스 로직에만 집중합니다.
- 장점
- 코드 변경 범위 최소화
- 코드 재사용성 증가
- 개발 역할 분담 용이
- 유지보수성과 확장성 향상
- 테스트 용이
3. Spring MVC의 구조
3-01. Spring MVC의 핵심 컴포넌트
Spring MVC는 여러 컴포넌트들이 요청과 응답을 유기적으로 처리하는 구조로 이루어져 다. 이 구조의 중심에 있는 건 DispatcherServlet이며, 그 외 여러 보조 컴포넌트들이 함께 동작한다.

[그림] Spring MVC의 동작 방식 및 구성요소
DispatcherServlet- Spring MVC 아키텍처의 핵심이며, 모든 HTTP 요청의 진입점
- Front Controller Patter 기반으로 동작하며, 클라이언트의 요청을 수신하고 이후 처리를 전담
HandlerMappgin- 클라이언트 요청 URI와 이를 처리할 Controller 메서드를 연결(매핑)해주는 역할
- 요청 URI를 분석하여 해당 요청을 처리할 수 있는 핸들러 객체(주로 Controller 클래스 내부의 메서드)를 찾아
DispatcherServlet에 반환 - 실무에서는 대부분
RequestMappingHandlerMapping을 사용
HandlerAdapter- 다양한 핸들러 타입을 일관된 방식으로 실행할 수 있도록 추상화
- 핸들러 호출 결과를 ModelAndView 형태로 표준화하여
DispatcherServlet에 전달- “Model 데이터 + View 이름”의 형태
- 커스텀 핸들러를 사용할 경우
HandlerAdapter를 직접 구현하여 Spring에 등록
- 응답 처리 컴포넌트
ViewResolver- Controller가 반환한 View 이름을 실제 JSP, Thymeleaf 템플릿 파일 등으로 찾아주는 역할
- 찾은 View 객체를
DispatcherServlet에게 전달 ViewResolver경로가 잘못 설정되면 뷰를 찾지 못해 500 오류가 발생
HttpMessageConverter- API 응답은 View가 아닌 직접 JSON, XML 등으로 데이터를 반환하는데, 이때 Java 객체를 직렬화(혹은 역직렬화)해주는 역할
- Controller에서 반환한 객체를 응답 헤더의
Content-Type에 따라 적절한 변환기(구현체)를 선택하고, HTTP Response Body에 맞게 변환- API 요청 처리시 상단의 이미지에서
ViewResolver와 View자리에 위치함
- API 요청 처리시 상단의 이미지에서
- 클라이언트의 요청 Body를 Java 객체로 역변환 (RequestBody)도 가능
4. Spring Boot MVC 시작하기
4-01. Spring-boot-starter-web
Spring Boot에서 spring-boot-starter-web은 웹 애플리케이션 개발에 필요한 핵심 의존성들을 묶어 놓은 스타터(Starter)로, 이 의존성 모듈 하나만 설치하면 아래 기능을 손쉽게 사용 가능
- 기능
- Spring MVC 웹 프레임워크
- 내장 Tomcat 서버
- Jackson을 이용한 JSON 처리
- 정적 리소스 제공
- 예외 처리 자동화
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-web'
- 주요 의존성
spring-web: Spring MVC 프레임워크 핵심 기능 제공spring-webmvc:DispatcherServlet, Controller, View 등 구성 요소 제공jackson-databind: JSON 직렬화/역직렬화 지원validation-api,hibernate-validator: Bean Validation 지원 (예:@Valid)spring-boot-starter-tomcat: 내장 Tomcat 서버로 애플리케이션 실행
- Spring Boot의 자동 설정(AutoConfiguration) 기능으로 인해
DispatcherServlet이나@RestController,@RequestMapping, 정적 리소스 핸들링 등의 설정들이 자동으로 구성됨
4-02. 정적 리소스 설정
Spring Boot는 기본적으로 정적 리소스 (HTML, CSS, JS, 이미지 등)를 다음 위치에 두면 자동으로 제공
/src/main/resources/
├── static/
├── public/
├── resources/
└── META-INF/resources/
위 폴더 중 하나에 index.html, style.css, script.js 등을 두면 자동으로 / 또는 http://localhost:8080/index.html 같은 경로로 접근 가능
5. API 테스트 도구
5-01. Postman
API 개발과 테스트에 최적화된 HTTP 클라이언트 도구
브라우저나 cURL로는 불편했던 API 테스트 과정을 쉽고 시각적으로 처리할 수 있게 해줌
RESTful API를 주로 사용하는 백엔드 개발자와 QA, 프론트엔드 개발자 간의 API 통신 검증 도구로 매우 널리 사용됨
- cURL(Client for URL)은 터미널이나 명령 프롬프트에서 URL을 기반으로 HTTP, HTTPS 등 다양한 프로토콜을 사용해 데이터를 전송하고 응답할 수 있는 명령줄 도구
- 컬렉션(Collection)
- 관련된 API 요청들을 하나의 그룹으로 묶어 관리할 수 있는 단위(컨테이너)
- 각 요청은 컬렉션 내에 저장되며, 요청 간 공통 설정(예: 환경 변수, 헤더 등)을 공유 가능
- 장점
- 테스트 시나리오 분류
- 일괄 실행
- 협업 용이
- 문서화 지원
- 환경(Environment) 변수
- 서버 주소, 토큰 등 환경별로 달라지는 값을 효율적으로 관리하는 값들을 변수(Variable)로 추출해 관리할 수 있는 기능
- 변수(Variable) 종류
- Global : 전체 요청(모든 환경)
- Environment : 특정 환경
- Collection : 컬렉션 내부
- Local : 한 요청 내
6. 기본적인 요청 처리하기
6-01. 패키지 구조 생성
- 패키지 구조
- 기능 기반 패키지 구조(package-by-feature)
- 애플리케이션의 패키지를 애플리케이션에서 구현해야 하는 기능을 기준으로 패키지를 구성하는 것
- 패키지 안에는 하나의 기능을 완성하기 위한 계층별(API 계층, 서비스 계층, 데이터 액세스 계층) 클래스들이 모여있음.
- 예시 구조
root/ ├── coffee/ │ ├── Coffee │ ├── CoffeeDTO │ ├── CoffeeController │ ├── CoffeeRepository │ └── CoffeeService ├── member/ │ ├── Member │ ├── MemberDTO │ ├── MemberController │ ├── MemberRepository │ └── MemberService ...
- 계층 기반 패키지 구조(package-by-layer)
- 패키지를 하나의 계층(Layer)으로 보고 클래스들을 계층별로 묶어서 관리하는 구조
- 예시 구조
root/ ├── DTO │ ├── CoffeeDTO │ └── MemberDTO ├── Entity │ ├── Coffee │ └── Member ...
- 기능 기반 패키지 구조(package-by-feature)
6-02. Controller 클래스 설계
- API 계층
- 클라이언트의 요청을 직접적으로 전달받는 계층으로, Controller가 존재함
- Controller 설계
- 현대의 웹 애플리케이션에서는 일반적으로 애플리케이션이 제공해야 될 기능을 리소스(Resource, 자원)로 분류
- 이 리소스에 해당하는 Controller 클래스를 구현하면 됨
- 하지만 핸들러 메소드(Handler Method)가 없다면 “404 Not Found”가 발생함.
- Controller 계층에서는 오버로딩 불가
-
예시 코드
package com.springboot.member; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; @Controller @RequestMapping("/v1/members") public class MemberController { @PostMapping public String postMember(@RequestParam("email") String email, @RequestParam("name") String name, @RequestParam("phone") String phone, Model model) { model.addAttribute("email", email); model.addAttribute("name", name); model.addAttribute("phone", phone); return "memberResult"; // templates/memberResult.html } @GetMapping("/{member-id}") public String getMember(@PathVariable("member-id") long memberId, Model model) { model.addAttribute("memberId", memberId); return "memberDetail"; // templates/memberDetail.html } @GetMapping public String getMembers(Model model) { System.out.println("# get Members"); return "memberList"; // templates/memberList.html } }
- 사용한 핸들러 메서드(Handler Method)
@RequestMapping("/v1/members")- 클래스 레벨에서 공통 URI 경로를 지정하는 애너테이션
@PostMapping- 클라이언트의 요청 데이터(request body)를 서버에 생성할 때 사용하는 애너테이션
@GetMapping- 클라이언트가 서버에 리소스를 조회할 때 사용하는 애너테이션
6-03. 다양한 요청 데이터 처리하기
핸들러 메서드 파라미터를 이용해 클라이언트의 요청 데이터를 유연하게 받을 수 있다.
@RequestHeader- 요청 헤더 정보 처리
@CookieValue- 쿠키 정보 처리
- 쿠키에 저장된 로그인 토큰 등을 받아올 때 사용
@RequestBody- JSON 형테의 요청 데이터(Request Body)를 Java 객체로 자동 변환
- 사용 시, 클라이언트에서
Content-Type: application/json을 명시해야 함. - DTO 클래스에는 기본 생성자와 getter/setter가 있어야 Jackson이 자동 바인딩할 수 있다.
@ModelAttribute- 클라이언트가 전송한 폼 데이터(form-data) 를 자바 객체에 자동으로 매핑
- 주로 HTML
<form>태그로 전송된 데이터를 처리할 때 사용 @ModelAttribute가 생략되더라도, POST 방식의 form 데이터는 자동으로 객체에 바인딩- DTO 클래스에는 getter와 setter가 필요
name같은 필드가 아닌getName()같은 속성(property)을 기준으로 값을 매핑하기 때문
@PathVariable- URL 경로의 변수 값 처리
- 요청 URL의 경로(path)에 포함된 값을 추출해 핸들러 메서드의 매개변수로 바인딩
- 괄호 안에 입력한 문자열 값은
@GetMapping("/{member-id}")처럼 중괄호({ }) 안의 문자열과 동일해야 한다.@PathVariable Long member-id
@RequestParam- 쿼리 스트링 또는 form 필드 값 처리
- 주로 클라이언트 쪽에서 전송하는 요청 데이터를 쿼리 파라미터(Query Parmeter 또는 Query String), 폼 데이터(form-data), x-www-form-urlencoded 형식으로 전송하면 이를 서버 쪽에서 전달 받을 때 사용하는 애너테이션
- 쿼리 파라미터(Query Parameter 또는 QueryString)는 요청 URL에서 ‘?’를 기준으로 붙는 key/value 쌍의 데이터
http://localhost:8080/coffees/1?page=1&size=10
7. 파일 업로드 처리하기
7-01. 파일 업로드
일반 텍스트 데이터 전송과는 달리, multipart/form-data 라는 특별한 HTTP 요청 형식으로 전송됨
한 번의 요청으로 텍스트 데이터와 바이너리 파일을 함께 서버에 전달할 수 있도록 설계됨
- 멀티파트 요청(Multipart Request)
- 파일 업로드는 일반적인
x-www-form-urlencoded가 아닌 multipart/form-data 형식으로 전송 - 브라우저가 파일 바이너리 데이터를 포함한 요청을 보낼 수 있도록 하는 특별한 인코딩 방식
- 파일 + 텍스트 동시 전송 가능
Content-Type을multipart/form-data로 설정- 사용 방식 :
@RequestParam MultipartFile -
예시 코드
@PostMapping("/v1/coffees/upload") public String uploadCoffeeImage(@RequestParam("coffeeName") String coffeeName, @RequestParam("image") MultipartFile file, Model model) throws IOException { String fileName = file.getOriginalFilename(); Path savePath = Paths.get("./uploads/" + fileName); Files.createDirectories(savePath.getParent()); file.transferTo(savePath); CoffeeImageDto dto = new CoffeeImageDto(); dto.setCoffeeName(coffeeName); dto.setFileName(fileName); model.addAttribute("coffeeName", coffeeName); model.addAttribute("fileName", fileName); return "uploadResult"; }
- 파일 업로드는 일반적인
8. 응답 처리 이해하기
8-01. ViewResolver와 View의 동작 원리
Spring MVC에서 컨트롤러의 반환 타입이 String일 경우, 이 문자열은 View 이름으로 해석됨
View 이름은 ViewResolver에 의해 실제 템플릿 파일로 변환됨
즉, return "memberResult"; → templates/memberResult.html로 변환
- ViewResolver 동작 순서
- 컨트롤러가
String형태의 View 이름을 반환 ViewResolver가 prefix/suffix 경로를 붙여 실제 HTML 경로를 생성- 템플릿 엔진(Thymeleaf 등)이 HTML 렌더링 후 클라이언트에 응답
- 컨트롤러가
8-02. HttpMessageConverter
ViewResolver가 동작하는 방식은 SSR(서버사이드 렌더링) 방식
하지만 REST API처럼 데이터를 직접 응답해야 하는 경우, Spring은 HttpMessageConverter 를 사용해 객체를 JSON, XML 등으로 변환
@ResponseBody가 붙으면 ViewResolver는 동작하지 않음- 대신
HttpMessageConverter가 작동하여 객체를 JSON으로 변환 - Spring Boot는 기본적으로 Jackson 라이브러리를 사용해 JSON 변환을 처리
8-03. @ResponseBody의 동작 원리
반환된 데이터를 HTTP 응답 본문(response body) 에 직접 작성하게 만듦
- View를 거치지 않기 때문에 REST API 개발 시 주로 사용됨
- 내부적으로
RequestMappingHandlerAdapter→HttpMessageConverter순서로 호출됨
@GetMapping("/v1/coffees/info")
@ResponseBody
public Map<String, Object> getCoffeeInfo() {
Map<String, Object> map = new HashMap<>();
map.put("name", "콜드브루");
map.put("price", 4000);
return map; // JSON 응답으로 처리됨
}
Leave a comment