티스토리 뷰

Back-End

[UMC/시니어 미션#3]

poopooreum 2025. 9. 20. 02:44
반응형

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]

✏️ 단계별 설명

  1. 진입: 모든 요청이 DispatcherServlet으로 들어옴(Front Controller).
  2. 핸들러 탐색 – HandlerMapping: URL/HTTP 메서드 등을 기준으로 실행할 핸들러(컨트롤러 메서드) 와 연결된 인터셉터 목록을 담은 HandlerExecutionChain을 반환 , 대표 구현체: RequestMappingHandlerMapping
  3. 호출 준비 – HandlerAdapter: 찾은 핸들러의 타입에 맞는 어댑터가 실제 호출을 수행.
    대표 구현체: RequestMappingHandlerAdapter (메서드 인자 바인딩, @Valid, @RequestBody 등 지원)
  4. 인터셉터 전처리: preHandle()에서 인증/로깅/트랜잭션 시작 등 공통 전처리. false 반환 시 체인 중단.
  5. 컨트롤러 실행: 비즈니스 로직 호출, Model 작성, 뷰 이름 또는 바디를 반환.
  6. 후처리/뷰 결정
    • 뷰 흐름(템플릿): String/ModelAndView 반환 → ViewResolver가 실제 템플릿을 찾아 렌더링(JSP/Thymeleaf 등).
    • REST 흐름: @ResponseBody/ResponseEntity → HttpMessageConverter가 객체를 JSON 등으로 직렬화하여 응답 바디 작성.
  7. 인터셉터 후처리: postHandle()에서 모델 보강/뷰 앞단 처리, afterCompletion()에서 예외 포함 최종 리소스 정리·로깅.
  8. 응답 전송: 결과가 클라이언트로 전달.

참고: 서블릿 필터는 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)이 클라이언트로 반환

 

✏️ 결과 예시

반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
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
글 보관함