TIL 23 : MVC, Annotation 복습
Spring MVC, Spring Boot 차이점
둘 다 Spring 프레임워크 기반이지만, Spring Boot는 Spring MVC를 더 쉽게 사용할 수 있도록 만들어진 버전.
비교 항목 | Spring MVC | Spring Boot |
설정(Configurations) | XML 또는 Java 설정을 직접 해야 함 |
자동 설정 (Spring Boot Starter)
|
내장 웹 서버 | 직접 Tomcat 등을 설정해야 함 |
기본적으로 Tomcat 내장 (Jetty, Undertow도 가능)
|
빌드 속도 | 설정이 많아 초기 설정 시간이 필요함 |
설정이 적어 빠르게 개발 가능
|
의존성 | 관리 필요한 라이브러리를 직접 추가해야 함 |
spring-boot-starter-*로 자동 관리
|
REST API 개발 | 컨트롤러 작성 후 설정 필요 |
컨트롤러만 작성하면 자동 실행
|
JSP 사용 가능 여부 | 가능 (기본적으로 JSP 기반) |
JSP 지원이 제한적 (Thymeleaf 권장)
|
프로젝트 구조 | 여러 설정 파일 필요 |
기본적인 프로젝트 구조 제공
|
어플리케이션 실행 | 외부 Tomcat에서 실행 |
main() 메서드 실행으로 쉽게 시작 가능
|
1️⃣ Spring MVC란?
Spring MVC는 Spring 프레임워크의 일부로, 웹 애플리케이션을 개발하는 데 사용하는 구조
✔ MVC 패턴(Model-View-Controller)을 기반으로 한 웹 프레임워크
✔ 컨트롤러(@Controller)와 뷰(Thymeleaf, JSP) 분리
✔ XML 또는 Java 설정이 필요
👎 설정이 많고 복잡함
👎 외부 Tomcat이 필요함
2️⃣ Spring Boot란?
Spring Boot는 Spring MVC를 더 쉽게 사용할 수 있도록 만든 프레임워크.
✔ Spring MVC의 모든 기능을 포함하면서도, 추가 설정 없이 바로 사용 가능!
✔ spring-boot-starter-web만 추가하면 웹 프로젝트가 바로 실행됨!
✔ 기본적으로 Tomcat을 내장하고 있어서 별도 설정 없이 실행 가능
👆 main() 메서드만 실행하면 Spring Boot 애플리케이션이 바로 실행됨! 🎉
👆 설정 없이 자동으로 웹 서버(Tomcat) 실행! 🚀
3️⃣ 어떤 걸 써야 할까?
🔹 Spring MVC
✔ 기존 프로젝트 유지보수 시 유용
✔ XML 기반 설정이 필요한 경우
✔ 서버 설정을 세밀하게 조정해야 할 경우
🔹 Spring Boot
✔ 새로운 프로젝트 시작 시 추천 ✅
✔ 빠르게 웹 애플리케이션을 개발하고 싶을 때
✔ 설정 없이 실행 가능한 REST API, 웹 애플리케이션을 만들고 싶을 때 🚀
싱글톤(Singleton)이란?
싱글톤(Singleton)은 애플리케이션에서 단 하나의 객체만 생성되도록 보장하는 디자인 패턴.
즉, 객체가 여러 개 생성되지 않고, 하나만 만들어져서 공유되는 구조.
📮 우체국(싱글톤) & 우체통(객체) 비유
✔ 각 마을에 하나의 우체국(싱글톤)이 있음
✔ 모든 사람이 같은 우체국을 사용해야 함
✔ 새로운 우체국을 만들면 안 되고, 기존 우체국을 공유해야 함
✔ 즉, “하나의 인스턴스만 존재하는 구조!”
싱글톤이 왜 필요할까?
✔ 메모리 낭비 방지 → 객체를 하나만 생성하므로 메모리 사용량 감소
✔ 객체 생성 비용 절약 → 매번 new 로 생성하지 않고 재사용
✔ 일관된 상태 유지 → 여러 곳에서 동일한 인스턴스를 사용 가능
✔ 전역적인 접근 가능 → 언제든지 동일한 객체를 가져올 수 있음
📌 싱글톤 패턴을 사용하지 않으면?
• 요청이 올 때마다 새로운 객체를 생성하면 메모리 낭비가 발생
• 객체마다 서로 다른 값을 가지면 데이터 일관성이 깨질 가능성 있음
싱글톤 패턴 구현 방법
싱글톤을 만드는 방법은 여러 가지가 있지만, 가장 많이 사용하는 “Lazy Initialization (지연 초기화)” 방식에 대하여 확인해보겠습니다.
✅ 싱글톤 구현 예제 (Lazy Initialization)
public class Singleton {
// 1. 정적(static) 변수로 유일한 인스턴스 생성 (초기에는 null)
private static Singleton instance;
// 2. 생성자를 private으로 선언 (외부에서 new 사용 불가)
private Singleton() {}
// 3. getInstance() 메서드를 통해 객체를 생성하거나 반환
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 객체가 없으면 생성
}
return instance; // 이미 있으면 기존 객체 반환
}
}
public class Main {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); // true (같은 객체)
}
}
싱글톤 패턴의 활용 예시
✅ 1) Spring의 Bean 관리
• Spring에서는 기본적으로 Bean이 싱글톤 패턴으로 관리됨
• 즉, 같은 Bean을 여러 곳에서 사용할 때도 단 하나의 객체만 생성됨
📌 Spring에서 싱글톤 예제
@Component
public class SingletonBean {
public void showMessage() {
System.out.println("싱글톤 Bean 실행!");
}
}
@Autowired
private SingletonBean singletonBean1;
@Autowired
private SingletonBean singletonBean2;
System.out.println(singletonBean1 == singletonBean2); // true (같은 객체)
✔ Spring에서 @Component, @Service, @Repository로 등록된 Bean은 기본적으로 싱글톤!
✅ 2) 데이터베이스 커넥션 풀 (DB Connection Pool)
• 매번 새로운 DB 연결을 생성하면 속도가 느려지고, 리소스 낭비가 발생
• 싱글톤 패턴을 사용해서 하나의 커넥션 풀을 공유하면 성능 최적화 가능!
📌 DB 커넥션 풀 예제
public class DatabaseConnection {
private static DatabaseConnection instance;
private DatabaseConnection() {
System.out.println("DB 연결 생성!");
}
public static DatabaseConnection getInstance() {
if (instance == null) {
instance = new DatabaseConnection();
}
return instance;
}
}
DatabaseConnection db1 = DatabaseConnection.getInstance();
DatabaseConnection db2 = DatabaseConnection.getInstance();
System.out.println(db1 == db2); // true (같은 DB 연결 공유)
싱글톤 패턴의 장단점
장점
- 메모리 절약
- 객체 생성 비용 절감
- 전역적으로 동일한 객체 사용
단점
- 멀티스레드 환경에서 동기화 문제 발생 가능
- 코드가 복잡해질 수 있음
- 의존성이 높아질 위험
📌 싱글톤의 단점 해결 방법
• 멀티스레드 환경에서는 동기화(Synchronized) 처리 필요
• Spring의 싱글톤 Bean을 사용하면 더 안전하게 관리 가능
서블릿(Servlet)
웹 서버에서 클라이언트의 요청을 받아 처리하고 응답을 반환하는 자바 프로그램
✔ HTTP 요청을 받아서 동적으로 데이터를 생성하는 역할을 함.
✔ 예를 들어, 사용자가 웹사이트에서 로그인을 하면, 서블릿이 요청을 받아 검증하고 결과를 반환하는 방식.
Servlet의 역할
✔ 클라이언트(브라우저)가 HTTP 요청을 보냄 → (GET, POST 등)
✔ 웹 서버(Tomcat 등)가 서블릿에게 요청을 전달
✔ 서블릿이 요청을 처리하고 응답을 생성
✔ 웹 서버가 클라이언트에게 응답을 반환
📌 서블릿과 서블릿 컨테이너의 관계 정리
✔ 서블릿(Servlet) → 클라이언트 요청을 처리하는 자바 클래스
✔ 서블릿 컨테이너(Servlet Container) → 서블릿을 실행하고 관리하는 환경
(서블릿을 직접 실행할 수 없고, 반드시 서블릿 컨테이너에서 실행해야함)
👉 즉, “서블릿 컨테이너 안에 서블릿이 있는 구조” ✅
👉 서블릿 컨테이너가 서블릿을 생성하고, 요청이 오면 실행함!
🍽️ 레스토랑 비유
서블릿 컨테이너 = “주방 + 서빙 시스템”
서블릿 = “요리를 만드는 셰프”
웹 서버(Tomcat) = “식당 건물”
클라이언트 요청 = “손님의 주문”
✔ 손님(클라이언트)이 식당(웹 서버)에 들어와서 주문하면
✔ 주방(서블릿 컨테이너)이 셰프(서블릿)를 호출해서 요리(응답)를 만들고
✔ 서빙 직원(서블릿 컨테이너)이 손님에게 요리를 가져다 줌!
👉 즉, 서블릿 컨테이너는 서블릿(셰프)이 제대로 동작할 수 있도록 관리하는 역할! 🚀
📌 서블릿 컨테이너의 역할
1️⃣ 서블릿을 생성 및 관리 (init())
2️⃣ 클라이언트 요청이 오면 서블릿의 service() 메서드 실행
3️⃣ 응답을 생성하고 클라이언트에게 반환
4️⃣ 서블릿이 필요 없으면 제거 (destroy())
📌 흐름 이해하기
1️⃣ Tomcat(웹 서버 + 서블릿 컨테이너)을 실행하면 서블릿 컨테이너가 준비됨
2️⃣ 개발자가 서블릿(HelloServlet)을 만들고 배포
3️⃣ 클라이언트가 http://example.com/hello 요청
4️⃣ 서블릿 컨테이너가 HelloServlet을 찾아 실행
5️⃣ 서블릿이 응답을 생성하고, 컨테이너가 클라이언트에게 반환
✔ 즉, 서블릿 컨테이너가 먼저 실행된 후, 우리가 만든 서블릿을 관리 함
💁서블릿 컨테이너를 먼저 만들고, 서블릿을 선언해야 할까?
✔ 직접 서블릿 컨테이너를 만들 필요는 없음.
✔ Tomcat 같은 웹 서버가 서블릿 컨테이너를 제공해 줌.
📌 서블릿 컨테이너 실행 방법
1️⃣ Tomcat을 실행하면 서블릿 컨테이너가 자동 실행됨
2️⃣ 우리가 만든 서블릿을 Tomcat에 배포하면 서블릿 컨테이너가 알아서 관리
3️⃣ 클라이언트 요청이 오면, 서블릿 컨테이너가 서블릿을 실행하여 응답을 반환
✔ 즉, 개발자는 Tomcat 같은 웹 서버를 실행하면 자동으로 서블릿 컨테이너가 실행되므로, 직접 만들 필요가 없음! 🚀
코드로 이해하기 (서블릿 + 서블릿 컨테이너)
1) 서블릿 코드 (HelloServlet)
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.getWriter().println("Hello, World!");
}
}
✔ @WebServlet("/hello") → http://example.com/hello 요청이 오면 실행됨
2) 서블릿 컨테이너의 동작 과정
1. Tomcat 실행 (서블릿 컨테이너 시작)
2. 클라이언트가 http://example.com/hello 요청
3. 서블릿 컨테이너가 HelloServlet 실행
4. HelloServlet이 "Hello, World!" 응답 생성
5. 서블릿 컨테이너가 응답을 클라이언트에게 반환
📌 Servlet과 Thread Pool의 관계
서블릿은 요청이 들어올 때마다 새로 생성되는 것이 아니라, “하나의 서블릿 객체”가 여러 개의 스레드에 의해 실행됨!
✔ 서블릿은 싱글턴(singleton)으로 관리됨
✔ 요청이 올 때마다 새로운 스레드가 서블릿의 service() 메서드를 실행
✔ 스레드는 Thread Pool에서 빌려와서 사용한 후 반납됨
✔ 하나의 서블릿 객체가 여러 요청을 동시에 처리할 수 있도록 멀티스레드 환경에서 동작
📌 Servlet이 요청을 처리하는 과정
1️⃣ 서블릿 컨테이너(Tomcat 등) 가 애플리케이션 실행 시 Servlet 객체를 하나 생성
2️⃣ 클라이언트 요청이 들어오면, Thread Pool에서 스레드를 할당
3️⃣ 해당 스레드가 Servlet의 service() 메서드를 실행
4️⃣ 요청이 GET이면 doGet(), POST이면 doPost() 실행
5️⃣ 요청 처리가 끝나면 스레드는 Thread Pool로 반환됨
📌 요청이 많으면 여러 개의 스레드가 같은 서블릿 객체의 service() 메서드를 동시에 실행할 수도 있음!
service(), doGet(), doPost()는 개발자가 직접 정의하는 것이 아니라, 서블릿이 기본적으로 제공하는 메서드로,
필요하면 우리가 doGet(), doPost()를 오버라이딩해서 원하는 로직을 작성할 수 있음 🚀
✔ 서블릿 컨테이너가 서블릿을 실행할 때 service()를 자동으로 호출함!
✔ 그리고 요청된 HTTP 메서드(GET, POST, PUT, DELETE 등)에 따라 doGet(), doPost() 같은 메서드를 실행
📌 즉, service()와 doGet(), doPost()는 이미 HttpServlet 클래스에 정의되어 있음!
📌 개발자는 doGet(), doPost()를 직접 오버라이딩해서 로직을 구현하는 것!
서블릿의 기본 구조 (HttpServlet 클래스 내부)
public class HttpServlet { protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String method = req.getMethod(); if (method.equals("GET")) { doGet(req, res); } else if (method.equals("POST")) { doPost(req, res); } else { // 다른 HTTP 메서드 처리 } } protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // 기본 구현 (오버라이딩 가능) } protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // 기본 구현 (오버라이딩 가능) } }
개발자가 서블릿을 만들 때 사용하는 코드
예제 1: doGet()만 사용하는 서블릿
✔ @WebServlet("/hello") → /hello 요청이 오면 실행됨@WebServlet("/hello") public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); response.getWriter().println("<h1>Hello, Servlet!</h1>"); } }
✔ doGet()을 오버라이딩해서, 브라우저에서 GET 요청을 받으면 "Hello, Servlet!" 출력
예제 2: doPost()를 사용하는 서블릿
@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); response.setContentType("text/html;charset=UTF-8"); if ("admin".equals(username) && "1234".equals(password)) { response.getWriter().println("<h1>로그인 성공!</h1>"); } else { response.getWriter().println("<h1>로그인 실패!</h1>"); } } }
✔ doPost()를 오버라이딩해서, POST 요청이 들어오면 데이터를 처리하도록 함
✔ 로그인 요청을 처리하는 서블릿!
예제 3: service()를 직접 오버라이딩하는 경우
보통은 doGet(), doPost()를 오버라이딩하지만,
특정한 경우 service()를 직접 오버라이딩할 수도 있음!
@WebServlet("/custom") public class CustomServlet extends HttpServlet { @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); response.getWriter().println("<h1>이 서블릿은 모든 요청을 service()에서 처리!</h1>"); } }
✔ service()를 직접 오버라이딩하면 GET, POST 상관없이 모든 요청이 service()에서 처리됨!
✔ 하지만 보통은 doGet(), doPost()를 각각 오버라이딩해서 사용함 ✅
서블릿에서 지원하는 HTTP 메서드 처리 메서드
HTTP 메서드 서블릿의 해당 메서드 역할 GET doGet() 데이터 조회 POST doPost() 데이터 생성 (또는 처리) PUT doPut() 데이터 전체 수정 PATCH doPatch() 데이터 일부 수정 DELETE doDelete() 데이터 삭제 HEAD doHead() GET 요청과 동일하지만, 응답 본문 없이 헤더만 반환 OPTIONS doOptions() 서버에서 지원하는 HTTP 메서드 목록 조회 TRACE doTrace() 클라이언트가 요청을 보낸 경로를 추적
MVC 패턴이란?
MVC(Model-View-Controller) 패턴은 웹 애플리케이션에서 역할을 분리하여 유지보수를 쉽게 하는 설계 방식
Model : 데이터(객체)를 관리하는 계층
View : 클라이언트에게 보여지는 화면 (JSP, HTML)
Controller : 사용자의 요청을 받고, 서비스에 전달 후 응답 반환
📌 즉, 사용자의 요청이 Controller로 가고, 비즈니스 로직을 처리한 후, View에 데이터를 전달하는 방식! 🚀
문제점
dispatcher.forward(request, response) View로 이동하는 forward가 항상 중복 호출된다.(코드 중복)
🔹 기본 MVC 패턴에서는 모든 컨트롤러(Servlet)에서 View(JSP)로 이동할 때 forward()를 사용해야 함
즉, 각 컨트롤러에서 같은 코드가 계속 중복됨!
✔ 모든 서블릿에서 RequestDispatcher.forward(request, response)를 반복적으로 작성해야 함
✔ View로 이동하는 방식이 항상 같지만, 각 컨트롤러마다 이 코드를 계속 중복 작성해야 함
✔ 컨트롤러의 개수가 많아질수록 중복 코드가 많아지고 유지보수가 어려워짐
해결 방법 - 프론트 컨트롤러(Front Controller) 패턴 사용
✔ “모든 요청을 하나의 서블릿이 받아서 처리한 후, View로 이동하는 방식”
✔ 즉, 각 서블릿에서 개별적으로 forward()를 호출하지 않고, 중앙에서 한 번만 호출하는 방식!
📌 ✅ Front Controller 패턴 구조
[Client (브라우저)]
↓
[FrontController] (모든 요청을 처리)
↓
[해당 컨트롤러 실행] → [View로 이동 (한 번만 forward)]
프론트 컨트롤러(Front Controller) 패턴 의문점
- 공통 처리 로직에 모든 컨트롤러가 연결되기 위해서는 모든 컨트롤러가 return 하는 결과의 형태가 동일해야 한다.
- 하지만, Controller 마다 로직이나 응답해야하는 결과는 당연히 다를테고 응답을 동일하게 맞추려고 한다면 해당 애플리케이션은 확장성, 유지보수성을 잃는다.
- 공통 로직에서 응답별로 퍼즐을 다시 하나하나 처리할 수 있으나 공통 부분의 책임이 너무 커지게된다. 또한, 컨트롤러에서 반환되는 결과가 달라지면 공통처리 부분의 변경또한 불가피하다.
- 바로 여기서 고안된것이 어댑터 패턴이다!
1. 컨트롤러(Handler)는 비지니스 로직을 처리하고 알맞은 결과를 반환한다.
2. 어댑터는 공통 로직과 컨트롤러(Handler)가 자연스럽게 연결되도록 한다.
3. 프론트 컨트롤러는 공통으로 처리되는 로직을 수행한다.
🍎 컨트롤러(Controller)
✔ 클라이언트의 요청을 받아서 처리하는 역할
✔ MVC 패턴에서 사용되는 개념
✔ 웹 애플리케이션에서 사용자의 입력을 받고, 비즈니스 로직(Service)으로 전달한 후, View로 응답을 반환하는 역할
🍇 핸들러(Handler)
✔ Spring MVC에서 “핸들러”는 클라이언트의 요청을 처리하는 특정 메서드를 의미!
✔ 컨트롤러 안에 있는 개별적인 메서드가 핸들러라고 보면 됨
✔ Spring MVC에서는 모든 컨트롤러가 핸들러이지만, 모든 핸들러가 컨트롤러는 아님
✔ Spring MVC에서는 컨트롤러가 핸들러이지만, 하나의 컨트롤러 안에 여러 개의 핸들러가 존재할 수 있음!
🍒 서블릿(Servlet)
✔ 서블릿은 Java 기반 웹 애플리케이션에서 요청을 처리하는 서버 측 컴포넌트
✔ Controller의 역할을 수행할 수 있지만, Spring MVC에서는 직접 사용하지 않고, DispatcherServlet이 대신 관리
🥝 어댑터 패턴(Adapter Pattern)
✔ 두 개의 서로 다른 인터페이스를 연결해 주는 패턴
✔ Spring MVC에서 “핸들러 어댑터(HandlerAdapter)“가 이 역할을 수행!
✔ 클라이언트 요청을 다양한 방식으로 처리할 수 있도록 도와줌
Spring에서 핸들러를 찾고 실행하는 과정
1️⃣ 클라이언트가 "/posts" 요청 2️⃣ `DispatcherServlet`이 요청을 받음 3️⃣ `HandlerMapping`이 "미리 저장된 매핑 테이블"에서 적절한 핸들러(메서드)를 바로 찾음 4️⃣ `HandlerAdapter`가 핸들러(메서드)를 실행 5️⃣ 실행 결과를 `ViewResolver`를 통해 JSP 또는 HTML로 변환 6️⃣ 클라이언트에게 응답 반환
Spring MVC는 어떻게 컨트롤러를 거치지 않고 바로 핸들러(메서드)를 찾을까? 🤔
Spring MVC는 미리 “요청 URL → 핸들러(메서드)” 매핑 정보를 저장해 둠
✔ Spring MVC는 서버가 실행될 때, 모든 컨트롤러 내부의 핸들러(메서드) 정보를 미리 수집해서 저장해 둠!
✔ 즉, 모든 책을 하나씩 뒤지는 게 아니라, “목록(인덱스)“을 보고 바로 찾는 방식!
📌 Spring MVC에서 사용하는 요청 매핑 테이블 예시
요청 URL HTTP 메서드 실행할 핸들러(메서드) /posts GET PostController.getAllPosts() /posts POST PostController.createPost() /users GET UserController.getAllUsers()
✔ 즉, Spring MVC는 실행할 때 컨트롤러를 전부 뒤지는 게 아니라, “미리 정리된 매핑 테이블”에서 바로 찾아가는 방식! 🚀
✔ Spring MVC 방식: “모든 책의 위치를 적어놓은 검색 목록(매핑 테이블)을 보고, 바로 해당 책의 위치로 가는 방식!”
📌 예제 1: 도서관에서 책을 찾는 두 가지 방법 비교
✔ Spring MVC는 컨트롤러를 하나씩 뒤지는 게 아니라, “미리 만들어진 요청-핸들러 매핑 테이블”을 보고 바로 실행할 핸들러를 찾음! 🚀
방식 설명 컨트롤러(클래스)를 먼저 찾고, 핸들러(메서드)를 찾는 방식 📚 종교 코너로 가서, 거기서 원하는 책을 찾는 방식 Spring MVC 방식 (매핑 테이블에서 바로 찾기) 📚 책 위치 목록(인덱스)을 보고, 바로 해당 책이 있는 서가로 가는 방식
🥐 Spring MVC는 언제 이 매핑 테이블을 만들까?
📌 Spring MVC는 서버가 실행될 때(@RequestMapping, @GetMapping, @PostMapping 등) 정보를 전부 수집해서 매핑 테이블을 생성함
📌 예제: Spring MVC의 컨트롤러 등록 과정
@Controller @RequestMapping("/posts") public class PostController { @GetMapping public String getAllPosts(Model model) { // ✅ 이 메서드가 핸들러! model.addAttribute("posts", List.of("Post 1", "Post 2")); return "posts"; } @PostMapping public String createPost(@RequestParam String title) { // ✅ 이 메서드도 핸들러! System.out.println("새 게시글 생성: " + title); return "redirect:/posts"; } }
✔ Spring MVC는 애플리케이션이 실행될 때, @RequestMapping, @GetMapping, @PostMapping 정보를 읽어서 “매핑 테이블”을 생성
📌 Spring 내부에서 요청 매핑 테이블을 만드는 과정
[요청 매핑 테이블] /posts [GET] → PostController.getAllPosts() /posts [POST] → PostController.createPost() /users [GET] → UserController.getAllUsers()
✔ 즉, 요청이 들어오면 이 매핑 테이블을 보고 바로 해당 메서드(핸들러)를 실행하는 것!
📌 Spring MVC의 요청 처리 과정 (도서관 비유 포함)
1️⃣ 클라이언트가 "/posts" 요청 (책을 찾으려 함) 2️⃣ `DispatcherServlet`이 요청을 받음 (도서관에 도착) 3️⃣ `HandlerMapping`이 요청 URL을 매핑 테이블에서 검색 (책 위치 목록을 확인) 4️⃣ 요청을 처리할 핸들러(메서드)를 바로 찾음 (해당 책이 있는 서가로 이동) 5️⃣ `HandlerAdapter`가 핸들러를 실행 (책을 가져옴) 6️⃣ 실행 결과를 `ViewResolver`를 통해 JSP 또는 HTML로 변환 (책을 읽음) 7️⃣ 클라이언트에게 응답 반환 (책을 전달)
✔ 즉, “매핑 테이블을 보고 바로 핸들러(메서드)를 찾기 때문에 컨트롤러를 먼저 찾지 않아도 됨!” 🚀
실전에서 어떻게 해결할까? (Spring MVC)
Spring MVC에서는 이 문제를 이미 해결한 구조를 제공함!
✔ Spring에서는 **“Front Controller 패턴”**을 사용하여 중복 코드를 없애고 더 편리하게 View로 이동할 수 있음.
✔ Spring MVC에서 이 역할을 하는 것이 DispatcherServlet!
📌 ✅ Spring MVC에서의 해결 방법 (중복 없이 View로 이동)
@Controller
@RequestMapping("/posts")
public class PostController {
@GetMapping
public String showPostList(Model model) {
List<String> posts = List.of("Post 1", "Post 2", "Post 3");
model.addAttribute("posts", posts);
return "posts"; // View 이름만 반환 (자동으로 /WEB-INF/views/posts.jsp 로 이동)
}
}
✔ Spring에서는 RequestDispatcher.forward()를 직접 호출할 필요 없음!
✔ 단순히 View 이름("posts")만 반환하면 자동으로 해당 JSP로 이동!
🚀 즉, Spring MVC에서는 DispatcherServlet이 Front Controller 역할을 해서 서블릿 MVC의 중복 문제를 해결해줌!
1️⃣ 기본적인 3계층 (Controller → Service → Repository)
🔹 대부분의 웹 애플리케이션에서는 Controller, Service, Repository 세 가지 레이어가 핵심 구조
Controller : 요청을 받고, 서비스를 호출 후 응답 반환
Service : 비즈니스 로직 처리
Repository : 데이터베이스와 통신
📌 하지만, 프로젝트가 커지면 유지보수를 위해 더 세부적인 레이어를 추가할 수 있음! 🚀
2️⃣ 추가로 사용될 수 있는 레이어들
✅ 1) DTO (Data Transfer Object)
✔ 데이터를 전달하는 객체
✔ 보통 클라이언트와 서버 간 데이터를 주고받을 때 사용
✔ 서비스와 컨트롤러 간 데이터 이동 시 엔터티(Entity) 대신 DTO를 사용하는 것이 일반적!
📌 DTO 사용 예제
public class UserDTO {
private String name;
private String email;
public UserDTO(String name, String email) {
this.name = name;
this.email = email;
}
// Getter & Setter
}
📌 서비스에서 DTO를 반환
public UserDTO getUserDTO(Long userId) {
User user = userRepository.findById(userId);
return new UserDTO(user.getName(), user.getEmail());
}
✔ DTO를 사용하면 데이터베이스 엔터티와 API 응답 구조를 분리할 수 있음!
✅ 2) DAO (Data Access Object)
✔ Repository와 비슷하지만, 더 낮은 수준의 데이터 접근 레이어
✔ JDBC, JPA 같은 데이터베이스 접근 로직을 담당
✔ Repository가 Spring Data JPA 같은 ORM을 활용하는 경우, DAO를 따로 만들지 않고 Repository를 대신 사용할 수도 있음!
📌 DAO vs Repository 차이
DAO | Repository |
데이터베이스와 직접 상호작용하는 레이어 | DAO보다 한 단계 추상화된 데이터 관리 레이어 |
SQL 쿼리를 직접 사용 (JDBC, MyBatis) | Spring Data JPA, Hibernate 사용 가능 |
📌 DAO 사용 예제 (JDBC)
public class UserDAO {
private Connection connection;
public UserDAO(Connection connection) {
this.connection = connection;
}
public User findById(Long id) throws SQLException {
String query = "SELECT * FROM users WHERE id = ?";
PreparedStatement stmt = connection.prepareStatement(query);
stmt.setLong(1, id);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return new User(rs.getLong("id"), rs.getString("name"), rs.getString("email"));
}
return null;
}
}
✔ DAO는 SQL을 직접 다루는 경우 사용됨 (Spring Boot에서는 Repository가 DAO 역할을 대신하는 경우가 많음!)
✅ 3) Facade Layer (파사드 레이어)
✔ 여러 개의 서비스(Service)를 조합하여 하나의 엔드포인트에서 제공하는 역할
✔ 복잡한 비즈니스 로직을 간소화하기 위해 사용
✔ 여러 서비스 호출을 한 곳에서 묶어서 제공하면 클라이언트가 쉽게 사용할 수 있음
📌 Facade 예제
public class UserFacade {
private UserService userService;
private OrderService orderService;
public UserFacade(UserService userService, OrderService orderService) {
this.userService = userService;
this.orderService = orderService;
}
public UserDashboardDTO getUserDashboard(Long userId) {
User user = userService.getUserById(userId);
List<Order> orders = orderService.getOrdersByUserId(userId);
return new UserDashboardDTO(user, orders);
}
}
✔ Facade 패턴을 사용하면, 여러 서비스를 조합하여 하나의 API에서 제공 가능!
✔ 클라이언트가 UserFacade만 호출하면, UserService와 OrderService를 한 번에 사용할 수 있음 ✅
✅ 4) Controller Advice (전역 예외 처리)
✔ 예외 처리를 위한 별도 레이어
✔ @ControllerAdvice를 사용하면 모든 컨트롤러에서 발생하는 예외를 한 곳에서 처리할 수 있음
✔ 에러 응답을 일관되게 관리 가능
📌 예제: 전역 예외 처리
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<String> handleUserNotFound(UserNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found: " + ex.getMessage());
}
}
✔ 예외가 발생해도 클라이언트에게 일관된 응답을 제공 가능! 🚀
✅ 5) Security Layer (보안 레이어)
✔ 사용자 인증(Authentication) & 권한 관리(Authorization)를 담당하는 레이어
✔ 보통 Spring Security 같은 라이브러리를 활용하여 구현
✔ JWT 토큰, OAuth, 세션 기반 인증 등을 관리
📌 Spring Security를 활용한 보안 설정
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin();
}
}
✔ Security 레이어를 활용하면 특정 경로에 대한 접근을 제어 가능! 🔐
🔹 일반적인 MVC + 추가 레이어 구조
[Client (브라우저)]
↓ (HTTP 요청)
[Controller] → [Controller Advice (예외 처리)]
↓
[Service] → [Facade (여러 서비스 통합)]
↓
[Repository] / [DAO]
↓
[Database]
✔ 필수 레이어: Controller → Service → Repository
✔ 추가 레이어: DTO, DAO, Facade, Security, Controller Advice 등
리다이렉트(Redirect)와 포워드(Forward)는 어디서 사용되는 기능일까?
✔ 리다이렉트와 포워드는 웹 애플리케이션에서 페이지 이동을 처리하는 기능.
✔ 서블릿(Servlet)과 JSP에서 클라이언트 요청을 처리하고, 다른 페이지로 이동할 때 사용
✔ 즉, 사용자의 요청을 처리한 후, 다음 화면을 어떻게 보여줄지를 결정하는 방법.
🔹 사용자가 웹사이트에서 특정 행동을 했을 때, 다음 페이지로 이동해야 하는 경우
예를 들어,
1️⃣ 로그인 후 → 대시보드 화면으로 이동
2️⃣ 게시글 작성 후 → 게시글 목록 화면으로 이동
3️⃣ 로그인하지 않은 사용자가 → 로그인 페이지로 이동
4️⃣ 특정 URL을 방문하면 → 새로운 페이지로 이동해야 하는 경우
📌 이처럼 “특정 작업을 수행한 후, 다른 페이지로 이동해야 하는 경우”에 리다이렉트와 포워드가 필요함! 🚀
리다이렉트(Redirect)는 어디서 쓰일까?
✔ 클라이언트가 다시 요청을 보내야 하는 경우 사용!
✔ 로그인 후 대시보드 이동, 회원가입 후 로그인 페이지 이동, 외부 사이트로 이동 등에 활용됨
📌 🔹 예제 1: 로그인 후 대시보드로 이동 (sendRedirect())
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
boolean isAuthenticated = checkLogin(request); // 로그인 검증
if (isAuthenticated) {
response.sendRedirect("/dashboard"); // 로그인 성공 시 대시보드로 이동
} else {
response.sendRedirect("/login?error=true"); // 로그인 실패 시 로그인 페이지 다시 요청
}
}
}
✔ 클라이언트가 다시 /dashboard 요청을 보내게 되어 URL이 변경됨 ✅
📌 🔹 예제 2: 로그인하지 않은 사용자를 로그인 페이지로 리다이렉트
@WebServlet("/dashboard")
public class DashboardServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("user") == null) {
response.sendRedirect("/login"); // 로그인하지 않았다면 로그인 페이지로 이동
return;
}
response.getWriter().println("<h1>Welcome to Dashboard</h1>");
}
}
✔ 로그인되지 않은 사용자가 /dashboard에 접근하면 로그인 페이지로 이동하도록 설정
📌 🔹 예제 3: 특정 URL 방문 시 다른 URL로 이동
@WebServlet("/oldPage")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.sendRedirect("/newPage"); // "/oldPage" 요청이 오면 "/newPage"로 이동
}
}
✔ 이전 URL(/oldPage)로 들어와도 자동으로 새로운 URL(/newPage)로 이동됨
포워드(Forward)는 어디서 쓰일까?
✔ 서버 내부에서 페이지를 변경해야 하는 경우 사용!
✔ JSP로 데이터를 전달하거나, 내부적으로 다른 페이지를 호출할 때 활용됨
📌 🔹 예제 1: 블로그 글 작성 폼을 JSP로 포워드 (RequestDispatcher.forward())
@WebServlet("/mvc/posts")
public class MvcPostFormServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = "/WEB-INF/views/post-form.jsp"; // JSP 페이지 경로
RequestDispatcher dispatcher = request.getRequestDispatcher(path);
dispatcher.forward(request, response); // 서버 내부에서 이동
}
}
✔ 클라이언트는 /mvc/posts 요청을 보냈지만, 서버는 내부적으로 /WEB-INF/views/post-form.jsp로 이동
✔ URL이 바뀌지 않고, 브라우저는 같은 요청으로 처리됨
📌 🔹 예제 2: 서블릿에서 JSP로 데이터 전달 (게시글 목록)
@WebServlet("/posts")
public class PostListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
List<String> posts = List.of("Post 1", "Post 2", "Post 3");
request.setAttribute("posts", posts); // 데이터를 JSP로 전달
RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/posts.jsp");
dispatcher.forward(request, response);
}
}
✔ 서블릿이 List<String> posts 데이터를 생성한 후, JSP로 전달
✔ 클라이언트는 posts.jsp를 응답으로 받지만, URL은 변경되지 않음
📌 🔹 예제 3: 로그인 후, 내부적으로 홈 페이지로 이동 (URL 유지)
@WebServlet("/home")
public class HomeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/home.jsp");
dispatcher.forward(request, response);
}
}
✔ /home 요청이 오면 내부적으로 home.jsp를 보여줌
✔ 브라우저의 주소창에는 여전히 /home이 표시됨 (URL 변경 X)
리다이렉트와 포워드는 웹 애플리케이션에서 어떻게 사용될까?
✅ 리다이렉트는 다음과 같은 경우에 사용됨
✔ 클라이언트가 새로운 요청을 보내야 할 때
✔ 로그인 후, 다른 페이지로 이동할 때
✔ 특정 URL을 방문하면 새로운 URL로 이동해야 할 때
✔ POST 요청 후, 새로고침 시 데이터가 중복되는 것을 방지할 때 (PRG 패턴)
✅ 포워드는 다음과 같은 경우에 사용됨
✔ 서버 내부에서 JSP로 데이터를 전달해야 할 때
✔ URL을 변경하지 않고 내부적으로 다른 페이지를 보여줄 때
✔ 데이터를 유지한 채로 다른 화면으로 이동할 때 (request.setAttribute())
Spring Bean이란?
✔ Spring에서 관리하는 객체(인스턴스)를 “Spring Bean”이라고 함.
✔ 즉, Spring이 자동으로 생성하고, 관리하는 객체를 의미
✔ Spring 컨테이너(Application Context)가 “Bean”을 생성하고, 필요할 때 주입(의존성 주입, DI)해 줌
📌 쉽게 말해서, “Spring에서 관리하는 객체(클래스의 인스턴스)“를 Bean이라고 부름! 🚀
1️⃣ Spring Bean을 이해하는 쉬운 비유!
📌 비유: “Spring은 큰 공장이고, Bean은 공장에서 만들어지는 제품”
✔ Spring 컨테이너 = 공장
✔ Spring Bean = 공장에서 만들어진 제품(객체)
✔ Spring 컨테이너가 모든 Bean을 생성하고, 관리함
📌 즉, 개발자가 직접 객체를 만들지 않아도, Spring이 자동으로 객체를 만들고, 관리해 줌!
2️⃣ Spring Bean의 특징
✅ Spring 컨테이너가 객체를 생성하고 관리함
✅ 개발자가 직접 객체를 만들 필요 없음 (new 키워드 사용하지 않음)
✅ 필요한 곳에 자동으로 주입(Dependency Injection, DI)할 수 있음
3️⃣ Spring에서 Bean을 만드는 방법
📌 Spring에서는 @Component, @Service, @Repository, @Controller 등의 애노테이션을 사용하면 자동으로 Bean이 생성됨!
✅ 방법 1: @Component 사용
@Component // Spring Bean으로 등록됨!
public class Car {
public void drive() {
System.out.println("자동차가 달립니다!");
}
}
✔ @Component를 붙이면 Spring이 이 클래스를 자동으로 Bean으로 등록
✔ 이제 new Car()를 사용하지 않고, Spring이 자동으로 관리함!
📌 사용 예제 (자동 주입)
@Component
public class Car {
public void drive() {
System.out.println("자동차가 달립니다!");
}
}
@Service
public class CarService {
private final Car car;
@Autowired // Spring이 자동으로 Car 객체를 주입해 줌!
public CarService(Car car) {
this.car = car;
}
public void start() {
car.drive();
}
}
✅ 방법 2: @Service, @Repository, @Controller 사용
어노테이션 | 역할 |
@Component | 일반적인 Bean 등록 |
@Service | 서비스 로직을 처리하는 Bean |
@Repository | 데이터베이스 관련 Bean |
@controller | 웹 요청을 처리하는 Bean (Spring MVC) |
✅ 방법 3: @Bean을 사용해서 직접 등록 (수동 등록)
📌 Spring에서 @Configuration과 @Bean을 사용하면 직접 Bean을 등록할 수도 있음!
@Configuration
public class AppConfig {
@Bean
public Car car() {
return new Car(); // 직접 Bean을 생성해서 등록
}
}
✔ @Bean을 사용하면, 개발자가 원하는 방식으로 직접 Bean을 등록 가능
✔ 하지만 일반적으로는 @Component 등의 애노테이션을 사용하여 자동 등록하는 것이 더 편리함 ✅
Spring Boot의 Handler Mapping, Handler Adapter
✔ HandlerMapping과 HandlerAdapter는 Spring MVC 내부에 이미 내장된 핵심 기능!
✔ 개발자가 따로 @Component 같은 애노테이션을 붙이지 않아도, Spring MVC가 자동으로 실행할 핸들러를 찾아줌!
✔ 하지만, @RequestMapping, @GetMapping, @PostMapping 같은 애노테이션을 사용해야 핸들러를 등록할 수 있음!
HandlerMapping : 요청된 URL과 매칭되는 핸들러(메서드)를 찾음
HandlerAdapter : 핸들러를 실행할 수 있도록 도와줌
✔ 핸들러를 실행하는 과정에서 HandlerMapping과 HandlerAdapter는 자동으로 동작
✔ 하지만, “어떤 핸들러를 실행할지”를 알려주려면 @RequestMapping 같은 애노테이션이 필요함!
@RequestMapping 애노테이션이 왜 필요할까?
Spring MVC에서는 “어떤 URL 요청을 처리할 것인지”를 컨트롤러(핸들러)에서 지정해야 함
✔ @RequestMapping을 사용하면, 해당 메서드가 특정 URL 요청을 처리한다는 것을 Spring에게 알려줄 수 있음!
📌 예제: @RequestMapping을 사용한 핸들러 등록
@Controller
@RequestMapping("/posts")
public class PostController {
@GetMapping
public String getAllPosts(Model model) {
model.addAttribute("posts", List.of("Post 1", "Post 2"));
return "posts";
}
@PostMapping
public String createPost(@RequestParam String title) {
System.out.println("새 게시글 생성: " + title);
return "redirect:/posts";
}
}
✔ @RequestMapping("/posts") → 이 컨트롤러가 /posts 경로의 요청을 처리
✔ @GetMapping → GET 요청을 처리하는 핸들러
✔ @PostMapping → POST 요청을 처리하는 핸들러
📌 즉, @RequestMapping, @GetMapping, @PostMapping 등의 애노테이션이 없으면 핸들러를 등록할 수 없음!
📌 HandlerMapping과 HandlerAdapter의 우선순위란?
✔ Spring MVC에서는 여러 개의 HandlerMapping과 HandlerAdapter가 존재할 수 있음.
✔ Spring은 요청이 들어오면 우선순위가 높은 HandlerMapping부터 실행해서 핸들러(메서드)를 찾고, 해당 핸들러를 실행할 HandlerAdapter도 우선순위대로 선택해 실행함!
✔ 즉, Spring MVC는 여러 개의 HandlerMapping과 HandlerAdapter 중 “가장 적절한 것”을 선택하는 우선순위 로직이 있음!
HandlerMapping 우선순위 (높은 순서부터 실행됨)
1. RequestMappingHandlerMapping
@RequestMapping, @GetMapping, @PostMapping을 처리
2. SimpleUrlHandlerMapping
BeanNameUrlHandlerMapping과 비슷하지만 URL을 직접 매핑
3. BeanNameUrlHandlerMapping
Spring Bean의 이름과 URL을 매핑
4. RouterFunctionMapping
WebFlux (리액티브 프로그래밍)에서 사용됨
📌 즉, Spring MVC는 먼저 RequestMappingHandlerMapping을 실행해서 핸들러(메서드)를 찾고, 없으면 다음 HandlerMapping을 실행하는 방식! 🚀
HandlerAdapter의 우선순위
✔ HandlerMapping이 핸들러(메서드)를 찾으면, 그 핸들러를 실행할 수 있는 HandlerAdapter도 여러 개가 존재할 수 있음!
✔ Spring은 우선순위가 높은 HandlerAdapter부터 실행해서 적절한 핸들러를 실행함!
1. RequestMappingHandlerAdapter
@RequestMapping, @GetMapping, @PostMapping을 실행
2. SimpleControllerHandlerAdapter
Controller 인터페이스를 구현한 컨트롤러 실행
3. HttpRequestHandlerAdapter
HttpRequestHandler를 구현한 컨트롤러 실행
4. SimpleServletHandlerAdapter
Servlet을 직접 실행하는 핸들러
📌 즉, 일반적인 Spring MVC 애플리케이션에서는 RequestMappingHandlerAdapter가 가장 먼저 실행됨! 🚀
ViewResolver란?
✔ ViewResolver(뷰 리졸버)는 Spring MVC에서 컨트롤러가 반환한 View 이름을 실제 View(JSP, HTML 등)로 변환하는 역할을 하는 컴포넌트야!
✔ 즉, 컨트롤러에서 “어떤 화면을 보여줄지” 결정하면, ViewResolver가 “실제 파일 경로”를 찾아서 클라이언트에게 응답을 반환하는 역할!
📌 즉, ViewResolver는 클라이언트가 볼 화면(View)을 결정하는 핵심 컴포넌트야! 🚀
✔ 컨트롤러가 return "home"을 반환하면, ViewResolver가 실제 JSP 파일(WEB-INF/views/home.jsp)을 찾아서 클라이언트에게 응답을 줌!
ViewResolver 설정 (prefix, suffix)
✔ Spring Boot에서는 application.properties에서 ViewResolver의 설정을 쉽게 할 수 있음!
✔ prefix(접두사)와 suffix(접미사)를 설정하면, ViewResolver가 자동으로 View 경로를 찾음!
📌 예제: application.properties에서 설정
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
✔ prefix: WEB-INF/views/ → View 파일들이 위치한 기본 경로
✔ suffix: .jsp → View 파일의 확장자
📌 컨트롤러에서 View 이름만 반환하면 자동으로 파일 경로를 찾음!
@Controller
public class HomeController {
@GetMapping("/home")
public String home() {
return "home"; // ViewResolver가 "/WEB-INF/views/home.jsp"로 변환
}
}
✔ return "home";을 반환하면, ViewResolver가 WEB-INF/views/home.jsp를 찾아서 응답
ViewResolver 종류
✔ Spring MVC에는 여러 종류의 ViewResolver가 있음!
✔ 어떤 ViewResolver를 사용하느냐에 따라 JSP, Thymeleaf, JSON 등의 응답을 처리할 수 있음!
📌 Spring에서 제공하는 ViewResolver의 종류
- InternalResourceViewResolver
- JSP 파일을 View로 사용 (prefix, suffix 설정 가능)
- ThymeleafViewResolver
- Thymeleaf 템플릿 엔진을 사용
- JsonViewResolver
- JSON 응답을 반환할 때 사용
- FreeMarkerViewResolver
- FreeMarker 템플릿 엔진을 사용
ViewResolver 직접 설정하는 방법 (수동 등록)
✔ @Bean을 사용해서 InternalResourceViewResolver를 수동으로 등록할 수도 있음 ✅
@Configuration
public class ViewConfig {
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
}
✔ REST API에서는 ViewResolver가 필요 없음!
✔ @ResponseBody 또는 @RestController를 사용하면, ViewResolver 없이 JSON 데이터를 바로 응답할 수 있음!
📌 예제: @RestController를 사용한 JSON 응답
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/data")
public Map<String, String> getData() {
return Map.of("message", "Hello, JSON!");
}
}
Spring Annotations
✔ 애노테이션(Annotation)은 클래스, 메서드, 필드 등에 추가 정보를 제공하는 메타데이터.
✔ Spring에서는 애노테이션을 사용해서 Bean 등록, 요청 매핑, DI(의존성 주입) 등을 쉽게 할 수 있음.
✔ 즉, 애노테이션을 사용하면 Spring이 자동으로 필요한 기능을 적용해 줌!
📌 즉, Spring 애노테이션은 코드를 더 간결하고, 유지보수하기 쉽게 만드는 핵심 기능! 🚀
1️⃣ @Slf4j (로깅 애노테이션)
✔ 로그를 찍을 때 사용되는 Lombok 애노테이션!
✔ log.info(), log.error() 등을 사용할 수 있음
✔ 개발자가 Logger 객체를 따로 만들 필요 없이 자동으로 log 변수를 제공함! ✅
- Log Level
- TRACE > DEBUG > INFO(default) > WARN > ERROR
- resources > application.properties에서 레벨 설정. 설정하지않으면 기본 info 설정
- logging.level.com.example.springbasicannotation=TRACE
📌 예제: @Slf4j 사용
import lombok.extern.slf4j.Slf4j;
@Slf4j // log 변수를 자동으로 생성해줌!
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
log.info("HomeController 실행됨!"); // 자동 생성된 log 사용
return "home";
}
}
2️⃣ @Controller VS @RestController
- @Controller
- View가 있는 경우에 사용한다.
- 즉, Template Engine인 Thymeleaf, JSP 등을 사용하는 경우
- @RestController
- JSON 데이터를 반환하는 컨트롤러 (@ResponseBody 포함)
- 응답할 Data가 있는 경우에 사용한다.
- 현재는 대부분 @RestController를 사용하여 API가 만들어진다. (Restful API)
📌 예제: @Controller (JSP View 반환)
@Controller
public class HomeController {
@GetMapping("/home")
public String home() {
return "home"; // ViewResolver가 home.jsp를 찾음
}
}
📌 예제: @RestController (JSON 반환)
@RestController
public class ApiController {
@GetMapping("/api")
public Map<String, String> getData() {
return Map.of("message", "Hello, JSON!");
}
}
✔ @RestController = @Controller + @ResponseBody
3️⃣ @Component (Spring Bean 등록)
✔ Spring이 자동으로 객체를 생성하고 관리하도록 등록하는 애노테이션!
✔ 특정 클래스를 Spring Bean으로 등록해서 DI(의존성 주입)할 수 있음!
📌 예제: @Component 사용
@Component
public class MyComponent {
public void doSomething() {
System.out.println("MyComponent 동작!");
}
}
@Service
public class MyService {
private final MyComponent myComponent;
@Autowired
public MyService(MyComponent myComponent) {
this.myComponent = myComponent;
}
public void execute() {
myComponent.doSomething();
}
}
✔ @Service, @Repository, @Controller도 사실 @Component의 확장형! ✅
4️⃣ @Indexed (Spring 검색 최적화)
✔ Spring이 @Component 스캔할 때, 성능을 최적화하도록 도와주는 애노테이션!
✔ Spring Boot 2.1부터 지원됨
✔ 일반적으로 @Repository, @Service, @Controller에 자동으로 적용됨
📌 예제: @Indexed 적용
import org.springframework.stereotype.Service;
import org.springframework.core.annotation.Indexed;
@Indexed
@Service
public class MyService {
public String getMessage() {
return "Hello from MyService";
}
}
✔ Spring이 빈을 검색할 때 더 빠르게 찾을 수 있도록 도와줌 ✅
5️⃣ @Target, @Retention, @Documented (메타 애노테이션)
@Target : 애노테이션이 어디에 적용될지 지정 (클래스, 메서드, 필드 등)
@Retention : 애노테이션이 언제까지 유지될지 지정 (RUNTIME, CLASS, SOURCE)
@Documented : Javadoc에 애노테이션 정보 포함 여부
📌 예제: 직접 커스텀 애노테이션 만들기
import java.lang.annotation.*;
@Target(ElementType.METHOD) // 메서드에만 적용
@Retention(RetentionPolicy.RUNTIME) // 런타임까지 유지
@Documented // Javadoc에 포함
public @interface MyAnnotation {
String value();
}
✔ @MyAnnotation("Hello")처럼 사용할 수 있음 ✅
@Target 부가설명
📌 애노테이션이 적용될 수 있는 위치별 예제
애노테이션 위치 | 예제 | 설명 |
클래스 위 | @Controller @Service @Component @RequestMapping | Spring Bean 등록 |
메서드 위 | @GetMapping @PostMapping @Transactional | HTTP 요청 매핑, 트랜잭션 관리 |
필드 위 | @Autowired @Value("${key}") | 의존성 주입 (DI), 값 주입 |
생성자 위 | @Autowired | 생성자 기반 DI |
매개변수 위 |
@PathVariable @RequestParam | 요청 데이터 매핑 |
애노테이션은 메서드 위에만 선언하는 걸까?
✔ 아니야! 애노테이션은 클래스, 메서드, 필드, 생성자 등 다양한 곳에 선언할 수 있음!
✔ 어떤 애노테이션을 사용하는지에 따라 적용할 위치가 달라짐!
1️⃣ 애노테이션의 적용 위치 (클래스, 메서드, 필드 등)
📌 애노테이션이 어디에 적용될 수 있는지에 대한 규칙은 @Target을 보면 알 수 있어!
✔ @Target은 애노테이션이 적용될 수 있는 위치를 지정하는 메타 애노테이션이야.
📌 예제: @Target 사용 예시
@Target({ElementType.TYPE, ElementType.METHOD}) // 클래스 & 메서드에 적용 가능
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value();
}
✔ ElementType.TYPE → 클래스에 적용 가능
✔ ElementType.METHOD → 메서드에 적용 가능
📌 즉, 애노테이션은 클래스뿐만 아니라 메서드, 필드, 생성자에도 적용 가능해! 🚀
2️⃣ 애노테이션이 적용될 수 있는 위치 예제
📌 (1) 클래스 위에 선언하는 애노테이션
✔ 컨트롤러, 서비스, 리포지토리 같은 Spring Bean을 등록할 때 주로 사용
@Controller // 컨트롤러 클래스 (클래스 단위)
@RequestMapping("/users") // 클래스 전체에 URL 매핑 적용
public class UserController {
@GetMapping("/{id}") // 메서드 단위 매핑
public String getUser(@PathVariable String id) {
return "User ID: " + id;
}
}
✔ @Controller, @RequestMapping은 클래스 단위 애노테이션!
✔ 클래스 위에 선언하면 해당 클래스가 컨트롤러 역할을 하도록 Spring이 인식함
📌 (2) 메서드 위에 선언하는 애노테이션
✔ HTTP 요청 매핑, 트랜잭션 관리, 실행 로깅 등에 사용
@RestController
@RequestMapping("/products")
public class ProductController {
@GetMapping("/{id}") // 특정 메서드에만 적용
public String getProduct(@PathVariable String id) {
return "Product ID: " + id;
}
@PostMapping
@Transactional // 이 메서드는 트랜잭션 관리됨
public String createProduct() {
return "Product Created!";
}
}
✔ @GetMapping, @PostMapping, @Transactional은 메서드 단위 애노테이션!
✔ 특정 요청(GET, POST)을 처리하거나, 트랜잭션을 관리할 때 메서드 위에 선언 ✅
📌 (3) 필드 위에 선언하는 애노테이션
✔ 의존성 주입(DI), 데이터 검증, JSON 매핑 등에 사용
@Component
public class Car {
@Autowired // Engine 객체 자동 주입 (DI)
private Engine engine;
@Value("${car.model}") // application.properties 값 주입
private String model;
}
✔ @Autowired → 필드에 의존성 주입
✔ @Value → 설정 파일(application.properties)에서 값을 주입
✔ 필드에 선언하면 해당 필드에 적용됨! ✅
📌 (4) 생성자 위에 선언하는 애노테이션
✔ Spring의 의존성 주입(DI)에서 주로 사용
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired // 생성자에 의존성 주입
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
✔ @Autowired를 생성자 위에 선언하면, 생성자 주입 방식으로 DI를 수행함! ✅
✔ Spring Boot에서는 생성자가 하나뿐이면 @Autowired 생략 가능!
📌 (5) 파라미터(매개변수) 위에 선언하는 애노테이션
✔ HTTP 요청 파라미터, 경로 변수, 폼 데이터 처리 등에 사용
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public String getUser(@PathVariable("id") String userId) {
return "User ID: " + userId;
}
@PostMapping
public String createUser(@RequestParam("name") String name) {
return "Created User: " + name;
}
}
✔ @PathVariable → URL 경로에서 변수 값 가져옴
✔ @RequestParam → HTTP 요청 파라미터(name=value) 값 가져옴
✔ 매개변수(파라미터) 위에 선언하면 해당 파라미터에 적용됨! ✅
6️⃣ @RequestMapping (요청 매핑)
✔ 클라이언트의 HTTP 요청을 컨트롤러 메서드와 매핑하는 애노테이션!
✔ URL, HTTP 메서드(GET, POST, PUT 등)를 설정 가능!
📌 예제: @RequestMapping 사용
@Controller
@RequestMapping("/users")
public class UserController {
@RequestMapping(value = "/list", method = RequestMethod.GET)
public String getUserList() {
return "userList";
}
}
✔ @RequestMapping("/users/list")는 GET /users/list 요청을 처리!
📌 축약형: @GetMapping, @PostMapping
@Controller
@RequestMapping("/users")
public class UserController {
@GetMapping("/list")
public String getUserList() {
return "userList";
}
@PostMapping("/add")
public String addUser() {
return "redirect:/users/list";
}
}
✔ @GetMapping, @PostMapping을 사용하면 더 간결하게 요청을 매핑할 수 있음! ✅
7️⃣ @PathVariable (경로 변수 매핑)
✔ URL 경로에서 변수 값을 받아올 때 사용!
📌 예제: @PathVariable 사용
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public String getUserById(@PathVariable String id) {
return "User ID: " + id;
}
}
✔ 클라이언트가 /users/123 요청을 보내면 "User ID: 123" 응답! ✅