고래씌

[Spring] 13. Aop 본문

Server/Spring

[Spring] 13. Aop

고래씌 2024. 1. 31. 16:26

1. Aop

 

proxy-target-class : 프록시 객체를 생성할 때 interface로 혹은, class로 생성할지를 결정하는 구문
    - 기본값 : true → 프록시객체 생성시 class로 생성
                   false → 다이나믹 프록시 인터페이스로 생성함

 

aop : 내가만든 공통 코드를 모듈로 관리하고자 할 때. 메소드단위로 작동시켜야함. 코드가 있을 때, 일반적으로 interceptor로 관리하지 못한 경우 사용한다.

 

aop : 주로 트랜잭션처리(@Transactional), 로깅처리, 로그인처리 등 비즈니스단에 공통적으로 필요한 메소드를 좀 더 세밀하게 조정할 때 사용한다.

interceptor : 디스패처서블릿이 컨트롤러로 호출하기 전/후로 끼어들어 실행하기 때문에, controller에게 전달하기 전에 request, response의 데이터를 처리하기에 좋다.

▶ 스프링 요청 / 응답 흐름

 

 

 

 

=> 메이븐 레파지토리에서 코드 복사

 

 

▶ pom.xml

- 다음과 같이 추가한 후, 수정

		<!-- Aop 의존성 -->
		<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
		<dependency>
		    <groupId>org.aspectj</groupId>
		    <artifactId>aspectjweaver</artifactId>
		    <version>${org.aspectj-version}</version>
		</dependency>
		
		<dependency>
		    <groupId>org.aspectj</groupId>
		    <artifactId>aspectjweaver</artifactId>
		    <version>${org.aspectj-version}</version>
		</dependency>

 

 

 

servelt.context.xml

 

=> aop 추가(namespaces)

 

 

 

 

 

▶ Aop.java

 

@Pointcut(advice가 끼어들어서 수행될 클래스나 메소드의 위치)
- joinpoint : advice가 적용될 수 있는 후보지들. 클라이언트가 호출하는 모든 비즈니스 메소드가 여기에 포함된다.

- pointcut : joinpoint들 중에서 실제로 advice가 실행될 지점

  Pointcut 작성방법

@Pointcut("execution([접근제한자] 반환형 풀클래스명.메소드명([매개변수]))")


 Pointcut 표현식

 * : 모든 | 아무값
.. : 하위 | 아래 (하위패키지) | 매개변수에 들어가면 0개 이상의 매개변수를 뜻함

 

 

package com.kh.spring.common.aop;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
@Aspect // 공통관심사가 작성된 클래스임을 명시
	    // 실제로 실행할 코드(advice), 실행시킬 위치(pointcut) 두개가 반드시 정의되어 있어야함
public class Aop {
	
	// com.kh.spring.board 아래에서 클래스들 중 Impl로 끝나는 클래스 내부에 존재하는 모든 메소드가 호출될때로 pointcut 지정
	// 모든 클래스들 중 클래스 가 Impl로 끝나는 0개의상의 메소드
	@Before("testPointcut()")  // 특정메소드가 실행되기 이전에 이코드가 중간에 끼어들어서 실행될 것임
	public void start() {
		
		// 특정 메소드가 실행되기 전에 항상 실행되는 advice
		log.info("================ service start ================");
	}
	
	@After("testPointcut()")
	public void end() {
		log.info("================ service end ================");
	}
	
	// @Pointcut : pointcut을 정의해두는 역할만 함. 어노테이션 안에 작성한 패턴을 저장하는 역할을 수행
	@Pointcut("execution(* com.kh.spring.board..*Impl.*(..))")
	public void testPointcut() {}
}

 

 

 

=> BoardServiceImpl.java 파일 확인한 결과, 끼어들어 실행되는 것을 확인할 수 있다!

 

 

 


2. 게시판 서비스용 / 모든 서비스용 포인트 컷

 

▶ Aop.java에서 작성하였던 @Before, @After 어노테이션 주석처리

 

 

 

 

 

=> BeforeTest.java, CommonPointcut.java, AfterTest모두 class 파일 생성해줌

 

 

 

▶ CommonPointcut.java

package com.kh.spring.common.aop;

import org.aspectj.lang.annotation.Pointcut;

// pointcut들을 모아둔 클래스
public class CommonPointcut {
	
	// 게시판 서비스용 포인트컷
	@Pointcut("execution(* com.kh.spring.board..*Impl.*(..))")
	public void boardPointcut() {
		
	}
	
	// 모든 서비스용 포인트컷
	@Pointcut("execution(* com.kh.spring..*Impl.*(..))")
	public void implPointcut() {
		
	}
}

 

 

 

▶ BeforeTest.java

package com.kh.spring.common.aop;

import java.util.Arrays;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Component
@Aspect
@Slf4j
public class BeforeTest {
	
	@Before("CommonPointcut.implPointcut()")
	public void beforeService(JoinPoint jp) {
		StringBuilder sb = new StringBuilder();
		sb.append("==============================================\n");
		
		// JoinPoint : advice가 적용될 수 있는 모든 후보 메소드
		
		// JoinPoint : advice가 실제로 적용되는 Target Object에 접근할 수 있는 메소드를 제공하는 인터페이스
		// TargetObject의 클래스정보, 전달되는 매개변수, 메소드명, 반환값, 예외정보 등을 얻어올 수 있다.
		// 모든 advice메소드의 첫번째 매개변수는 무조건 JoinPoint로 작성해야함
		sb.append("start : " + jp.getTarget().getClass().getSimpleName()+" - ");
		sb.append(jp.getSignature().getName()); // 메소드명
		sb.append("(" + Arrays.toString(jp.getArgs()) + ")");
		
		log.info(sb.toString());
	}

}

 

 

 

▶ AfterTest.java

package com.kh.spring.common.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Component
@Aspect
@Slf4j
@Order(1) // after관련된 어노테이션들 중 첫번째로 실행하도록 설정
public class AfterTest {
	
	@After("CommonPointcut.implPointcut()")
	public void serviceEnd(JoinPoint jp) {
		
		log.info("\n=============== service End ==================");
	}
}

 

 

 

 

=> 반환받은 이후(메소드 리턴문 이후)

 

 

▶ AfterReturningTest.java

 

- @AfterReturning : 메소드 실행 이후에 "반환값"을 얻어오는 기능의 어노테이션

- returning : 반환값을 저장할 "매개변수명"을 지정하는 속성 

 

package com.kh.spring.common.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import com.kh.spring.member.model.vo.Member;

import lombok.extern.slf4j.Slf4j;

@Component
@Aspect
@Slf4j
@Order(5)
public class AfterReturningTest {
	
	// return되는 변수명지정(returnObj) 반환되는 값이 여기에 담길 것
	@AfterReturning(pointcut="CommonPointcut.implPointcut()", returning="returnObj") 
	public void returnValue(JoinPoint jp, Object returnObj) {   // 모든 메소드에는 반드시 JoinPoint가 있어야한다
		
		if(returnObj instanceof Member) {
			Member m = (Member) returnObj;
			m.setUserName("하고래");   // 이런식으로 값의 조정이 가능하다! 모든 사용자의 이름이 "하고래"로 바뀔것
		}
		log.info("return value : {}", returnObj);
	}

}

 

 


3. 게시글 수정 막기

 

 

▶ AfterThrowingTest.java

 - @AfterThrowing : 메소드 실행 이후에 발생하는 예외를 얻어오는 어노테이션

-  throwing : 반환된 예외값을 저장할 매개변수명 지정하는 속성

package com.kh.spring.common.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Component
@Aspect
@Slf4j
public class AfterThrowingTest {
	
	// @AfterThrowing : 메소드 실행 이후에 발생하는 예외를 얻어오는 어노테이션
	//       throwing : 반환된 예외값을 저장할 매개변수명 지정하는 속성
	
	@AfterThrowing(pointcut="CommonPointcut.implPointcut()", throwing="exceptionObj")
	public void serviceReturnException(JoinPoint jp, Exception exceptionObj) {
		StringBuilder sb = new StringBuilder("Exception : " + exceptionObj.getStackTrace()[0]);
		sb.append("에러 메세지 : " + exceptionObj.getMessage()+"\n");
		
		log.error(sb.toString());
		
	}
	
}

 

 

 

 

▶ BoardService.java

//	게시판 수정
	int updateBoard(Board board, String deleteList, MultipartFile upfile, List<MultipartFile> upfiles) throws Exception;

 

 

 

▶ BoardServiceImpl.java

 

throw new Exception("dd");  추가

//	게시판 수정
	@Transactional(rollbackFor = {Exception.class})  // 어떠한 에외가 발생한다면 롤백처리하겠다! 트랜잭션 처리를 함
	@Override
	public int updateBoard(Board board, String deleteList, MultipartFile upfile, List<MultipartFile> upfiles) throws Exception{
		
		
		
		
		
		board.setBoardContent(Utils.XSSHandling(board.getBoardContent()));
		board.setBoardContent(Utils.newLineHandling(board.getBoardContent()));
		board.setBoardTitle(Utils.XSSHandling(board.getBoardTitle()));
		
		// 게시글 업데이트
		int result = boardDao.updateBoard(board);
		
		// 이미지 및 첨부파일 등록
		String webPath = "/resources/images/board/" + board.getBoardCd() + "/";
		String serverFolderPath = application.getRealPath(webPath);
		
		// 게시글 등록이 성공적으로 완료되었을 때
		if(result > 0) {
			
			// 업로드된 이미지를 분류작업
			List<BoardImg> imgList = new ArrayList();
			
			if(upfiles != null) {  // 일반게시판, 자유게시판에서는 upfiles가 null임
				for(int i=0; i<upfiles.size(); i++) {
					if(!upfiles.get(i).isEmpty()) {
						
						String changeName = Utils.saveFile(upfiles.get(i), serverFolderPath);
						
						// BoardImg객체 생성후, 필요한 값을 추가해서 imgList에 추가
						BoardImg bi = new BoardImg();
						bi.setChangeName(changeName);
						bi.setOriginName(upfiles.get(i).getOriginalFilename());
						bi.setRefBno(board.getBoardNo());
						bi.setImgLevel(i);
						
						imgList.add(bi);
					}
				}
			}
			
			// x버튼을 눌러서 이미지를 삭제하고자 하는 경우
			if(deleteList != null && !deleteList.equals("")) {
				
				// 삭제하기 위해서는 board_img_no가 필요함
				result = boardDao.deleteBoardImg(deleteList);
			}
			
			// db에서 삭제처리 완료됐거나 혹은 게시판 업데이트 성공시
			if(result > 0) {
				// BoardImg객체 하나하나 업데이트
				
				for(BoardImg bi : imgList) {
					result = boardDao.updateBoardImg(bi);
					
					// result값은 1혹은 0으로 반환
					// result == 0 ? 실패 → 기존에 이미지가 없던 경우
					// result == 1 ? 성공 → 기존에 이미지가 있던 경우
					
					if(result == 0) {   // 기존에 이미지가 없으면 insert문 호출
						result = boardDao.insertBoardImg(bi); 
						// 기존에 있던 boardMapper 이용. changeName, OriginName, refBno, imgLevel 값이 다 있어서 문제없음
					}
				}
			}
		}
		
		throw new Exception("dd");
		
		
		// return result;
	}

 

 

▶ BoardController.java

 

 

=> try/catch문 추가

 


 

 

 

▶ AroundTest.java

 

- @Around : before, after 합쳐놓은 어노테이션

- ProceedingJoinPoint : before/after처리 관련된 기능을 제공.

- 값을 얻어올수 있는 메소드도 함께 제공

 

package com.kh.spring.common.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Component
@Aspect
@Order(4)
@Slf4j
public class AroundTest {
	
	// @Around : before, after 합쳐놓은 어노테이션
	
	@Around("CommonPointcut.implPointcut()")
	public Object checkRunningTiem(ProceedingJoinPoint jp) throws Throwable{
		
		// ProceedingJoinPoint : before/after처리 관련된 기능을 제공
		// 값을 얻어올수 있는 메소드도 함께 제공
		
		// jp.proceed() 메소드 호출전 : before의 역할수행
		long start = System.currentTimeMillis();
		
		Object obj = jp.proceed(); // 메소드 실행. 결과값 반환
		
		// jp.proceed()호출 이후 : after의 역할 수행
		long end = System.currentTimeMillis();
		
		log.info("Running Time : {} ms", (end-start));
		
		return obj;
	}
}