WebSecurityConfigureAdapter(Deprecated) / Spring AOP / @Transactional의 정체(항해일지 24일차)

김형준·2022년 6월 1일
0

TIL&WIL

목록 보기
24/45
post-thumbnail

1. 학습일지

1) ❗❗❗WebSecurityConfigureAdapter(Deprecated)

  • 2022년 2월 21일 부터 WebSecurityConfigureAdapter는 Deprecated 처리가 되었다.
  • 그 이유에 대한 키워드는 Lambda DSL, IO, Webflux 등이었는데 아직은 학습하기 어려운 부분이었다. 따라서 추후에 공부할 키워드로 !
  • WebSecurityConfig에서는 SecurityFilterChain을 @Bean으로 등록하여 lambda를 사용하며 .and()를 제거했다.
  • 이 부분은 어떠한 장점을 지니기에, WebSecurityConfigureAdapter를 권장하지 않는다는 공식 발표를 했는 지 꼭 공부해보자!

2) TOP 5 회원 선정하기

서버 사용시간 TOP 5 회원을 선정하기.

  • API 사용 시간을 측정한다.
    • Scratch 파일 생성하여 테스트해보기.
    • 상위 단의 File-new-scratch file-java 선택
    • 간단하게 sumFromOneTo()를 정의하여 시작 시점과 끝 시점 사이에서 돌리고 between 값을 출력한다.
class Scratch {
    public static void main(String[] args) {
        // 측정 시작 시간
        long startTime = System.currentTimeMillis();

        // 함수 수행
        long output = sumFromOneTo(1_000_000_000);

        // 측정 종료 시간
        long endTime = System.currentTimeMillis();

        long runTime = endTime - startTime;
        System.out.println("소요시간: " + runTime);
    }

    private static long sumFromOneTo(long input) {
        long output = 0;

        for (int i = 1; i < input; ++i) {
            output = output + i;
        }

        return output;
    }
}

  • 상품 등록 API에 위의 방식과 같이 시작, 끝 시점을 정해주고 between 시간을 구해 저장한다.
    • try, finally를 사용하여 return 후에 finally 구문이 실행되도록 해준다.
    • 이 때 누적합이 되도록 조건문을 추가해준다.
    // 신규 상품 등록
    @PostMapping("/api/products")
    public Product createProduct(@RequestBody ProductRequestDto requestDto,
                                 @AuthenticationPrincipal UserDetailsImpl userDetails){
        // 측정 시작 시간
        long startTime = System.currentTimeMillis();

        try {
            // 로그인 되어 있는 회원의 Id
            Long userId = userDetails.getUser().getId();
            Product product = productService.createProduct(requestDto, userId);

            // 응답 보내기
            return product;
        } finally {
            // 측정 종료 시간
            long endTime = System.currentTimeMillis();

            long runTime = endTime - startTime;
            System.out.println("소요시간: " + runTime);

            Users user = userDetails.getUser();
            ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(user).orElse(null);
            if(apiUseTime == null){
                apiUseTime = new ApiUseTime(user, runTime);
            } else{
                apiUseTime.addUseTime(runTime);
            }
            System.out.println("[API User Time Username: " + user.getUsername() + ", Total Time: " + apiUseTime.getTotalTime());
            apiUseTimeRepository.save(apiUseTime);
        }
    }
  • 관리자 권한으로 이를 조회할 수 있도록 한다.
    • 이 때 ApiUseTime을 그대로 response에 담는다면, 포함된 User의 정보가 모두 전달된다.
    • 따라서 responseDto를 따로 생성하여 전달할 정보만을 담아 보내도록 한다.

2) AOP 란?

  • 위와 같이 TOP5 회원 선정 기능이 추가된 것을 회원들이 알아야 할까?

핵심기능

  • 각 API 별 수행해야 할 비즈니스 로직
    • ex) 상품 키워드 검색, 관심 상품 등록, 회원가입, 관심상품 폴더에 추가 ...

부가기능

  • 핵심 기능을 보조하는 기능
    • ex) 회원 패턴 분석을 위한 로그 기록, API 수행 시간 저장...

만약 위에서 했던 것과 같이 모든 핵심 기능에 부가 기능을 덧붙인다면?...

  • 핵심 기능이 많다면?..
  • 핵심 기능이 나중에 추가된다면?..
    • 항상 부가기능을 추가해줘야 한다.. 만약 깜빡한다면 부가기능의 신뢰성을 잃게된다.
  • 핵심 기능 수정 시 부가 기능이 섞여있다면 두 부분을 온전히 분리하기 위해 모든 코드를 이해해야한다.
  • 부가 기능의 변경 및 삭제가 필요하다면 모든 핵심기능 마다 변경사항을 적용해야한다. (유지보수성 최악)

부가 기능을 모듈화 하자!

  • AOP(Aspect Oriented Programming)를 통해 부가기능을 모듈화한다.
    • 부가기능은 핵심기능과는 관점(Aspect), 관심이 다르다.
    • 따라서 부가기능을 핵심기능과 분리하여 부가기능 중심으로 설계 및 구현이 가능하다.

Spring이 제공하는 AOP

  • 어드바이스 = 부가기능
  • 포인트 = 부가기능 적용 위치

  • 프록시 객체가 생성되고 이는 곧 부가기능의 역할을 수행한다.

  • AOP 후 과정
    • DispatcherServlet이 요청을 받아서 Controller로 보내는 것은 동일하다
    • 그러나 Controller로 가기 전에 AOP Proxy가 이 요청을 가로챈다.
    • (정확히는 핵심 기능이 DI 될 때 프록시 객체를 중간에 삽입한다.)
    • 가로 챈 후 @Around가 붙은 메서드가 실행 된다.
    • 메서드 내 ProceedingJoinPoint.proceed()가 실행될 때 Controller로 해당 요청을 보내준다.
    • 이 후 컨트롤러가 Response를 보내면 다시 AOP Proxy가 이를 받아와 ProceedingJoinPoint.proceed() 이후의 기능을 수행한다.
    • proceed()에 의해 원래 호출하려고 했던 함수, 인자가 전달된다.
    • (이 때 AOP Proxy는 Object output = joinPoint.proceed();와 같이 Java의 모든 객체를 받을 수 있는 Object 타입으로 결과를 받아오고 이를 리턴한다.)
    • 메서드가 종료되면 이를 다시 Dispatcher Servlet으로 보낸다.

+ 스프링 AOP 어노테이션

  • @Aspect: 스프링 빈 (Bean) 클래스에만 적용 가능하며, 해당 클래스를 AOP로 사용하겠다는 뜻이다.
  • 어드바이스(Advice) 종류
    • @Around: 핵심기능 수행 전과 후 모두 (@Before + @After)
    • @Before: 핵심기능 호출 전에만 (ex. Client 의 입력값 Validation 수행)
    • @After: '핵심기능' 수행 성공/실패 여부와 상관없이 언제나 동작 (try, catch 의 finally() 처럼 동작)
    • @AfterReturning: '핵심기능' 호출 성공 시에만 (함수의 Return 값 사용 가능)
    • @AfterThrowing: '핵심기능' 호출 실패 시에만. 즉, 예외 (Exception) 가 발생한 경우만 동작 (ex. 예외가 발생했을 때 개발자에게 email 이나 SMS 보냄)
  • 포인트 컷(PointCut)
    • 포인트컷 Expression Language
//음영처리 부분은 필수값임
execution(modifiers-pattern? `return-type-pattern` declaring-type-pattern? `method-name-pattern(param-pattern)` throws-pattern?)

//예제
@Around("execution(public * com.spring.springcore.controller..*(..))")
  • modifiers-pattern(접근제어자): public, private, *.....
  • return-type-pattern(반환 타입): void, String, List<//String>, *....
  • declaring-type-pattern(패키지 포함한 클래스명): com.sparta.springcore.controller.*(..) -> .별은 해당 패키지 모두, ..은 하위패키지까지 모두
  • method-name-pattern(param-pattern) (함수명(파라미터 패턴)):
    • 함수명: 그대로 쓰거나, xxx*과 같이 해당 문자열을 포함한 모든 함수를 지정할 수도 있음 혹은 ..으로 모든 함수에 사용 가능
    • 파라미터 패턴: 패키지 포함하여 인수 명 그대로 쓰거나 / () 인수없음 / (*) 타입 관계없이 1개 / (..) 타입 관계없이 0~N개
  • @PointCut: 메서드 위에 정의하여 범위를 지정하고 재사용, 결합사용 가능하다.
@Component
@Aspect
public class Aspect {
	@Pointcut("execution(* com.sparta.springcore.controller.*.*(..))")
	private void forAllController() {}

	@Pointcut("execution(String com.sparta.springcore.controller.*.*())")
	private void forAllViewController() {}

	@Around("forAllContorller() && !forAllViewController")
	public void saveRestApiLog() {
		...
	}

	@Around("forAllContorller()")
	public void saveAllApiLog() {
		...
	}	
}

Controller - Service - Repository 3계층에 맞춰 구현을 해야하는 다른 이유

  • Controller에 비즈니스 로직을 추가한다면 AOP가 있을 경우 훨씬 수월하다. (부가기능이 분리되어있기 때문)
  • Service영역에 AOP를 적용시켰는데, Controller에서 Repository를 바로 호출한다면? 부가기능이 적용되지 않는 메서드가 생긴다.

3) 트랜잭션 (@Transactional)

트랜잭션이란?

  • 데이터베이스에서 데이터에 대한 하나의 논리적 실행단계
  • ACID (원자성, 일관성, 고립성, 지속성)는 데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질을 가리키는 약어
  • 쉽게 말해, DB 관련 최소 작업 단위라고 생각하면 된다.
  • @Transactional이 붙은 메서드에서
    • 모두 정상적인 값을 배출하면 Commit!
    • 하나라도 오류를 뱉어내면 Rollback! -> 이 때 이미 DB에 변동 사항이 생겼더라도 모두 롤백 처리된다.
    • 아래 코드는 트랜잭션 어노테이션의 실제 구현 코드이다.
		public List<Folder> addFolders(List<String> folderNames, User user) {
				// 트랜잭션의 시작
        TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
				try {
	        // 1) 입력으로 들어온 폴더 이름을 기준으로, 회원이 이미 생성한 폴더들을 조회합니다.
	        List<Folder> existFolderList = folderRepository.findAllByUserAndNameIn(user, folderNames);
	
	        List<Folder> savedFolderList = new ArrayList<>();
	        for (String folderName : folderNames) {
	            // 2) 이미 생성한 폴더가 아닌 경우만 폴더 생성
	            if (isExistFolderName(folderName, existFolderList)) {
	                // Exception 발생!
	                throw new IllegalArgumentException("중복된 폴더명을 제거해 주세요! 폴더명: " + folderName);
	            } else {
	                Folder folder = new Folder(folderName, user);
	                // 폴더명 저장
	                folder = folderRepository.save(folder);
	                savedFolderList.add(folder);
	            }
	        }

					// 트랜잭션 commit
          transactionManager.commit(status);

	        return savedFolderList;
        } catch (Exception ex) {
            // 트랜잭션 rollback
            transactionManager.rollback(status);
            throw ex;
        }
    }

  • 어디서 많이 본 것 같은데?...(바로 위에서 배움ㅋ)

➕ 사실 트랜잭션은 AOP이다. 😲

  • 위에 코드를 보면 트랜잭션의 시작 + Commit / Rollback (try-catch) 로 구현되는데,
  • 트랜잭션이 시작되고, 해당 메서드 내부 기능이 수행되고, 결과에 따라 try-catch로 Commit과 Rollback이 결정되는 것이다.

2. 코멘트

  • 오늘은 3주차 과제를 마무리했다.
    • WebSecurityConfigureAdapter가 Deprecated되어 권장사항으로 코드 변경
    • 회원가입 validation check 부분 테스트 프레임워크 및 모키토 (Mock Object 생성 자동화) 를 사용하여 구현했다.
profile
BackEnd Developer

0개의 댓글