🍃 Spring

스프링 AOP와 🐌 용어 정리 🐌 그리고 공통 관심사 호출해보기

보배 진 2026. 1. 23. 10:29

스프링 AOP

스프링이란?

"IoC와 AOP를 지원하는 경량의 프레임워크"

IoC : 제어의 역행

AOP : 관점 지향 프로그래밍

경량의 : POJO

 

"유지보수가 용이한 코드"

 IoC(제어의 역전) ▶ 낮은 결합도
AOP(관점 지향 프로그래밍) ▶ 높은 응집도

 


용어 정리

🐌 관심 분리 🐌 
Separation of Concerns
비즈니스 메서드마다 공통으로 존재하는 코드들
이러한 코드들을 다른 클래스에 작성하는 것
(박혀있는 횡단 관심을 빼내는 것)
🐌 횡단 관심 🐌 
Crossing Concerns

로깅, 트랜잭션, 인증▪인가, 예외 처리
비즈니스 메서드마다 공통으로 존재하는 코드들
어드바이스
공통 로직
🐌 핵심 관심 🐌 
Core Concerns
비즈니스 메서드
CRUD
조인 포인트
JoinPoint
🐌 조인 포인트 🐌
JoinPoint
포인트컷 후보
(핵심관심이 AOP로 들어오면 조인포인트라 불린다)
🐌 포인트컷 🐌
Pointcut
선택된 조인 포인트
선택된 비즈니스 메서드
(핵심관심이 AOP로 들어오면 포인트컷라 불린다)

🐌 어드바이스 🐌
Advice
횡단 관심
비즈니스 메서드마다 공통으로 존재하는 코드들
'동작 시점'이라는 개념이 존재함
🐌 위빙 🐌
Weaving
포인트컷으로 지정한 핵심 관심 메서드가 호출될 때,
어드바이스에 해당하는 횡단 관심 메서드가 삽입되는 것
스프링에서는 런타임 위빙 처리만!
(런 == 실행 : 즉, 실행시에만 위빙 처리됨)
🐌 에스팩트 🐌
Aspect
포인트컷과 어드바이스의 결합
어드바이져 (Advisor)
애스팩트 설정에 따라 위빙을 처리하는 스프링

 

ex) 조인포인트(비즈니스 메서드)가 4개 있다

트랜잭션 횡단관심사의 포인트컷(비즈니스 메서드에서 선택된 것)은 CUD이다

 


 

 

 

package com.example.biz.common;


// 로그찍는 횡단관심사
public class LogAdvice {
	public void printLog() {
		System.out.println("[로그] 유진이의 로그");
	}
}

로그 찍는 LogAdvice.java 생성

 

 

@Service("bs")
public class BoardServiceImpl implements BoardService {
	@Autowired
	private BoardDAO boardDAO;
	private LogAdvice logAdcive;
	// private 어드바이스(횡단관심사, 공통로직) 어드바이스 
	
    // 일단 DI를 해줬다 (new 형태라 좋은 코드는 아님)
	public BoardServiceImpl() {
		logAdcive = new LogAdvice();
	}
    
	@Override
	public boolean insertBoard(BoardDTO dto) {
		logAdcive.printLog();
		return boardDAO.insertBoard(dto);
	}
	[ 생략.. ]

BoardServiceImpl.java에서

멤버변수 생성하고 insertBoard에서 호출

 

이제 로그가 먼저 찍한다

동작 시점이 핵심관심(DAO)보다 먼저다

 

 

package com.example.biz.common;

public class PlusLogAdvice {
	public void printLog() {
		System.out.println("[로그] 유진이의 향상된 로그");
	}
}

PlusLogAdvice.java 를 새로 만들었다고 치자

로그를 이걸로 바꾸고 싶으면 

private PlusLogAdvice logAdcive;

이렇게 바꾸면 됨

근데 AOP는 "횡단관심사를 코드에 직접 사용하지 않는다"

 

 

 

위에서 한 거 AOP 설정으로 끼워넣어보기

AOP 적용 전 (나쁜 코드)

logAdvice.printLog();

▪ 비즈니스 코드에 횡단 관심이 박혀 있음

▪ 로그 바꾸려면 코드 수정해야 함

 

 

AOP 적용 후 (좋은 코드)

▪  new 하지 않음

▪  AOP 설정으로만 연결

▪ 비즈니스 코드는 CRUD에만 집중

 

 

AOP 설정 흐름

1️⃣ 의존성 추가 (pom.xml) : aspectjrt, aspectjweaver

2️⃣ AOP 네임스페이스 추가 : xmlns:aop="http://www.springframework.org/schema/aop"

3️⃣ Pointcut 설정 : execution(* cohttp://m.example.biz..*Impl.*(..))

4️⃣ Aspect 설정 : <aop:before method="printLog" pointcut-ref="aPointcut"/>

 

 

 

 <  AOP 설정 해보기 >

1) .jarx2를 pom.xml을 통해 DI

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

 

pom.xml 적용이 잘 안될 떄는

Maven ▶ Update Project ▶Force Update of Snapshots 채크 후 OK

 

 

 

 

 

2) applicationContext.xml을 AOP 스키마(네임스페이스) 추가

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
   
   		[ 코드 추가 될 예정 ]
        
   <context:component-scan base-package="com.example.biz" />
</beans>

 

 

 

 

[ 코드 추가 될 예정 ]에 들어갈 코드

<bean class="com.example.biz.common.LogAdvice" id="la" />
<bean class="com.example.biz.common.PlusLogAdvice" id="pla" />

<aop:config>
     <aop:pointcut expression="execution(* com.example.biz..*Impl.*(..))" id="aPointcut" />
     <aop:pointcut expression="execution(* com.example.biz..*Impl.get*(..))" id="bPointcut" />
     <!-- 표현식
     expression = 표현식 | 표현식에 해당하는 애를 id=" "에 있는 걸로 부르겠다~ 라는 것
     함수명 : com.example.biz..*Impl.* / (..) : input / 맨 앞의 * : Output  
     com.example.biz 하위의 모든 
     위 : CRRUD : * = 모두 5개
     아래 : RR : get으로 시작하는 것만 2개
     -->
     <aop:aspect ref="어드바이스객체명">
        <aop:before method="그객체가갖는메서드명" pointcut-ref="포인트컷이름"/>
     </aop:aspect>
</aop:config>

🔼 추가된 코드

이제 로그를 찍는 걸 new 해줘야 한다
pointcut은 조인포인트들 중에 선택된 것

aop 설정
1) 포인트컷 설정
선택될 비즈니스 메서드를 정의
내가 가진 CRUD중에서 뭐를 공통로직과 엮을 건지 정의하는 것

포인트컷 표현식

 

2) 애스팩트 설정

맨 위에서 부터 5개가 동작시점이다

순서대로 총 5가지의 동작시점을 해석하면 이렇다

1. after : 핵심 관심사 호출 후

2. after-returning : 핵심 관심사 반환값이 있었고, 그걸 반환한 다음

3. after-throwing : 핵심 관심사에서 예외 발생하면

4. around : 핵심 관심사 전후

5. before : 핵심 관심사 전

 

 

 

 

<aop:aspect ref="어드바이스객체명">
	<aop:동작시점 method="그객체가갖는메서드명" pointcut-ref="결합할포인트컷명"/>
</aop:aspect>

아까 우리는 서비스 들어오자 마자 로그를 찍었다 == before

 

 

 

이렇게 applicationContext.xml 전체 코드

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
   
   <context:component-scan base-package="com.example.biz" />
   
   
   
   <bean class="com.example.biz.common.LogAdvice" id="la" />
   <bean class="com.example.biz.common.PlusLogAdvice" id="pla" />
   
   <aop:config>
         <aop:pointcut expression="execution(* com.example.biz..*Impl.*(..))" id="aPointcut" />
         <aop:pointcut expression="execution(* com.example.biz..*Impl.get*(..))" id="bPointcut" />
         <!-- 함수명 : com.example.biz..*Impl.* / (..) : input / 맨 앞의 * : Output  
         com.example.biz 하위의 모든 이하는 뜻
         위 : CRRUD : * = 모두 5개
         아래 : RR : get으로 시작하는 것만 2개
         -->
         <aop:aspect ref="la">
         	<aop:before method="printLog" pointcut-ref="aPointcut"/>
         </aop:aspect>
         <aop:aspect ref="pla">
         	<aop:before method="printLog" pointcut-ref="bPointcut"/>
         </aop:aspect>
   </aop:config>
</beans>

 

 

 

공통 관심사 호출해보기

package com.example.biz.board;

import java.util.Scanner;

import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class BoardClient {
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		AbstractApplicationContext factory = new GenericXmlApplicationContext("applicationContext.xml");

		BoardService boardService = (BoardService)factory.getBean("bs");

		while(true) {
			System.out.println("1. 전체출력");
			System.out.println("2. 1개출력");
			System.out.println("3. 글 작성");
			System.out.println("4. 글 삭제");
			System.out.println("0. 종료");
			System.out.print(">> ");
			int command = sc.nextInt();
			if(command == 0) {
				break;
			}
			else if(command == 1) {
				System.out.println(boardService.getBoardList(null));
			}
			else if(command == 2) {
				System.out.print("출력할 글 번호 입력 >> ");
				BoardDTO dto = new BoardDTO();
				dto.setBid(sc.nextInt());
				boardService.updateBoard(dto);
				System.out.println(boardService.getBoard(dto));
			}
			else if(command == 3) {
				BoardDTO dto = new BoardDTO();
				System.out.print("글 제목 입력 >> ");
				dto.setTitle(sc.next());
				System.out.print("작성자 입력 >> ");
				dto.setWriter(sc.next());
				System.out.print("글 내용 입력 >> ");
				dto.setContent(sc.next());
				boardService.insertBoard(dto);
				System.out.println("작성 완료");
			}
			else if(command == 4) {
				System.out.print("삭제할 글 번호 입력 >> ");
				BoardDTO dto = new BoardDTO();
				dto.setBid(sc.nextInt());
				boardService.deleteBoard(dto);
				System.out.println("삭제 완료");
			}
		}

		factory.close();
	}
}

이 코드를 실행시켜보면?

spring이 다 new 해주고 출력도 해주고 있다.. 

글 작성의 경우 위에서 설정한 get이 아니기 때문에 로그가 하나만 나올 것이다