본문 바로가기
Spring Boot

스프링 부트(Spring Boot) - AOP와 트랜잭션(Transaction)

by 도뎡 2023. 4. 10.
반응형

이전 글에서는 애플리케이션에 인터셉터를 적용해서 사용자가 요청한 기능의 URI를 로그로 출력하는 방법을 알아보았습니다.

이번 글에서는 스프링의 핵심 기능 중 하나인 AOP를 애플리케이션에 적용하고, 트랜잭션(Transaction)에 대해 공부해 볼 건데요. 이번 글은 두 가지 서적을 참고해서 포스팅했습니다.


구멍가게 코딩단코드로 배우는 스프링 웹 프로젝트
★ 흔한 개발자의 개발 노트를 운영하시는 흔한 개발자(?) 김인우 선생님의 스프링 부트 시작하기 ★


1. AOP(Aspect Oriented Programming)란?

AOP는 관점 지향 프로그래밍입니다. AOP는 자바와 같은 객체 지향 프로그래밍(OOP)을 더욱 OOP 답게 사용할 수 있도록 도와주는 역할을 합니다.

AOP는 여러 개의 핵심 비즈니스 로직 외에 공통으로 처리되어야 하는 로그 출력, 보안 처리, 예외 처리와 같은 코드를 별도로 분리해서 하나의 단위로 묶는 모듈화의 개념으로 생각할 수 있습니다.

AOP에서 관점은 핵심적인 관점과 부가적인 관점으로 나눌 수 있습니다. 핵심적인 관점은 핵심 비즈니스 로직을 의미하고, 부가적인 관점은 앞에서 말씀드린 공통으로 처리되어야 하는 코드를 의미합니다.

여러분의 쉬운 이해를 위해 이미지를 첨부하도록 하겠습니다.

객체 지향 프로그래밍 (OOP)

 

각각의 화살표는 하나의 기능을 구현하는 데 필요한 작업을 의미합니다. MVC 패턴의 특성상, 컨트롤러(Controller) ▶ 서비스(Service) ▶ 매퍼(Mapper) 순으로 작동합니다.

만약, 필수적으로 처리되어야 하는 로그, 보안, 트랜잭션, 예외 처리와 같은 부가적인 기능들이, 규모가 커다란 시스템에서 각각의 기능마다 추가된다면 코드가 얼마나 길어지게 될까요?

AOP는 이러한 문제를 관점이라는 개념을 통해 해결할 수 있습니다. 앞에서 말씀드린 부가적인 관점에서는 핵심 비즈니스 로직이 어떤 기능을 수행하는지에 대해 전혀 알 필요가 없습니다. 단지, 핵심 비즈니스 로직 안에서 필요한 시점에 부가적인 관점이 포함되기만 하면 되는 것입니다.

이미지를 하나 더 첨부하도록 하겠습니다.

관점 지향 프로그래밍 (AOP)

 

객체 지향 프로그래밍과 달리, 부가적인 관점이 핵심 비즈니스 로직의 바깥에 포함되어 있습니다. 즉 AOP를 적용하면 로그, 보안, 트랜잭션, 예외 처리와 같은 부가적인 기능들을 비즈니스 로직마다 일일이 추가하지 않아도 됩니다.

 

2. AOP 용어 알아보기

AOP를 제대로 이해하기 위해서는 몇 가지 용어를 기억해야 합니다.

용어 설명
관점(Aspect) 공통적으로 적용될 기능을 의미합니다. 부가적인 기능을 정의한 코드인 어드바이스와, 어드바이스를 어느 곳에 적용할지 결정하는 포인트컷의 조합으로 만들어집니다.
어드바이스(Advice) 실제로 부가적인 기능을 구현한 객체를 의미합니다.
조인 포인트(Join point) 어드바이스를 적용할 위치를 의미합니다. 예를 들어, PostService에서 CRUD를 처리하는 메서드 중 원하는 메서드를 골라서 어드바이스를 적용할 수 있습니다. 이때 PostService의 모든 메서드는 조인 포인트가 됩니다.
포인트컷(Pointcut) 어드바이스를 적용할 조인 포인트를 선별하는 과정이나 그 기능을 정의한 모듈을 의미합니다. 정규 표현식이나 AspectJ 문법을 이용해서 어떤 조인 포인트를 사용할지 결정합니다.
타겟(Target) 실제로 비즈니스 로직을 수행하는 객체를 의미합니다. 즉, 어드바이스를 적용할 대상입니다.
프록시(Proxy) 어드바이스가 적용되었을 때 생성되는 객체를 의미합니다.
인트로덕션(Introduction) 타겟에는 없는 새로운 메서드나 멤버(인스턴스) 변수를 추가하는 기능입니다.
위빙(Weaving) 허리를 중심으로 상체를 숙여서 상대의 훅을 피하는 복싱 기술입니다. 네, 죄송합니다.

포인트컷에 의해서 결정된 타겟의 조인 포인트에 어드바이스를 적용하는 것을 의미합니다.

 

3. LoggerAspect 클래스 생성하기

java 디렉터리에 LoggerAspect 클래스를 추가한 후 소스 코드를 작성해 주세요.

package com.study.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.thymeleaf.util.StringUtils;

@Slf4j
@Aspect
@Component
public class LoggerAspect {

    @Around("execution(* com.study.domain..*Controller.*(..)) || execution(* com.study.domain..*Service.*(..)) || execution(* com.study.domain..*Mapper.*(..))")
    public Object printLog(ProceedingJoinPoint joinPoint) throws Throwable {

        String name = joinPoint.getSignature().getDeclaringTypeName();
        String type =
                StringUtils.contains(name, "Controller") ? "Controller ===> " :
                StringUtils.contains(name, "Service") ? "Service ===> " :
                StringUtils.contains(name, "Mapper") ? "Mapper ===> " :
                "";

        log.debug(type + name + "." + joinPoint.getSignature().getName() + "()");
        return joinPoint.proceed();
    }

}

 

어노테이션 설명
@Component 스프링 컨테이너에 빈(Bean)으로 등록하기 위한 어노테이션입니다. @Bean은 개발자가 제어할 수 없는 외부 라이브러리를 빈(Bean)으로 등록할 때 사용하고, @Component는 개발자가 직접 정의한 클래스를 빈(Bean)으로 등록할 때 사용합니다.
@Aspect AOP 기능을 하는 클래스의 클래스 레벨에 선언하는 어노테이션입니다.
@Around 어드바이스(Advice)의 종류 중 한 가지로 어드바이스는 모두 다섯 가지의 타입이 있습니다. 다섯 가지 중 어라운드(Around)는 메서드의 호출 자체를 제어할 수 있기 때문에 어드바이스 중 가장 강력한 기능이라고 볼 수 있습니다.

 

다섯 개의 어드바이스(Advice)
타입 어노테이션 기능
Before Advice @Before Target 메서드 호출 이전에 적용할 어드바이스 정의
After Returning @AfterReturning Target 메서드가 성공적으로 실행되고, 결괏값을 반환한 뒤에 적용
After Throwing @AfterThrowing Target 메서드에서 예외 발생 이후에 적용 (try/catch의 catch와 유사)
After @After Target 메서드에서 예외 발생에 관계없이 적용 (try/catch의 finally와 유사)
Around @Around Target 메서드 호출 이전과 이후 모두 적용 (가장 광범위하게 사용됨)

 

@Around 안에서 execution으로 시작하는 구문은 포인트컷을 지정하는 문법으로, 다른 명시자로는 within과 bean이 있습니다. 세 개 중 가장 많이 사용되는 명시자는 execution으로, 접근 제어자, 리턴 타입, 타입 패턴, 메서드, 파라미터 타입, 예외 타입 등을 조합해서 정교한 포인트컷을 만들 수 있습니다.

모든 예시는 execution( )으로 감싸져 있다는 가정 하에 작성되었습니다.
예시 설명
PostResponse select*(..) 리턴 타입이 PostResponse 타입이고, 메서드의 이름이 select로 시작하며, 파라미터가 0개 이상인 모든 메서드가 호출될 때 (0개 이상은 패키지, 메서드, 파라미터 등 모든 것을 의미)
* com.study.controller.*() 해당 패키지 내의 파라미터가 없는 모든 메서드가 호출될 때
* com.study.controller.*(..) 해당 패키지 내의 파라미터가 0개 이상인 모든 메서드가 호출될 때
* com.study..select(*) com.study의 모든 하위 패키지에 존재하는 select로 시작하고, 파라미터가 한 개인 모든 메서드가 호출될 때
* com.study..select(*, *) com.study의 모든 하위 패키지에 존재하는 select로 시작하고, 파라미터가 두 개인 모든 메서드가 호출될 때

 

우리가 작성한 LoggerAspect 클래스의 포인트컷을 보면 "or" 표현식이 있습니다. 포인트컷 표현식은 and( && ) 또는 or( || )를 조합해서 사용할 수 있습니다. execution 포인트컷은 AOP에서 정말 중요한 개념이니, 꼭 기억해 주세요.
예시 설명
* com.study.domain..*Controller.*(..) com.study.domain의 모든 하위 패키지 중 xxxController와 같은 패턴의 이름을 가진 클래스에서 파라미터가 0개 이상인 메서드를 의미
* com.study.domain..*Service.*(..) com.study.domain의 모든 하위 패키지 중 xxxService와 같은 패턴의 이름을 가진 클래스에서 파라미터가 0개 이상인 메서드를 의미
* com.study.domain..*Mapper.*(..) com.study.domain의 모든 하위 패키지 중 xxxMapper와 같은 패턴의 이름을 가진 인터페이스에서 파라미터가 0개 이상인 메서드를 의미

 

 

ProceedingJoinPoint와 전체 로직

다음은 ProceedingJoinPoint 타입의 객체인 joinPoint입니다. joinPoint 객체 안의 signature 객체는 클래스와 메서드에 대한 정보를 담고 있는 객체로, 다음과 같은 구조를 가집니다.

 

(이미지를 클릭하시면 확대해 보실 수 있습니다.)

signature 객체 구조

 

전체 로직에 대해 짧게 말씀드리겠습니다. 18번 라인의 name에는 대상 파일의 경로(패키지 + 파일명)가 저장되며, 결과적으로 printLog( )는 signature 객체가 가진 정보를 이용해서, 어떤 클래스의 어떤 메서드가 호출되었는지를 로그로 출력하는 기능을 합니다.

  • 컨트롤러 : com.study.domain.post.PostController
  • 서비스 : com.study.domain.post.PostService
  • 매퍼 : com.study.domain.post.PostMapper

 

추가적으로 ProceedingJoinPoint 인터페이스는 JoinPoint 인터페이스를 상속받는데요. JoinPoint는 다음의 메서드들을 포함하고 있습니다.

메서드 설명
Object[ ] getArgs( ) 전달되는 모든 파라미터들을 Object 타입의 배열로 가지고 옵니다.
String getKind( ) 해당 어드바이스(Advice)의 타입을 가지고 옵니다.
Signature getSignature( ) 실행되는 대상 객체의 메서드에 대한 정보를 가지고 옵니다.
Object getTarget( ) 타겟(Target) 객체를 가지고 옵니다.
Object getThis( ) 어드바이스(Advice)를 행하는 객체를 가지고 옵니다.

 

4. AOP 로그 출력해 보기

게시글 리스트로 접근해 보면, IDE 콘솔에 LoggerAspect에 작성한 로직대로 로그가 출력됩니다. 사실, 인터셉터만으로도 백엔드 영역을 추적할 수 있긴 하지만, AOP 설정을 통해 어떤 클래스의 어떤 메서드가 실행되었는지 한눈에 확인할 수 있게 되었습니다.

printLog( ) 실행 결과

 

5. 트랜잭션(Transaction)이란?

트랜잭션은 쉽게 말해, 하나의 작업에 여러 개의 작업이 같이 묶여 있는 개념으로 생각할 수 있습니다. 대표적으로 은행 업무를 생각할 수 있는데요. A가 B에게 50,000원을 보내야 하는 상황으로 가정해 보겠습니다.

  • A가 B의 은행을 선택하고, 계좌번호와 이체할 금액을 입력한다.
  • A의 계좌에서 50,000원이 출금된다.
  • B의 계좌에 50,000원이 입금된다.
  • A ==> B의 계좌 이체가 완료된다.

 

그런데, A의 계좌에서 50,000원이 출금되었지만 B의 계좌에는 50,000원이 입금되지 않는다면? A와 B 모두 피해를 입은 상황이 됩니다. 당연한 이야기지만, A의 계좌 이체는 성공했고 B의 계좌에는 입금이 되지 않았다면, A의 계좌는 이체하기 전 상태로 돌아가야 합니다.

이와 같이 2번과 3번의 시나리오처럼 함께 묶여 있는 작업을 트랜잭션이라고 생각할 수 있습니다.

 

6. 트랜잭션(Transaction)의 기본 원칙

IT 계열을 전공했거나, 정보 처리 관련 자격증을 가지고 계신다면 ACID라는 키워드가 낯이 익으실 겁니다. ACID에서 트랜잭션의 성격을 가장 잘 표현한 것은 원자성(Atomicity)입니다.

원자성이란, 쉽게 말해 트랜잭션의 여러 가지 작업들 중 하나라도 실패 처리되었다면 앞에서 성공했던 작업들도 모두 원래 상태로 되돌아가야 함을 의미합니다.

특성 설명
원자성(Atomicitiy) 하나의 트랜잭션은 모두 하나의 작업 단위로 처리되어야 합니다. 트랜잭션이 A, B, C로 구성된다면 A, B, C의 처리 결과는 모두 동일해야 합니다. 하나라도 실패했을 경우에는 세 가지 모두 처음 상태로 되돌아가야만 합니다.
일관성(Consistency) 트랜잭션이 성공했다면 데이터베이스의 모든 데이터는 일관성을 유지해야 합니다.
고립성(Isolation) 트랜잭션은 독립적으로 처리되며, 처리되는 중간에 외부에서의 간섭은 없어야 합니다.
지속성(Durability) 트랜잭션은 독립적으로 처리되며, 그 결과는 지속적으로 유지되어야 합니다.

 

7. 트랜잭션(Transaction) 적용

코드 퀄리티 개선 작업 이전에는 애플리케이션 전역에서 DB Transactional이 작동하도록 설정했었는데요. PostService의 일부 메서드에 적용되어 있는 선언적 트랜잭션(@Transactional)이 퍼포먼스상 이득일 거라는 판단하에 전역 트랜잭션 설정은 스킵하고, 선언적 트랜잭션을 이용해서 남은 기능들을 구현해 볼까 합니다.

 

마치며

이번 글을 끝으로, 개발 단계에서의 퀄리티를 높여주는 필수 설정들은 마침표를 찍어도 될 것 같습니다. 앞으로는 다음의 순서대로 게시판의 필수 기능들을 구현합니다.

  • 페이징과 검색 처리
  • REST API 방식의 댓글 처리
  • 파일 업로드/다운로드 처리

 

정말 많이 부족한 글이지만, 응원해 주시고 기다려 주시는 방문자 여러분에게 보답하는 마음으로 더 열심히 활동하고, 더 좋은 내용으로 다음 글에서 뵙겠습니다.

오늘도 방문해 주셔서 정말 감사드립니다. 좋은 하루 보내세요 :)

 

Board.zip
0.67MB

반응형

댓글