티스토리 뷰
반응형
1. 서블릿 vs. Spring MVC 비교
✏️ 표로 비교하기

✏️ 코드로 비교하기
(A) 서블릿 방식
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
String name = req.getParameter("name");
req.setAttribute("name", name);
req.getRequestDispatcher("/WEB-INF/views/hello.jsp").forward(req, resp);
}
@Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String json = new BufferedReader(new InputStreamReader(req.getInputStream()))
.lines().collect(Collectors.joining());
// 직접 파싱/검증/응답 작성...
resp.setContentType("application/json");
resp.getWriter().write("{\"ok\":true}");
}
}
(B) Spring MVC 방식
@Controller
@RequestMapping("/hello")
public class HelloController {
@GetMapping
public String hello(@RequestParam String name, Model model) {
model.addAttribute("name", name);
return "hello"; // ViewResolver -> /WEB-INF/views/hello.jsp
}
@PostMapping
@ResponseBody
public Map<String, Object> post(@RequestBody RequestDto dto) {
return Map.of("ok", true, "payload", dto);
}
}
✏️ 왜 Spring MVC가 더 편리한가?? (프레임워크 역할)
- Front Controller 패턴: 모든 요청을 DispatcherServlet이 단일 진입점에서 표준 절차로 처리 → 일관성/확장성 ↑
- 어노테이션 매핑: @RequestMapping, @GetMapping 등으로 URL/HTTP 메서드 매핑을 선언적으로 작성
- 데이터 바인딩/검증: 파라미터 → DTO 자동 바인딩, @Valid + BindingResult로 검증 일원화
- 예외 처리 표준화: @ExceptionHandler, @ControllerAdvice로 전역 예외/에러 응답 규약
- 뷰 해석: ViewResolver로 템플릿(JSP, Thymeleaf 등) 선택 자동화
- 메시지 컨버터: HttpMessageConverter로 JSON/XML 등 자동 변환(REST 개발 생산성↑)
- 확장 지점: HandlerInterceptor(전/후처리), ArgumentResolver, ReturnValueHandler, 필터/AOP 등 다층 확장
✏️ DispatcherServlet 내부 요청 처리 플로우
키워드: HandlerMapping → HandlerAdapter → Controller → ViewResolver/HttpMessageConverter → Interceptor
✏️ 다이어그램 (Mermaid)
flowchart TD
A[Client Request] --> B[DispatcherServlet]
B --> C[HandlerMapping<br/>요청 → HandlerExecutionChain]
C -->|핸들러 + 인터셉터 목록| B
B --> D[Interceptor.preHandle()]
D --> E[HandlerAdapter.invoke(Controller)]
E --> F[Controller Method 실행]
F --> G{리턴 타입?}
G -->|뷰 이름/ModelAndView| H[ViewResolver]
H --> I[View 렌더링]
G -->|@ResponseBody/HttpEntity| J[HttpMessageConverter로 바디 작성]
I --> K[Interceptor.postHandle()]
J --> K
K --> L[Interceptor.afterCompletion()]
L --> M[Response to Client]
✏️ 단계별 설명
- 진입: 모든 요청이 DispatcherServlet으로 들어옴(Front Controller).
- 핸들러 탐색 – HandlerMapping: URL/HTTP 메서드 등을 기준으로 실행할 핸들러(컨트롤러 메서드) 와 연결된 인터셉터 목록을 담은 HandlerExecutionChain을 반환 , 대표 구현체: RequestMappingHandlerMapping
- 호출 준비 – HandlerAdapter: 찾은 핸들러의 타입에 맞는 어댑터가 실제 호출을 수행.
대표 구현체: RequestMappingHandlerAdapter (메서드 인자 바인딩, @Valid, @RequestBody 등 지원) - 인터셉터 전처리: preHandle()에서 인증/로깅/트랜잭션 시작 등 공통 전처리. false 반환 시 체인 중단.
- 컨트롤러 실행: 비즈니스 로직 호출, Model 작성, 뷰 이름 또는 바디를 반환.
- 후처리/뷰 결정
- 뷰 흐름(템플릿): String/ModelAndView 반환 → ViewResolver가 실제 템플릿을 찾아 렌더링(JSP/Thymeleaf 등).
- REST 흐름: @ResponseBody/ResponseEntity → HttpMessageConverter가 객체를 JSON 등으로 직렬화하여 응답 바디 작성.
- 인터셉터 후처리: postHandle()에서 모델 보강/뷰 앞단 처리, afterCompletion()에서 예외 포함 최종 리소스 정리·로깅.
- 응답 전송: 결과가 클라이언트로 전달.
참고: 서블릿 필터는 DispatcherServlet 바깥(서블릿 컨테이너 레벨), 인터셉터는 스프링 MVC 체인 내부에서 동작합니다.
2. AOP(Aspect-Oriented Programming) 원리 탐구
✏️ AOP란 무엇인가?
- 관점 지향 프로그래밍(Aspect-Oriented Programming)
- 프로그램의 핵심 로직(Core Concern)과 공통 관심사(Cross-cutting Concern)를 분리하여 모듈화하는 기법
- 대표적인 공통 관심사: 로깅, 트랜잭션, 보안, 예외 처리
👉 즉, 비즈니스 로직 코드에 반복적으로 흩어져 있는 기능을 따로 빼내어 관리하는 방식
✏️ OOP vs AOP 차이
| 초점 | 객체 단위로 책임 분리 | 횡단 관심사를 모듈화(Aspect) |
| 예시 | UserService, OrderService | LoggingAspect, TransactionAspect |
| 문제 | 공통 코드가 여러 클래스에 중복됨 | 공통 코드가 하나의 Aspect로 관리됨 |
| 강점 | 객체 중심 추상화, 재사용성 ↑ | 코드 중복 제거, 유지보수성 ↑ |
✏️ AOP 핵심 개념
- Advice: 언제 실행할 것인지 정의된 코드 (Before, After, Around 등)
- JoinPoint: 프로그램 실행 중에 Advice가 끼어들 수 있는 지점 (ex. 메서드 호출 시점)
- Pointcut: Advice를 적용할 JoinPoint의 표현식 (ex. 특정 패키지/클래스의 모든 메서드)
- Aspect: Advice + Pointcut을 합친 모듈 (ex. LoggingAspect 클래스)
- Weaving: 실제 코드 실행 지점에 Aspect를 삽입하는 과정
- 컴파일 타임 위빙: 컴파일 시점에 바이트코드에 삽입
- 런타임 위빙: 실행 시점에 프록시 객체를 생성하여 삽입 (Spring AOP 기본 방식)
✏️ Spring AOP와 프록시 패턴
- Spring AOP는 프록시 패턴 기반으로 동작
- @EnableAspectJAutoProxy → 스프링이 **프록시 객체(Proxy)**를 생성
- 클라이언트는 실제 Bean이 아니라 프록시 객체를 호출
- 프록시는 실제 대상 객체 호출 전후로 Advice 로직을 끼워 넣음
- 프록시 생성 방식:
- JDK Dynamic Proxy: 인터페이스 기반 (기본)
- CGLIB Proxy: 클래스 기반 (인터페이스가 없는 경우)
✏️ 예시 코드
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.umc.project..*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("호출 메서드: " + joinPoint.getSignature());
}
@AfterReturning(value = "execution(* com.umc.project..*(..))", returning = "result")
public void logAfter(Object result) {
System.out.println("리턴 값: " + result);
}
}
3. 도전 과제 - Spring AOP 기반 Discord 알림 전략
✏️ Gradle 설정
// build.gradle.kts
// WebClient (비동기 HTTP 통신)
implementation("org.springframework.boot:spring-boot-starter-webflux")
// AOP
implementation("org.springframework.boot:spring-boot-starter-aop")
✏️ AOP 설정 클래스
// AspectConfig.kt
@Configuration(proxyBeanMethods = true)
@EnableAspectJAutoProxy(proxyTargetClass = true)
class AspectConfig {
}
✏️ Discord 알람 전송기
// DiscordAlarmSender.kt
@Component
class DiscordAlarmSender(
@Value("\${logging.discord.web-hook-url}")
private val webHookUrl: String,
private val environment: Environment,
private val discordUtil: DiscordUtil,
private val webClient: WebClient = WebClient.create()
) {
private val log = LoggerFactory.getLogger(this::class.java)
fun sendDiscordAlarm(exception: Exception, httpServletRequest: HttpServletRequest) {
val alarmingProfiles = listOf("dev")
if (environment.activeProfiles.none { it in alarmingProfiles }) return
val alarmBody = discordUtil.createMessage(exception, httpServletRequest)
webClient.post()
.uri(webHookUrl)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(alarmBody)
.retrieve()
.bodyToMono(Void::class.java)
.subscribe(
null,
{ error -> log.error("[*] Error to Send Discord Alarm", error) },
{ log.info("[*] Success to Send Discord Alarm") },
)
}
}
✏️ AOP 클래스 (Aspect)
// DiscordLoggerAop.kt
@Aspect
@Component
class DiscordLoggerAop(
private val discordAlarmSender: DiscordAlarmSender
) {
@Pointcut("execution(* gat_be.dev.common.exception.GeneralExceptionAdvice.handleGeneralException(..))")
fun generalExceptionErrorLoggerExecute() {}
@Pointcut("execution(* gat_be.dev.common.exception.GeneralExceptionAdvice.handleException(..))")
fun serverExceptionErrorLoggerExecute() {}
@Before("generalExceptionErrorLoggerExecute()")
fun generalExceptionLogging(joinPoint: JoinPoint) {
val request = getCurrentRequest()
val exception = joinPoint.args[0] as GeneralException
if (exception.errorStatus.httpStatus == HttpStatus.INTERNAL_SERVER_ERROR) {
discordAlarmSender.sendDiscordAlarm(exception, request)
}
}
@Before("serverExceptionErrorLoggerExecute()")
fun serverExceptionLogging(joinPoint: JoinPoint) {
val request = getCurrentRequest()
val exception = joinPoint.args[0] as Exception
discordAlarmSender.sendDiscordAlarm(exception, request)
}
private fun getCurrentRequest(): HttpServletRequest {
return (RequestContextHolder.currentRequestAttributes() as ServletRequestAttributes).request
}
}
✏️ Discord 메시지 유틸
// DiscordUtil.kt
@Component
class DiscordUtil {
fun createMessage(exception: Exception, httpServletRequest: HttpServletRequest): MessageDto {
return MessageDto(
content = "# 🚨 서버 에러 발생 🚨",
embeds = listOf(
EmbedDto(
title = "에러 정보",
description =
"### 에러 발생 시간\n" +
ZonedDateTime.now(ZoneId.of("Asia/Seoul"))
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH시 mm분 ss초")) +
"\n### 요청 엔드포인트\n" + getEndPoint(httpServletRequest) +
"\n### 요청 클라이언트\n" + getClient(httpServletRequest) +
"\n### 에러 스택 트레이스\n" +
"```\n" + getStackTrace(exception).substring(0, 1000) + "\n```"
)
)
)
}
private fun getEndPoint(request: HttpServletRequest): String {
val method = request.method
val url = request.requestURL.toString()
return "[$method] $url"
}
private fun getClient(request: HttpServletRequest): String {
val ip = request.remoteAddr
val userIdentifier = request.userPrincipal?.name?.let { "/ [UserId]: $it" } ?: ""
return "[IP]: $ip $userIdentifier"
}
private fun getStackTrace(e: Exception): String = e.stackTraceToString()
}
✏️ DTO 정의
// DiscordRequest.kt
data class MessageDto(
val content: String,
val embeds: List<EmbedDto>
)
data class EmbedDto(
val title: String,
val description: String
)
✏️ 전체 과정
sequenceDiagram
participant User as 클라이언트
participant App as Spring App
participant AOP as DiscordLoggerAop
participant Sender as DiscordAlarmSender
participant Discord as Discord Webhook
User ->> App: HTTP 요청
App ->> App: 예외 발생
App ->> AOP: GeneralExceptionAdvice 호출 직전
AOP ->> Sender: sendDiscordAlarm(exception, request)
Sender ->> Discord: WebClient POST (JSON 메시지)
Discord -->> Sender: 응답 (성공/실패)
Sender ->> AOP: 로그 기록
AOP ->> App: 예외 처리 계속 진행
단계별 설명
사용자가 API 요청 → 서버에서 예외 발생
Spring 전역 예외 처리기(GeneralExceptionAdvice) 호출
DiscordLoggerAop의 @Before Advice가 가로채어 Discord 알림 로직 실행
DiscordAlarmSender가 WebClient로 Discord Webhook 호출
Discord 채널에 메시지 전송 → 에러 정보 확인 가능
이후 정상적으로 예외 응답(ResponseEntity)이 클라이언트로 반환
✏️ 결과 예시

반응형
'Back-End' 카테고리의 다른 글
| [Spring/스프링] - QueryDSL에서 페이징 성능 개선하기 (0) | 2025.11.21 |
|---|---|
| [UMC/시니어 미션#4] (0) | 2025.09.27 |
| [UMC/시니어 미션 #2] (3) | 2025.09.18 |
| [UMC/시니어 미션 #1] (0) | 2025.09.17 |
| [Spring/스프링] - 스프링 부트(Kotlin) Github Actions 자동 배포 (0) | 2025.08.29 |
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- js
- 자바스크립트
- 자료구조
- C++
- Do it!
- 스프링 부트 crud 게시판 구현
- HTML5
- 유니온 파인드
- DP
- java
- DFS
- c++ string
- 투 포인터
- 백준 풀이
- 카운팅 정렬
- 반복문
- 세그먼트 트리
- html
- BFS
- 스택
- 유클리드 호제법
- 백준
- 이분 매칭
- C++ Stack
- CSS
- 알고리즘 공부
- 에라토스테네스의 체
- 우선순위 큐
- 알고리즘
- 자바
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
글 보관함
