Recording/멋쟁이사자처럼 BE 13기

[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_35일차_"스프링 AOP"

LEFT 2025. 1. 21. 17:42

🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [35]일차

🚀35일차에는 AOP에 대해 배우고 관련된 용어 @Before, @After 어노테이션 등에 대해서도 실습해보았다.

회고를 통해 Proxy 객체에 대해서도 부족했던 부분을 더 공부해봐야겠다.


AOP (Aspect Oriented Programming)

  • 관점지향 프로그래밍은 객체지향 프로그래밍을 보완
  • 객체지향과 상반된 것 같지만 목적에 맞게 클래스를 만들어서 하나의 객체로 분리하는 객체지향과 달리
    관점지향은 비즈니스 웹 애플리케이션에서 핵심관심과 횡단관심으로 분리

  • 프레젠테이션 계층 / 서비스 계층 / 데이터 계층 에 대해
    전체적으로 필요한 관심사들(로깅/보안/트랜잭션 등)이 횡단으로 존재

  • 자체적 언어라기보단 기존의 OOP언어를 보완하고 확장함 (=객체지향을 좀 더 잘 사용하도록 도와줌)
  • 자바 진영의 AOP도구 중 대표적으로 AspectJ, JBossAOP, SpringAOP
  •  

정리하자면
💡AOP는 횡단 관심사 코드를 핵심 비즈니스 로직 코드와 분리해
간결성을 높이고 변경에 유연하도록 확장이 가능한 목적


AOP 적용 방식 3가지

  • 컴파일 시점 적용 = AspectJ컴파일러가 .java파일을 컴파일 시 부가기능을 추가해 .class파일로 컴파일

  • 클래스 로딩 시점 적용 = JVM 내 .class파일을 올리는 시점에서 바이트 코드를 조작하여 부가기능 추가

  • 런타임 시점 적용 = 이미 실행 중이어서 코드를 조작하기 어려울때 스프링, 컨테이너, DI, Bean 등
    여러 개념 기능을 활용해 “프록시”를 통해 부가기능을 추가
    ➡️Proxy(프록시):
    메소드 실행시점에만 다음 타겟을 호출할 수 있어 “런타임 시점 적용”방식은 메소드 실행 지점으로 제한

    ➡️스프링AOP는 런타임 시점에 적용하는 방식 사용
    ➡️이유 : 컴파일 시점과 클래스 로딩 시점에 적용하기 위해 필요한 별도 컴파일러와 클래스 로더가 복잡하기때문

AOP 목적 및 장점

  • 중복을 줄여서 적은 코드 수정에도 전체 변경이 가능하도록 함 
  • 관심의 분리 (Seperation of Concerns)
    ➡️핵심관심(업무로직 = 비즈니스로직) + 횡단관심(트랜잭션/로그/보안/인증처리 등)으로 관심을 분리
    ex. 핵심관심(계좌이체/입출금/이자계산) + 횡단관심(비밀번호 유효성 검사/입출금 트랜잭션관리)

  • 장점 : 중복코드제거, 효율적 유지보수, 높은 생산성, 변화 수용 용이

정리하자면
핵심관심들은 수정하지 않고 횡단관심을 끼워넣어 코드의 중복을 제거함
기존에는 메소드 호출코드 등을 핵심관심 부분에 포함해야해서 메소드 교체가 이루어지면
핵심관심 또한 수정이 필요한 경우가 있었는데 AOP는 메소드 교체가 이루어져도 핵심관심은 그대로 유지하고
횡단관심 부분에서 메소드를 갈아끼워서 쉽게 교체할 수 있도록함


AOP 용어

  • Joinpoint
    - 메소드를 호출하는시점, 예외발생시점 같이 애플리케이션을 실행할떄 특정 작업이 실행되는 “시점”을 의미

  • Advice
    - Joinpoint에서 실행되어야하는 코드 (=특정 작업이 실행되는 시점에 실행되어야하는 코드)
    - 횡단관점(횡단관심)에 해당 (트랜잭션/로그/보안/인증 등) ➡️메소드 실행 시의 특정 행동을 의미

  • Target
    - 실질적 비즈니스 로직을 구현하고 있는 코드 (핵심관점(=핵심관심, 비즈니스로직))

  • Pointcut (포인트컷)
    - Target클래스와 Advice 가 결합(Weaving)될때 둘 사이의 결합 규칙 정의
    ex. Advice가 실행된 Target의 특정 메소드 등을 지정

  • Aspect (애스펙트)
    - Advice와 Pointcut을 합쳐서 하나의 Aspect 일정한 패턴을 가지는 클래스에 Advice를 적용하도록 지원할 수 있는 것

  • Weaving (위빙)
    - AOP에서 Joinpoint들을 Advice로 감싸는 과정 이 Weaving작업을 도와주는 것이 AOP 도구의 역할

Spring AOP

  • 스프링은 Aspect의 적용 대상 객체 (Target) 에 Proxy를 만들어서 제공
    대상 객체 (Target) 를 사용하는 코드에서는 Proxy를 통해 간접적으로 접근

  • Proxy : 공통기능(Advice)을 실행한뒤 대상 객체 (Target) 의 실제 메소드를 호출하거나
    대상 객체 (Target) 의 실제 메소드가 호출된 뒤 공통기능(Advice)을 실행

▶️실습 - AOP

  • AOP사용을 위해 build.gradle에 dependencies 추가
    implementation 'org.springframework.boot:spring-boot-starter-aop'

 

package com.example.aop.exam;

@Component
public class SimpleService {
    // 핵심관심. = 개발자가 구현
    public String drinking(){
        //target
        System.out.println("SimpleService (drinking()) 실행");
        return "물을 마십니다.";
    }
    public void hello(){
        System.out.println("SimpleService (hello()) 실행");
    }
    public void setName(String name){
        System.out.println("setName() 실행");
    }
    public void getName(){
        System.out.println("getName() 실행");
    }
}
  • 핵심관심부분인 대상 객체 (Target)가 있는 SimpleService클래스

@Before

  • JoinPoint가 실행되기 전에 Advice를 실행
  • JoinPoint : 현재 실행 중인 메소드나 대상 객체와 관련된 정보를 제공함
  • 💡작업 전 준비 작업
package com.example.aop.exam;

import org.aspectj.lang.annotation.Aspect;

// Pointcut + Advice
@Aspect
@Component
public class ServiceAspect {
    @Before("execution(* com.example.aop.exam.SimpleService.*(..))")
    public void before(JoinPoint joinPoint){// before 매개변수로는 Joinpoint를 받아낼 수 있음
        System.out.println("Before의 메소드(before()) 실행 :::::::::::::: ");
    }
}
  • @Before("execution(* com.example.aop.SimpleService.*(..))")
    ➡️
    execution(리턴타입 패키지경로.클래스이름.메소드이름(매개변수)) 구조
    ➡️execution 뒤 (*) : 리턴타입이 무엇이든지 실행 하겠다는 의미
    ➡️SimpleService 클래스 뒤 (.*) : SimpleService의 모든 메소드를 사용하겠다는 의미
    ➡️SimpleService.*(..) : 매개변수는 상관하지 않음

  • @Before("execution(* com.example..)");
    ➡️이렇게 쓰이면 example패키지 안의 다른 패키지들도 포함

  • ❓패키지와 클래스명을 모두 생략
    ➡️ 이 애플리케이션 내에 있는 것들이 모두 대상이 되고 그 안에 있는 것들을 사용한다.
    실제 애플리케이션이 커졌을 경우에는 지정해서 사용하도록 구체적으로 명시해주는 것이 좋다.

  •  
  • Pointcut 과 Advice를 합친 @Aspect를 어노테이션으로 선언

  • @Component : 컴포넌트임을 명시해야함

  • Advice의 유형 : Before, After Returning, After Throwing, After, Around

  • @Before 작성 후에는 SimpleService 클래스의 메소드가 수행되기 전 Before로 지정한 메소드가 먼저 수행

만약

@Before("execution(* com.example.aop.exam.SimpleService.hello(..))")

  • SimpleService.hello(..)
    ➡️모든 메소드(*)가 아닌 hello(..)로 특정하게되면 hello(..) 메소드의 출력 전에만 Before메소드가 실행

만약

@Before("execution(* com.example.aop.exam.SimpleService.set*(..))")
  • SimpleService.set*(..)
    ➡️setter 관련 메소드가 실행되기 전에만 Before메소드가 실행되도록 설정

정리하자면
애플리케이션이 실행될 때 @Before가 끼어들어가서 SimpleService 클래스의 “핵심로직”의 코드를 변경하지 않고도
핵심로직 코드의 출력(메소드들) 중간에 Before 메소드가 끼어들어 출력되고 있는 것을 확인 가능

만약

@Before("execution(* com.example.aop.exam.*Service.*(..))")
public void before(JoinPoint joinPoint){// before 매개변수로는 Joinpoint를 받아낼 수 있음
    System.out.println("Before의 메소드(before()) 실행 :::::::::::::: " 
    					+ joinPoint.getSignature().getName());
}
  • getSignature().getName()
    ➡️getSignature() : Service로 끝나는 클래스의 모든 메소드 정보가 포함 (SimpleService클래스 해당)
    ➡️getName() : 메소드의 이름을 가져올 것

  • 메소드 이름들이 출력

@After

  • JoinPoint의 실행 완료 후 항상 Advice를 실행
  • 💡작업이 끝난 후 뒷정리
@After("execution(* com.example.aop.exam.*Service.*(..))")
public void after(){
    System.out.println("After의 메소드 (after()) 실행 :::::::::::::: ");
}


@Pointcut

  • 특정 지점(Point)을 표현
  • 어떤 메소드에 Advice (작업)을 적용할지 “정의”
  • 💡클래스의 메소드에 규칙을 정함
@Pointcut("execution(* com.example.aop.exam.*Service.*(..))")
public void pc(){ }

@Pointcut("execution(* hello())")
public void pcHello(){ }

@Before("pc()")
public void before(JoinPoint joinPoint){
    System.out.println("Before의 메소드(before()) 실행 :::::::::::::: " + joinPoint.getSignature().getName());
}

@After("pc()")
public void after(){
    System.out.println("After의 메소드 (after()) 실행 :::::::::::::: ");
}
  • @Pointcut에 execution지정해준 것과 지정한 메소드(pc())를 통해 before와 after에도 전달하여 사용 가능
  • @Pointcut은 여러개 선언할 수 있고 before와 after등에선 원하는 @Pointcut 메소드를 선택해서 사용 가능

@AfterReturning

  • JoinPoint가 정상적으로 완료된 후 Advice를 실행
  • 💡작업이 성공적으로 끝났을 경우의 할 일
@AfterReturning(pointcut = "pc()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result){ // result는 어떤 값이 올지 모르므로 Object타입으로 받음
    System.out.println("AfterReturning 실행 :::::::::::::: " + joinPoint.getSignature().getName() + ", return value : " + result);
}
  • @After와의 차이점은 @AfterReturning은 예외발생 시 실행되지 않지만 @After는 조인포인트 처리 완료 후 항상 실행

  • Advice가 중간에 return값을 가로챌 수 있음
    ex. "물을 마십니다" 출력 전 return value에서 먼저 “물을 마십니다”가 출력되었음

@AfterThrowing

  • JoinPoint에서 예외 발생 시 Advice를 실행
  • 💡작업 중 예외 발생 시 예외를 수습하는 작업
@AfterThrowing(value = "pc()", throwing = "ex")
public void afterThrowing(Throwable ex){
    System.out.println("AfterThrowing 실행 :::::::::::::: ");
    System.out.println("exception value : " + ex);
}
public void hello(){
    System.out.println("SimpleService (hello()) 실행");

    if(1==1){ // 강제로 Exception을 발생시켜봄
        throw new RuntimeException();
    }
}
  • SimpleService.hello()메소드의 예외를 강제로 발생시킨 후
    @AfterReturning은 실행되지 않고 @After는 실행됨 (@After는 예외에 관련없이 항상 실행)
  • ex : 발생한 예외의 객체

  • @AfterReturning과 @After가 잘 실행되다가 hello()메소드를 만나게되면
  • 예외를 발생에 따라 @AfterThrowing이 실행된다.
  • @After는 예외발생과 상관없이 항상 실행되므로 after()는 실행됨

@Around

  • JoinPoint 실행 전 후의 동작을 모두 제어 가능
  • ProceedingJoinPoint : 실제 메소드를 실행하는 역할을 함
  • 작업 준비 전 계획서 작성 및 작업 완료 후 작업 완료 보고서 작성 등에 대한 전 후 작업을 수행
@Around("pc()")
public String around(ProceedingJoinPoint pjp) throws Throwable{
    System.out.println("Around :::::::::::::: 실제 메소드가 [실행되기 전] 해야할 일");

    String value = (String)pjp.proceed(); // 이 proceed()를 호출해줘야지만 실제 Target의 메소드를 호출한다. -- 이 줄을 기준으로 윗 부분은 실제 메소드가 실행되기 전, 아랫 부분이 시점이 달라짐
    System.out.println("Around :::::::::::::: 실제 메소드가 [실행된 후] 해야할 일");
    value += "Test AOP Run!!";

    return value;
}

  • @Around 어노테이션으로 감싸져서 애플리케이션이 실행
  • @Around에서 반환 값 뒤에 "Test AOP Run" 문자열을 추가하여 출력
    ➡️ex. 이를 활용하면 할인율 같은 것을 적용 가능

  • String value = (String)pjp.proceed()
    pjp.proceed()의 결과값을 문자열로 변환하여 “물을 마십니다”가 value에 들어오고

  • value += “Test AOP Run!!”
    모두 "물을 마십니다.Test AOP Run!!" 문자열이 출력

 

@SpringBootApplication

  • 이 Bean들을 사용할 Application 클래스
@SpringBootApplication
public class AopServiceApplication implements CommandLineRunner {
    public static void main(String[] args) {
        SpringApplication.run(AopServiceApplication.class);
    }
    @Autowired
    private SimpleService simpleService;

    // CommandLineRunner의 메소드를 오버라이딩
    @Override
    public void run(String... args) throws Exception {
        System.out.println("-----simpleService Test-----");
        System.out.println(simpleService.drinking());
        simpleService.hello();
        simpleService.setName("Miguel");
        simpleService.getName();
    }
}
  • private SimpleService simpleService;
    ➡️@Autowired를 통해 SimpleService를 주입시킴 (Injection)

CommandLineRunner

  • Spring Boot에서 제공하는 기능으로 애플리케이션이 초기화되고
    실행된 후에는 특정 로직을 실행할 수 있도록 제공되는 인터페이스

  • 애플리케이션 실행 직 후 SimpleService 테스트

  • CommandLineRunner의 run() 메소드는 Spring 컨텍스트 초기화 이후 실행되므로
    SimpleService 객체가 @Autowired로 이미 주입된 상태

  • 주입된 SimpleService를 이용해 SimpleService클래스의 메소드를 호출하고 출력

CommandLineRunner 활용

  • 애플리케이션 초기화 작업: 데이터베이스의 초기 데이터를 삽입 및 설정 값을 로드
  • 테스트 및 디버깅: 특정 서비스나 컴포넌트의 동작을 간단히 확인
  • 명령줄 입력 처리: args 파라미터를 사용하여 애플리케이션 실행 시 전달된 명령줄 인수를 처리

SimpleService의 코드를 변경하지 않아도(=핵심로직을 수정하지 않아도)
트랜잭션, 로깅을 추가해도 AopServiceApplication을 실행하게되면 기존 핵심로직에 덧붙여서 실행됨


Proxy

  • 대리객체 ➡️직접접근하는 대신 간접접근하여 Proxy를 통하여 수행 가능
  • 사용자 입장에서는 원래 객체를 사용하는 것처럼 Proxy (대리객체)를 사용
    ➡️Proxy 객체는 실제 객체와 동일한 인터페이스를 구현하거나 상속 관계를 통해 동일한 메소드를 제공하기 때문

  • 사용자가 어떤 메소드를 호출하면, 요청이 Proxy 객체로 전달되어
    Proxy 객체가 부가 작업을 수행한 후에 실제 객체의 메소드를 호출

즉 실제 객체의 앞뒤로 부가 작업(ex. 로깅, 트랜잭션 관리 등)을 추가하기 위해 생성되는 객체


MVC 패턴

  • Model, View, Controller 의 약자로 “하나의 애플리케이션을 3가지 역할로 나눈 패턴”

  • Model : 애플리케이션의 하는 일 (=비즈니스 로직 담당)
    ex. DB연동으로 사용자 입력 데이터를 입력, 사용자에게 출력할 데이터 관리
    ➡️Model은 View나 Controller에 대해 어떤 정보도 갖지 않고 Model의 역할만 해야한다.

  • View : 사용자에게 보여지는 UI 담당
    ex. 웹개발에서 JSP가 보통 View를 담당
    ➡️Model의 정보를 가지고 있으면 안되며, Controller을 통해 Model의 정보를 받아 출력만 해줌

  • Controller : 다리 역할로 Model과 View를 연결 및 정보전달 담당

Model 1 vs Model 2

  • JSP를 이용해 구성할 수 있는 웹 애플리케이션 구조는 Model1과 Model2로 나뉨

Model 1

  • View와 Controller의 역할을 JSP가 모두 수행
  • JSP가 사용자 요청을 처리하고 View(응답 페이지) 처리를 모두 수행

  • 장점 : 구조가 단순, 개발시간이 짧아 개발 비용 감소 (작은 프로젝트에 유리)
  • 단점 :
    ➡️View코드와 요청 처리 Java코드가 함께있어서 JSP 코드가 복잡
    ➡️JSP코드에 백엔드, 프론트엔드가 함께있어서 분업화가 힘듦
    ➡️프로젝트 규모가 커질수록 유지보수가 어렵고 확장성이 좋지 않음

Model 2

  • 유지보수가 힘든 Model 1의 단점을 보완하여 등장
  • JSP가 View(응답 페이지)처리만 수행, Controller 역할은 Servlet이 수행
  • MVC패턴을 “웹 개발에 도입한 구조”
  • 사용자 요청 처리는 Servlet ⇒ (MVC 패턴의 Viewl)
  • 비즈니스 로직 처리는 Java Class (Service, Dao, Java Beans) ⇒ (MVC 패턴의 Model)
  • 사용자에게 응답 페이지 출력하는 것은 JSP ⇒ (MVC 패턴의 Controller)

  • 장점 :
    ➡️Model에 비해 복잡하지 않음
    ➡️백엔드/프론트엔드 분업화 가능 유지보수가 쉽고 확장성이 좋음
    ➡️큰 프로젝트에 유리
  • 단점 : 구조가 복잡, 개발시간이 증가하여 개발비용도 증가

Spring MVC

  • Spring에서 제공하는 웹 모듈로 Model, View, Controller 세가지 구성요소를 사용하여
    클라이언트의 요청을 편리하게 해주는 기능을 제공

  • 사용자의 HTTP Request 요청들을 처리하여 응답할 수 있도록 도와주는 프레임워크
  • Servlet 기반으로 동작하며 웹 개발을 위한 MVC 프레임워크를 제공함

Spring MVC 구성요소

  • DispatcherServlet(Front Controller)
    ➡️모든 클라이언트의 요청을 전달받아서 Controller에게 요청을 전달
    ➡️Controller가 결과값을 리턴하면 View에 다시 전달하여 응답을 생성하게됨
  • HandlerMapping
    ➡️클라이언트의 요청 URL을 처리할 Controller를 결정하게됨 (핸들러 객체를 선택)
    ➡️DispatcherServlet은 하나 이상의 HandlerMapping을 가질 수 있음

  • ModelAndView
    ➡️Controller가 처리한 데이터 및 화면에 대한 정보를 가진 객체
  • Controller, View, ViewResolver 등

💡HandlerMapping, HandlerAdaptor, ViewResolver 등은 "인터페이스"이므로 스프링이 각각 구현체들을 이미 만들어놨음

Spring MVC 실행순서

  • 1. DispatcherServlet이 클라이언트의 요청을 받음 (수신)
  • 2. HandlerMapping을 통해 Controller가 선택
  • 3. 요청을 선택된 Controller에 전달하고 Controller는 요청 처리 후 결과 반환
  • 4. ModelAndView 객체에 담긴 수행결과는 DispatcherServlet에 리턴됨
    ➡️실제 JSP정보를 갖고 있진 않음
  • 5. View에서 반환된 결과를 화면에 출력

🚀 회고를 통해 MVC 패턴 및 Spring MVC, Proxy에 대해서 추가공부를 할 수 있었다.

점점 문법도 많아지고 생소한 기능이 많아서 익히기 위해서는 실습코드를 자주 보고 해석하는 연습을 해야할 것 같다.

그래도 회고를 진행하면서 AOP에 대해서 좀 더 이해할 수 있게되어 다행이었다.