728x90
반응형
SMALL
SMALL

 

AWS를 공부하기 전 클라우드 서비스에 대해 먼저 이해해보자.

클라우드 서비스란?

인터넷을 통해 IT 리소스와 애플리케이션을 사용자가 원할 때 언제든지 사용한 만큼 (On-demand) 요금을 내는 서비스

 

클라우드 컴퓨팅의 유형은 크게 보면 2가지가 있다.

  • 퍼블릭(Public) 클라우드
    • 사용자가 컴퓨팅 리소스를 소유하지 않는 방식
    • 인터넷을 통해 제공
    • 가상화 기술로 만든 서비스를 그대로 사용
  • 프라이빗(Private) 클라우드
    • 특정 조직내에서 컴퓨팅 리소스를 '소유'
    • 사설(Private) 네트워크를 통해 제공
    • 가상 컴퓨팅 기술을 직접 구축

이 두가지 경우로부터 파생되는 경우가 있는데 이는 '하이브리드 클라우드'와 '멀티 클라우드'이다. 하이브리드 클라우드는 퍼블릭 클라우드와 프라이빗 클라우드 또는 데이터센터 간 네트워크를 연결해서 사용하는 방식이다. 데이터 및 애플리케이션을 각 클라우드가 공유한다. 멀티 클라우드는 다수의 퍼블릭 클라우드를 합쳐 사용하는 방식으로 예를 들면 AWS + GCP + Azure를 합친 클라우드 방식을 말한다. 내가 공부할 AWS는 대표적인 퍼블릭 클라우드이다.

 

 

클라우드 서비스의 특징

  • 탄력성/민첩성: 리소스에 대해 필요할 때 언제든 늘리고, 줄일 수 있고 이를 클릭 몇번으로 가능하게 한다.
  • 확장성: 물리 서버를 확장하려면 시간이 오래 걸리며 고정 비용이 발생하는 반면 클라우드는 즉시 확장이 가능하고 역시 사용한 만큼의 비용이 발생한다. 이러한 특징 때문에 서비스에 사용자가 많아져 급증하는 서비스 트래픽에 빠르게 대비가 가능하다.
  • 사용한 만큼만 비용을 지불: 전기 요금처럼 사용한 만큼 과금되며, 비용 예측이 가능하다.
  • 내결함성 및 재해복구: 클라우드 백업 및 클라우드 DR 구성으로 데이터 손상 등 긴급 상황에 대처가 가능하다.
  • 고 가용성: 손쉬운 다중 가용영역 설정에 따라 고 가용성을 보장
  • 유지 관리 간소화: 물리적인 리소스를 유지할 필요가 없고, 부분적으로 클라우드 CSP(Cloud Service Provider) 벤더에 위임한다.

 

사용한 만큼만 비용을 지불하기 때문에 무리한 자본지출(CAPEX)없이 빠른 시도와 회수가 가능해진다. 좀 더 자세히 말하자면 클라우드 서비스를 이용하지 않고 초장부터 서비스의 흥행을 기대해 무리한 물리 서버 구축으로 인해 회수 불가능한 자본지출이 투자됐는데 서비스가 생각보다 성과를 이루지 못하는 경우 회수할 수 있는 비용이 없어지는 반면 클라우드 서비스를 이용해서 사용자가 늘어나면 확장하며 사용한 만큼만 지불하게 하면 운영 지출(OPEX)만으로 서비스 운영이 가능해진다. 

 

클라우드 서비스 모델(IaaS/PaaS/SaaS)

클라우드 서비스 모델은 크게 3가지가 있다. 

 

  • IaaS (Infrastructure as a Service): 서비스로 제공되는 인프라 스트럭쳐. 즉, 개발사에게 제공하는 물리적 자원을 가상화한다.
  • PaaS (Platform as a Service): 서비스로 제공되는 플랫폼. 즉, 개발사에게 제공하는 플랫폼을 가상화한다.
  • SaaS (Software as a Service): 서비스로 제공되는 소프트웨어. 고객에게 제공되는 서비스를 가상화한다.

즉, 이 세 가지의 차이는 어디까지 가상화로 제공해 주는가에 있다. 이를 그림으로 좀 더 쉽게 이해해보자.

 

IaaS는 개발사에게 인프라(물리적 자원)을 가상화하여 제공한다고 했다. 그렇기 때문에 가상의 서버, 스토리지, 네트워크를 제공자가 관리하는 영역으로 구분한다.

PaaS는 개발사에게 플랫폼을 가상화하여 제공한다고 했다. 그렇기 때문에 가상의 서버, 스토리지, 네트워크를 포함하며 OS와 개발 환경까지 제공자가 관리하는 영역으로 구분한다. 따라서 개발사는 그 위에 본인의 애플리케이션과 데이터를 올려두면 된다.

SaaS는 고객에게 제공하는 서비스를 가상화한다고 했다. 그렇기 때문에 서비스가 돌아가기 위해 필요한 모든 것들(서비스 포함) 제공자가 관리한다. 

그에 반면 클라우드를 사용하지 않는 경우 모든 것을 사용자가 관리해야 하는 On-Premise가 있다.

 

기존 방식인 On-Premise와 클라우드의 차이점은 위 그림으로만 봐도 눈에 띄게 알 수 있지만 비교를 좀 더 깊게 해보자.

항목 On-Premise Cloud
인프라 운영/보안 사용자가 모두 운영하고 관리 공동 책임 모델이 적용
구축 및 배포 자원 구축/배포 시간이 길다 단 시간에 인프라 구성이 가능
탄력성/확장성 서버 증설 시 예산 및 시간이 소요 몇번의 클릭으로 서버 증설 가능
비용지출 방식 자본 지출: Capital Expense(CAPEX) 운영 지출: Operation Expense(OPEX)
네트워크 트래픽 인터넷 공급자(ISP) 회선 계약 따라 회선속도 및 트래픽 용량을 사전에 설정 회선 속도나 용량을 정할 수 없음 트래픽을 사용한만큼 지출 (Outbound)
오픈소스 모든 오픈소스 Application을 스스로 구축 Pre-built된 오픈소스 Application을 즉시 사용

 

(이 외 유사한 명칭인 FaaS(Function as a Service), BaaS(Backend as a Service)가 있다.)

728x90
반응형
LIST
728x90
반응형
SMALL

이제 새로운 마이크로 서비스인 CatalogService를 만들자. 이 서비스는 상품에 대한 데이터를 관리하는 서비스이다.

상품명, 상품아이디, 수량, 단일가격 등에 대한 정보를 가지고 있는 엔티티를 가지고 있을 것이고 그 엔티티에 대한 관리가 일어나는 서비스이다.

SMALL

 

우선, 스프링 프로젝트를 만들어야 하는데 Spring Initializer를 사용하는 방법은 기존 포스팅에 작성해 두었으니 dependencies부터 시작하자.

 

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.1'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'springmsa'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '21'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

ext {
    set('springCloudVersion', "2023.0.0")
}

dependencies {
    implementation 'org.modelmapper:modelmapper:3.1.1'

    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

tasks.named('test') {
    useJUnitPlatform()
}

 

필요한 dependencies는 다음과 같다.

  • Lombok
  • Spring Data JPA
  • Spring Web
  • Eureka Client
  • Spring Boot Devtools
  • H2
  • ModelMapper

application.yml

server:
  port: 0

spring:
  application:
    name: catalog-service
  jpa:
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        format_sql: true
        show_sql: true
        default_batch_fetch_size: 500
    defer-datasource-initialization: true
  sql:
    init:
      mode: always

  datasource:
    url: jdbc:h2:tcp://localhost/~/h2/msacatalog
    driver-class-name: org.h2.Driver
    username: sa
    password:

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true # Eureka Server로부터 Eureka Server에 등록된 다른 인스턴스의 정보를 주기적으로 갱신하는 옵션
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    instance-id: ${spring.cloud.client.hostname}:${spring.application.instance_id:${random.value}}

 

여기서는 서버를 띄울 때 데이터베이스에 초기데이터를 만들어주기 위해 spring.jpa.defer-datasource-initialization: true 속성과 spring.sql.init.mode: always 속성을 추가했다. 이는 resources 폴더에 data.sql 파일이 있을 때 해당 파일을 보고 초기데이터를 넣어주기 위함이다.

 

data.sql (resources)

insert into catalog(product_id, product_name, stock, unit_price) values ('CATALOG-001', 'Berlin', 100, 1500);
insert into catalog(product_id, product_name, stock, unit_price) values ('CATALOG-002', 'Tokyo', 200, 1000);
insert into catalog(product_id, product_name, stock, unit_price) values ('CATALOG-003', 'Stockholm', 300, 2500);

 

 

 

Entity

BaseEntity

package springmsa.springmsacatalogservice.entity;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
}

 

Catalog

package springmsa.springmsacatalogservice.entity;

import jakarta.persistence.*;
import lombok.Data;


@Data
@Entity
public class Catalog extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 120, unique = true)
    private String productId;

    @Column(nullable = false)
    private String productName;

    @Column(nullable = false)
    private Integer stock;

    @Column(nullable = false)
    private Integer unitPrice;
}

 

Repository

 

CatalogRepository

package springmsa.springmsacatalogservice.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import springmsa.springmsacatalogservice.entity.Catalog;

@Repository
public interface CatalogRepository extends JpaRepository<Catalog, Long> {
    Catalog findByProductId(String productId);
}

 

 

Service

CatalogService

package springmsa.springmsacatalogservice.service;

import springmsa.springmsacatalogservice.dto.CreateCatalogDto;
import springmsa.springmsacatalogservice.entity.Catalog;

public interface CatalogService {
    Iterable<Catalog> getAllCatalogs();

    Catalog createCatalog(CreateCatalogDto catalogDto);
}

 

인터페이스는 두 개의 메서드가 있다. 전체 조회와 생성.

 

CatalogServiceImpl

package springmsa.springmsacatalogservice.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
import springmsa.springmsacatalogservice.dto.CreateCatalogDto;
import springmsa.springmsacatalogservice.entity.Catalog;
import springmsa.springmsacatalogservice.repository.CatalogRepository;

@Slf4j
@RequiredArgsConstructor
@Service
public class CatalogServiceImpl implements CatalogService {

    private final CatalogRepository catalogRepository;
    private final ModelMapper modelMapper;

    @Override
    public Iterable<Catalog> getAllCatalogs() {
        return catalogRepository.findAll();
    }

    @Override
    public Catalog createCatalog(CreateCatalogDto catalogDto) {
        Catalog catalog = modelMapper.map(catalogDto, Catalog.class);
        catalogRepository.save(catalog);

        return catalog;
    }
}

 

 

DTO

CatalogDto

package springmsa.springmsacatalogservice.dto;

import lombok.Data;

@Data
public class CatalogDto {
    private String productId;
    private Integer quantity;
    private Integer unitPrice;
    private Integer totalPrice;
    private String orderId;
    private String userId;
}

 

CreateCatalogDto

package springmsa.springmsacatalogservice.dto;

import lombok.Data;

@Data
public class CreateCatalogDto {
    private String productId;
    private String productName;
    private Integer stock;
    private Integer unitPrice;
}

 

ResponseCatalogDto

package springmsa.springmsacatalogservice.dto;

import lombok.Data;

import java.time.LocalDateTime;

@Data
public class ResponseCatalogDto {
    private String productId;
    private String productName;
    private Integer unitPrice;
    private Integer stock;
    private LocalDateTime createdDate;
}

 

ApiResponseDto

package springmsa.springmsacatalogservice.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class ApiResponseDto<T> {
    private T data;
    private String errorMessage;
}

 

 

Controller

CatalogController

package springmsa.springmsacatalogservice.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import springmsa.springmsacatalogservice.dto.ApiResponseDto;
import springmsa.springmsacatalogservice.dto.CreateCatalogDto;
import springmsa.springmsacatalogservice.dto.ResponseCatalogDto;
import springmsa.springmsacatalogservice.entity.Catalog;
import springmsa.springmsacatalogservice.service.CatalogService;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@RestController
@RequestMapping("/catalogs")
@RequiredArgsConstructor
public class CatalogController {

    private final CatalogService catalogService;
    private final ModelMapper modelMapper;

    @GetMapping("")
    public ResponseEntity<ApiResponseDto<List<ResponseCatalogDto>>> getCatalogs() {
        Iterable<Catalog> allCatalogs = catalogService.getAllCatalogs();

        List<ResponseCatalogDto> result = new ArrayList<>();

        allCatalogs.forEach(catalog -> {
            ResponseCatalogDto catalogDto = modelMapper.map(catalog, ResponseCatalogDto.class);
            result.add(catalogDto);
        });

        return ResponseEntity.status(HttpStatus.OK).body(new ApiResponseDto<>(result, null));
    }

    @PostMapping("")
    public ResponseEntity<ApiResponseDto<ResponseCatalogDto>> createCatalog(@RequestBody CreateCatalogDto catalogDto) {
        Catalog catalog = catalogService.createCatalog(catalogDto);

        ResponseCatalogDto responseCatalogDto = modelMapper.map(catalog, ResponseCatalogDto.class);

        return ResponseEntity
                .status(HttpStatus.CREATED)
                .body(new ApiResponseDto<>(responseCatalogDto, null));
    }
}

 

 

 

마무리

User Service와 구조나, 코드 내용이 상이한게 거의 없기 때문에 코드만 써도 괜찮아 보인다. 전체적인 핵심은 이렇게 도메인 별 MicroService를 만들어서 서비스의 크기를 작게 나누고 API Gateway를 통해 서비스로 접근하는 방식을 고수하고 있다는 점이다. 이 서비스 역시 유레카 서버에 등록되고 API Gateway가 관리하는 서비스이다.

728x90
반응형
LIST
728x90
반응형
SMALL

유저 서비스에서 Security 관련 설정을 진행해보자. 사실 지금 딱히 Security 관련 설정할 내용이 있는건 아니다만 구현하고자 하는 서비스는 웹 관련 서비스가 아니기 때문에 설정해주면 좋을만한게 있다.

SMALL

 

WebSecurity

다른 설정 파일과 똑같이 @Configuration 애노테이션으로 클래스를 만들고 그 안에 @Bean 등록으로 설정 내용을 적용하면 된다. 


WebSecurity

package springmsa.springmsa_user_service.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class WebSecurity {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable);

        http.authorizeHttpRequests((requests) -> requests.requestMatchers("/users/**").permitAll());

        return http.build();
    }
}

 

추후에 설정 내용이 더 추가될 예정이다. 지금은 저 csrf를 disable()하는것에 초점을 두자. 우선 csrf는 무엇인지부터 확인해보자.

csrf

Cross Site Request Forgery의 약자로 사이트 간 위조 요청을 말한다. 이는 정상적인 유저가 의도치 않게 비정상적인 요청을 하는 것을 말하는데 특정 사이트에 정상 권한을 가지고 있는 유저에게 비정상적인 링크를 누군가가 보내고 그 링크를 아무런 의심없이 해당 유저가 클릭할 때 역시 이 비정상적인 요청을 할 수 있다. 그리고 해당 사이트는 이러한 요청에 대해 이 사용자가 악용된 사용자인지 일반 유저인지 구분할 수 없다. 

 

그래서 이를 방어하기 위해 csrf 토큰을 웹 사이트에서는 부여하여 이 토큰이 요청에 포함되어야만 요청을 받아들인다. 그럼 csrf를 왜 disable()했을까?

 

REST API만을 사용한다면 CSRF는 의미가 없다. Spring security 문서를 보면 non-browser-clients만을 위한 서비스라면 csrf를 disable해도 상관이 없다. REST API만을 이용하는 클라이언트는 요청 시 요청에 인증 정보(예: JWT)를 포함하여 요청하고 서버에서 인증 정보를 저장하지 않기 때문에 굳이 불필요한 csrf 코드들을 포함할 필요가 없는것이다. 

 

 

그니까 결론은, 브라우저를 이용하지 않고 모든 요청은 REST API로 들어온다면 CSRF 관련 코드를 빼주는 게 더 효율적인 서비스가 될 수 있다. 

 

 

authorizeHttpRequests()

두번째 라인은 특정 패턴의 요청이 들어왔을 때 요청을 허용할지에 대한 코드이다. 다음 코드를 보자.

http.authorizeHttpRequests((requests) -> requests.requestMatchers("/users/**").permitAll());

 

요청에 "/users/**" 패턴이 있으면 어떤 요청이든지 허가하겠다는 코드이다. 추후에 변경할 예정이지만 일단은 이렇게 해두자. 인증 관련 설정은 추후에 계속 할 예정이다.

728x90
반응형
LIST
728x90
반응형
SMALL

이번에는 UserService에서 조회 부분에 대한 API를 만들어보자.

SMALL

 

Service

기존에 만들었던 UserService 인터페이스에 다음 메서드들을 추가한다.

UserDto findUserById(Long id);

Iterable<Users> findAll();

 

UserService

package springmsa.springmsa_user_service.service;

import springmsa.springmsa_user_service.dto.UserDto;
import springmsa.springmsa_user_service.entity.Users;

public interface UserService {
    Users createUser(UserDto userDto);

    UserDto findUserById(Long id);

    Iterable<Users> findAll();
}

 

메서드를 새롭게 추가 했으니 구현 클래스에서 해당 메서드들을 구현해야한다.

UserServiceImpl

package springmsa.springmsa_user_service.service;

import jakarta.ws.rs.NotFoundException;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import springmsa.springmsa_user_service.dto.ResponseOrderDto;
import springmsa.springmsa_user_service.dto.UserDto;
import springmsa.springmsa_user_service.entity.Users;
import springmsa.springmsa_user_service.repository.UserRepository;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
    private final UserRepository userRepository;
    private final ModelMapper modelMapper;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Override
    public Users createUser(UserDto userDto) {
        userDto.setUserId(UUID.randomUUID().toString().substring(0, 8));

        Users users = modelMapper.map(userDto, Users.class);
        users.setEncryptedPwd(bCryptPasswordEncoder.encode(userDto.getPwd()));

        userRepository.save(users);

        return users;
    }

    @Override
    public UserDto findUserById(Long id) {
        Optional<Users> user = userRepository.findById(id);

        if (user.isEmpty()) {
            throw new NotFoundException("User not found");
        }

        UserDto userDto = modelMapper.map(user.get(), UserDto.class);

        List<ResponseOrderDto> orders = new ArrayList<>();
        userDto.setOrders(orders);

        return userDto;
    }

    @Override
    public Iterable<Users> findAll() {
        return userRepository.findAll();
    }
}

 

findUserById(Long id)findAll() 메서드를 구현하는데 내용은 간단하다.

findAll()은 repository에 위임하는것이 끝이고 findUserById(Long id)는 유저 아이디를 파라미터로 받으면 repository에서 먼저 유저를 찾은 후 있다면 ModelMapper를 이용해서 DTO로 변환한다. 유저는 추후에 만들 Order MicroService에 존재하는 주문 내역을 가지는데 우선은 Order MicroService를 만들지 않았으니 유저가 가지고 있는 주문 내역은 빈 리스트로 넣어 반환한다.

 

DTO는 다음과 같다.

ResponseOrderDto

package springmsa.springmsa_user_service.dto;

import lombok.Data;

import java.time.LocalDateTime;

@Data
public class ResponseOrderDto {
    private String productId;
    private Integer qty;
    private Integer unitPrice;
    private Integer totalPrice;
    private LocalDateTime createdAt;
    private String orderId;
}

 

UserDto

package springmsa.springmsa_user_service.dto;

import lombok.Data;

import java.time.LocalDateTime;
import java.util.List;

@Data
public class UserDto {
    private String email;
    private String name;
    private String pwd;
    private String encryptedPwd;
    private String userId;
    private LocalDateTime createdAt;

    private List<ResponseOrderDto> orders;
}

 

 

Controller

이제 유저 조회에 대한 컨트롤러 작업을 해보자. URI는 다음과 같다.

  • 유저 전체 조회: /users 
  • 유저 단일 조회: /users/{id}

UserController

package springmsa.springmsa_user_service.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import springmsa.springmsa_user_service.dto.*;
import springmsa.springmsa_user_service.entity.Users;
import springmsa.springmsa_user_service.service.UserService;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserController {

    private final UserService userService;
    private final ModelMapper modelMapper;

    @PostMapping("")
    public ResponseEntity<ApiResponseDto<ResponseUserDto>> createUser(@RequestBody RequestUserDto requestUserDto) {
        log.info("createUser payload: {}", requestUserDto);

        try {
            Users createdUser = userService.createUser(modelMapper.map(requestUserDto, UserDto.class));
            return ResponseEntity
                    .status(HttpStatus.CREATED)
                    .body(new ApiResponseDto<>(modelMapper.map(createdUser, ResponseUserDto.class), null));
        } catch (DataIntegrityViolationException e) {
            return ResponseEntity
                    .status(HttpStatus.BAD_REQUEST)
                    .body(new ApiResponseDto<>(null, e.getMessage()));
        }
    }

    @GetMapping("")
    public ResponseEntity<ApiResponseDto<List<ResponseUsersDto>>> getUsers() {
        Iterable<Users> users = userService.findAll();

        List<ResponseUsersDto> result = new ArrayList<>();

        users.forEach(user -> {
            ResponseUsersDto userDto = modelMapper.map(user, ResponseUsersDto.class);
            result.add(userDto);
        });

        return ResponseEntity.status(HttpStatus.OK).body(new ApiResponseDto<>(result, null));
    }

    @GetMapping("/{id}")
    public ResponseEntity<ApiResponseDto<ResponseUserDto>> getUser(@PathVariable Long id) {
        UserDto findUser = userService.findUserById(id);

        ResponseUserDto userDto = modelMapper.map(findUser, ResponseUserDto.class);

        return ResponseEntity.status(HttpStatus.OK).body(new ApiResponseDto<>(userDto, null));
    }
}

 

컨트롤러를 보면 getUsers()getUser(@PathVariable Long id)가 있다. 

 

전체 조회 코드를 먼저 보면, 서비스로부터 전체 유저 데이터를 받아온다. 그 다음 받아온 결과를 DTO로 변환해주는 코드가 필요하다. 

항상 컨트롤러에서 데이터를 반환할 땐 엔티티 자체가 아닌 DTO로 변환하여 돌려주어야 한다. 그래야 해당 엔티티의 변화에도 API 스펙에 영향이 가지 않을 뿐더러 (사실 이게 제일 중요) 엔티티를 리턴하는 것 자체가 좋은 방법이 아니다. 불필요한 데이터까지 API에 모두 태울 수 있으니. 

 

단일 조회 코드를 보면, URI로부터 유저 ID를 받아온다. 그 ID로 서비스로부터 유저를 조회하여 받아온다. 받아온 유저를 역시나 DTO로 변환한다. 굳이 ResponseUserDto와 ResponseUsersDto로 구분지은 이유는 전체 유저를 조회할 땐 유저의 주문 내역을 반환하지 않기 위해서다.

 

ResponseUsersDto

package springmsa.springmsa_user_service.dto;

import lombok.Data;

import java.util.List;

@Data
public class ResponseUsersDto {
    private String userId;
    private String email;
    private String name;
}

 

ResponseUserDto

package springmsa.springmsa_user_service.dto;

import lombok.Data;

import java.util.List;

@Data
public class ResponseUserDto {
    private String userId;
    private String email;
    private String name;

    private List<ResponseOrderDto> orders;
}

 

 

테스트

Postman으로 테스트를 해보자. 일단 유레카 서버와 API Gateway 서버가 모두 띄워져 있어야 한다. 나는 API Gateway로 요청할것이기 때문에. 확인을 위해 유레카 서버에 접속해보자. 

 

유레카 서버

인스턴스에 APIGATEWAY-SERVICE, USER-SERVICE가 등록되어 있으면 정상이다. 

 

  • http://localhost:8000/users (전체 조회)

전체 조회 API에 대해 테스트한 결과이다.

 

  • http://localhost:8000/users/1 (유저 단일 조회)

유저 단일 조회 API다. 이 데이터는 유저가 가지는 주문 내역에 대한 데이터 'orders'가 있다. 테스트에서 조회한 유저는 주문 내역이 없기 때문에 응답된 데이터가 없다.

 

728x90
반응형
LIST
728x90
반응형
SMALL
SMALL

참고자료

 

스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런

김영한 | 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기

www.inflearn.com

 

포인트컷 지시자

포인트컷 지시자에 대해서 자세히 알아보자. AspectJ는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공한다. 예를 들면 이렇다.

@Pointcut("execution(* hello.aop.order..*(..))")
  • 포인트컷 표현식은 execution과 같은 포인트컷 지시자(Pointcut Designator)로 시작한다. 줄여서 PCD라고도 한다.

포인트컷 지시자 종류

포인트컷 지시자의 종류는 다음과 같다.

  • execution: 메서드 실행 조인 포인트를 매칭한다. 스프링 AOP에서 가장 많이 사용한다.
  • within: 특정 타입(클래스, 인터페이스) 내의 조인 포인트를 매칭한다.
  • args: 인자가 주어진 타입의 인스턴스인 조인 포인트
  • this: 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
  • target: Target 객체(스프링 AOP 프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트
  • @target: 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
  • @within: 주어진 애노테이션이 있는 타입 내 조인 포인트
  • @annotation: 주어진 애노테이션을 가지고 있는 메서드를 조인 포인트로 매칭
  • @args: 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
  • bean: 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정한다.

말로만 보면 이해하기가 정말 난해하다. 그래서 코드로 하나씩 뜯어보자. execution이 가장 많이 사용되고 나머지는 거의 사용하지 않는다. 따라서 execution을 중점적으로 이해해보자.

 

예제 만들기

ClassAop

package cwchoiit.springadvanced.aop.member.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAop {
}

MethodAop

package cwchoiit.springadvanced.aop.member.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAop {

    String value() default "";
}

 

  • 우선, 두 개의 애노테이션을 만들었다. 하나는 클래스 레벨에 적용할 애노테이션이고 하나는 메서드 레벨에 적용할 애노테이션이다.
  • 애노테이션을 만드려면 기본적으로 두 개의 애노테이션이 필요하다. @Target, @Retention.
  • @Target은 이 애노테이션이 어디에 달릴지를 설명하는 애노테이션이다. ElementType.TYPE으로 설정하면 클래스 또는 인터페이스에 레벨에 적용할 애노테이션이고 ElementType.METHOD는 메서드 레벨에 적용할 애노테이션이다.
  • @Retention은 이 애노테이션이 살아있는 레벨을 말한다고 보면 된다. RetentionPolicy.RUNTIME으로 설정하면 런타임에도 해당 애노테이션은 살아 있는 상태로 남아있다. 그래서, 동적으로 애노테이션을 읽을 수 있다. RUNTIME말고 SOURCE도 있는데 이는 컴파일하면 컴파일된 파일에서 애노테이션이 보이지 않고 사라진다. 그래서 동적으로 이 애노테이션을 읽을 수 없다.
  • 그리고 MethodAop 애노테이션은 value() 라는 값을 가질 수 있다. 값의 형태는 문자열이다.

MemberService

package cwchoiit.springadvanced.aop.member;

public interface MemberService {
    String hello(String param);
}

 

MemberServiceImpl

package cwchoiit.springadvanced.aop.member;

import cwchoiit.springadvanced.aop.member.annotation.ClassAop;
import cwchoiit.springadvanced.aop.member.annotation.MethodAop;
import org.springframework.stereotype.Component;

@ClassAop
@Component
public class MemberServiceImpl implements MemberService {

    @Override
    @MethodAop("test value")
    public String hello(String param) {
        return "ok";
    }

    public String internal(String param) {
        return "ok";
    }
}

 

  • 이번엔 인터페이스와 그 인터페이스를 구현한 클래스를 만들었다. 간단하게 하나의 메서드를 가지는 인터페이스(MemberService)와 그를 구현한 MemberServiceImpl이 있고, 이 구현 클래스는 @ClassAop 애노테이션을 달았다. 그리고 이 구현 클래스 내부에 hello(String param)@MethodAop 애노테이션이 달려있다. 

ExecutionTest

package cwchoiit.springadvanced.aop.pointcut;

import cwchoiit.springadvanced.aop.member.MemberServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;

import java.lang.reflect.Method;

@Slf4j
public class ExecutionTest {

    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    Method helloMethod;

    @BeforeEach
    public void init() throws NoSuchMethodException {
        helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
    }

    @Test
    void printMethod() {
        log.info("helloMethod = {}", helloMethod);
    }
}

 

  • 테스트 코드다. 리플렉션을 이용해서 구현한 MemberServiceImplhello 메서드를 가져온다. 각 테스트의 실행마다 그 바로 직전에 리플렉션을 활용해서 메서드를 가져오기 위해 @BeforeEach를 사용했다. 
  • AspectJExpressionPointcut은 포인트컷 표현식을 처리해주는 클래스다. 여기에 포인트컷 표현식을 지정하면 된다. 이 클래스는 상위에 Pointcut 인터페이스를 가진다. 
  • 간단하게 printMethod()를 실행해보면 다음과 같은 결과를 얻는다.

실행 결과

helloMethod = public java.lang.String cwchoiit.springadvanced.aop.member.MemberServiceImpl.hello(java.lang.String)
  • 이 결과 어디서 많이 본 것 같다. 맞다. 이렇게 메서드 정보를 포인트컷 표현식으로 매칭해서 포인트컷 대상을 찾을 것이다. 

execution - 1

가장 중요한 포인트컷 지시자이다. 이 execution은 다음과 같이 사용한다.

execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)

 

  • execution은 메서드 실행 조인 포인트를 매칭한다. 그래서 결국 모든 메서드들 중 이 표현식에 일치하는 메서드들이 AOP로 적용된다. 위 표현 방식에서 '?'가 있는 것은 생략이 가능하다는 뜻이다.
  • 그럼 하나씩 천천히 알아보자. 가장 정확한(자세한) 포인트 컷으로 표현해보자. 

가장 정확한(자세한) 포인트컷

execution(public String cwchoiit.springadvanced.aop.member.MemberServiceImpl.hello(String))
  • 접근제어자?: public
  • 반환 타입: String
  • 선언 타입?: cwchoiit.springadvanced.aop.member.MemberServiceImpl
  • 메서드이름: hello
  • 파라미터: (String)
  • 예외?: 생략

이렇게 예외를 제외하고 모든 키워드를 작성했다. hello 메서드에 예외는 없기 때문에 제외했다.

이렇게 포인트컷을 지정하고 해당 포인트컷과 hello 메서드가 매치하는지 확인하는 테스트 코드를 작성해보자.

@Test
void exactMatch() {
    pointcut.setExpression("execution(public String cwchoiit.springadvanced.aop.member.MemberServiceImpl.hello(String))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

 

 

가장 많이 생략한 포인트컷

execution(* *(..))
  • 접근제어자?: 생략
  • 반환 타입: *
  • 선언 타입?: 생략
  • 메서드이름: *
  • 파라미터: (..)
  • 예외?: 생략

'*'은 와일드카드로 모든것을 허용한다는 의미로 받아들이면 될 것 같다. 여기서 생략을 할 수 없는 필수 키워드인 반환 타입, 메서드명, 파라미터만을 작성했다. 반환 타입은 전체(*)이며 메서드명 또한 어떠한 것도 상관 없다는 의미의 '*'이고 파라미터는 어떤 파라미터라도 상관없다는 의미의 (..)를 사용했다. (..)는 파라미터가 없거나 여러개거나 한개거나 어떠한 상태여도 상관이 없다는 의미이다.

 

이런 포인트컷을 사용해서 메서드가 일치하는지 확인해보자.

@Test
    void allMatch() {
        pointcut.setExpression("execution(* *(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

 

  • 결과는 당연히 테스트 통과한다.

메서드 이름 매칭 관련 포인트컷

메서드 이름과 관련된 포인트 컷을 여러개 확인해보자. 메서드 이름에도 '*'를 사용할 수 있다.

@Test
void nameMatch() {
    pointcut.setExpression("execution(* hello(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void nameMatchStar1() {
    pointcut.setExpression("execution(* hel*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void nameMatchStar2() {
    pointcut.setExpression("execution(* *el*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void nameMatchFalse() {
    pointcut.setExpression("execution(* none(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
  • 메서드 이름 앞뒤로 *을 사용해서 매칭할 수 있다.

패키지 매칭 관련 포인트컷

패키지 이름과 관련된 포인트 컷도 여러개 확인해보자. 주의할 점은 하위 패키지 전부를 허용하고 싶을 땐 '..'을 사용해야 한다. (점 두개)

@Test
void packageExactMatch1() {
    pointcut.setExpression("execution(* cwchoiit.springadvanced.aop.member.MemberServiceImpl.hello(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void packageExactMatch2() {
    pointcut.setExpression("execution(* cwchoiit.springadvanced.aop.member.*.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void packageExactFalse() {
    pointcut.setExpression("execution(* cwchoiit.springadvanced.aop.*.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}

@Test
void packageMatchSubPackage1() {
    pointcut.setExpression("execution(* cwchoiit.springadvanced.aop..*.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

 

  • packageExactFalse()를 확인해보면 cwchoiit.springadvanced.aop.*.*(..)로 되어 있는데 이는 하위 패키지도 포함하는게 아니다. 즉, 정확히 cwchoiit.springadvanced.aop경로의 모든 타입(인터페이스, 클래스)의 모든 메서드를 지정하는 포인트 컷이다. 하위 패키지도 포함하려면 packageMatchSubPackage1()처럼 cwchoiit.springadvanced.aop..*.*(..)로 작성해야 한다. 

 

execution - 2

타입 매칭 포인트컷 

타입 정보에 대한 매치 조건이다.

@Test
void typeExactMatch() {
    pointcut.setExpression("execution(* com.example.aop.member.MemberServiceImpl.*(..))");

    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

 

이처럼 정확히 패키지 + 타입(클래스)가 일치하게 포인트컷을 지정할 수 있다. 근데 한가지 조심할 게 있다. 부모 타입은 어떻게 될까?

그러니까 MemberServiceImpl은 상위에 MemberService 인터페이스가 있다. 그럼 포인트컷 표현식에 부모 타입을 작성했을 때 저 hello 메서드는 포인트컷 조건에 만족할까? 결론부터 말하면 만족한다.

@Test
void typeMatchSuperType() {
    // 상위 타입으로 expression 을 설정
    pointcut.setExpression("execution(* com.example.aop.member.MemberService.*(..))");

    // pointcut 은 상위 타입이고 상위 타입이 가지고 있는 메서드면 자식 메서드도 역시 가능하다. 이유는 자식은 부모에 들어갈 수 있으니까.
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

자식은 부모에 들어가는 게 가능하기 때문에, 포인트컷 표현식을 부모로 설정하면 자식 클래스들은 포인트컷을 만족한다. 단, 인터페이스에서 선언된 메서드에 한하여. 이 말은 무슨말이냐면 부모일지언정 부모에 선언된 메서드가 아니라 자식 내부적으로만 가지고 있는 메서드는 포인트컷을 만족하지 못한다는 말이다.

 

위에서 MemberService와 MemberServiceImpl을 보면 부모인 인터페이스에는 hello 메서드만 있고 internal은 없다. 자식인 구체 클래스에는 internal 이라는 내부 메서드가 있다. 이 땐 부모 타입으로 포인트컷을 지정하면 자식 내부적으로만 가지고 있는 메서드에는 포인트 컷 조건이 만족하지 않는다.

@Test
void typeMatchInternal() throws NoSuchMethodException {
    // 상위 타입으로 expression 을 설정
    pointcut.setExpression("execution(* com.example.aop.member.MemberService.*(..))");

    Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);

    // 상위 타입으로 pointcut 의 expression 을 설정한 경우, 상위 타입이 가지고 있는 메서드만 가능하다.
    assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isFalse();
}

 

 

파라미터 매칭 포인트 컷

이 파라미터 매칭 조건은 다음 예시를 보면 하나하나 다 이해가 가능할 것이다.

/**
 * 모든 메서드 중 파라미터가 String 타입 하나 인 것들을 매치
 * */
@Test
void argsMatch() {
    pointcut.setExpression("execution(* *(String))");

    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

/**
 * 모든 메서드 중 파라미터가 없는 것들을 매치
 * */
@Test
void argsMatchNoArgs() {
    pointcut.setExpression("execution(* *())");

    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}

/**
 * 모든 메서드 중 모든 타입을 허용하지만 딱 한 개의 파라미터만 허용
 * */
@Test
void argsMatchWildCard() {
    pointcut.setExpression("execution(* *(*))");

    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

/**
 * 모든 메서드 중 모든 타입, 모든 개수의 파라미터를 허용
 * */
@Test
void argsMatchAll() {
    pointcut.setExpression("execution(* *(..))");

    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

/**
 * 모든 메서드 중 파라미터가 String 타입으로 시작하고 그 이후는 모든 타입, 모든 개수의 파라미터를 허용 또는 없어도 된다.
 * */
@Test
void argsMatchComplex() {
    pointcut.setExpression("execution(* *(String, ..))");

    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

/**
 * 모든 메서드 중 파라미터가 딱 두개이면서 둘 다 String 타입인 것
 * */
@Test
void argsMatchComplexExactly() throws NoSuchMethodException {
    pointcut.setExpression("execution(* *(String, String))");

    Method twoParamsMethod = MemberServiceImpl.class.getMethod("twoParams", String.class, String.class);

    assertThat(pointcut.matches(twoParamsMethod, MemberServiceImpl.class)).isTrue();
}

/**
 * 모든 메서드 중 파라미터가 딱 두개이면서 첫번째는 String, 두번째는 모든 타입
 * */
@Test
void argsMatchComplexExactly2() throws NoSuchMethodException {
    pointcut.setExpression("execution(* *(String, *))");

    Method twoParamsMethod = MemberServiceImpl.class.getMethod("twoParams", String.class, String.class);

    assertThat(pointcut.matches(twoParamsMethod, MemberServiceImpl.class)).isTrue();
}

 

 

within

within 지시자는 특정 타입 내의 조인 포인트들로 매칭을 제한한다. 이 말만 보면 무슨말인지 잘 모르겠다. 쉽게 말하면 작성한 타입이 매칭되면 그 안의 메서드들이 자동으로 매치된다.

 

WithinTest.java

package com.example.aop.pointcut;

import com.example.aop.member.MemberServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;

import java.lang.reflect.Method;

import static org.assertj.core.api.Assertions.*;

/**
 * Within은 타입(클래스, 인터페이스)을 지정하면 그 안에 메서드는 모두 매치가 되게 하는 방법
 * */
public class WithinTest {

    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    Method helloMethod;

    @BeforeEach
    public void init() throws NoSuchMethodException {
        helloMethod = MemberServiceImpl.class.getMethod("hello", String.class); // method 이름이 hello, 파라미터의 타입이 String
    }

    @Test
    void withinExactly() throws NoSuchMethodException {
        pointcut.setExpression("within(com.example.aop.member.MemberServiceImpl)");

        Method internal = MemberServiceImpl.class.getMethod("internal", String.class);

        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut.matches(internal, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void withinWildCard() {
        pointcut.setExpression("within(com.example.aop.member.*Service*)");

        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void withinSubPackage() {
        pointcut.setExpression("within(com.example.aop..*)");

        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    @DisplayName("타겟의 정확하게 타입에만 직접 적용해야 한다")
    void withinSuperTypeFalse() {
        // 상위 타입으로 설정하면 within 은 안된다. 정확히 그 타입으로 지정해야 한다. execution 은 이게 가능했는데 within 은 아니다.
        pointcut.setExpression("within(com.example.aop.member.MemberService)");

        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }
}

 

withinExactly()를 보면 within(com.example.aop.member.MemberServiceImpl)이라고 되어 있다. 이렇게 하면 MemberServiceImpl 클래스 내 메서드들이 이 포인트컷에 매칭된다.

 

주의

그러나, 주의할 부분이 있다. 표현식에 부모 타입을 지정하면 안된다. 정확하게 타입이 맞아야 한다. 이 점이 execution과 다른 점이다.

 

 

args

인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭. 말이 또 어려운데 쉽게 말해 파라미터가 매치되는 녀석들이 다 조인 포인트가 된다고 보면 된다. 아래 코드를 보면 바로 이해가 될 것이다.

 

ArgsTest.java

package com.example.aop.pointcut;

import com.example.aop.member.MemberServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;

import java.lang.reflect.Method;

import static org.assertj.core.api.Assertions.assertThat;

public class ArgsTest {

    Method helloMethod;

    @BeforeEach
    public void init() throws NoSuchMethodException {
        helloMethod = MemberServiceImpl.class.getMethod("hello", String.class); // method 이름이 hello, 파라미터의 타입이 String
    }

    private AspectJExpressionPointcut pointcut(String expression) {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(expression);

        return pointcut;
    }

    @Test
    void args() {
        // hello(String)과 매칭
        assertThat(pointcut("args(String)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("args(Object)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("args()").matches(helloMethod, MemberServiceImpl.class)).isFalse();
        assertThat(pointcut("args(..)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("args(*)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("args(String, ..)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void argsVsExecution() {
        // Args (Args 는 상위 타입을 허용한다)
        assertThat(pointcut("args(String)")
                .matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("args(java.io.Serializable)")
                .matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("args(Object)")
                .matches(helloMethod, MemberServiceImpl.class)).isTrue();

        // Execution (Execution 은 상위 타입을 허용하지 않고 딱 정확하게 선언해야 한다)
        assertThat(pointcut("execution(* *(String))")
                .matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("execution(* *(java.io.Serializable))")
                .matches(helloMethod, MemberServiceImpl.class)).isFalse();
        assertThat(pointcut("execution(* *(Object))")
                .matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }
}

 

args()를 보면, pointcut으로 args(String), args(Object),... 이렇게 되어 있다. 즉, 이 파라미터와 일치하는 메서드를 매치시키는 방법. 근데 이 args는 execution과 다르게 부모 타입도 허용한다. 즉, 파라미터의 타입이 String인 메서드라면 args(Object)로 해도 매치가 된다는 뜻이다. 

 

참고로 args 지시자는 단독으로 사용되기 보다는 파라미터 바인딩에서 주로 사용된다. 

 

 

@target, @within

정의

@target: 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트

@within: 주어진 애노테이션이 있는 타입 내 조안 포인트

 

사실 그렇게 중요하지도 않고 정의만 보고서는 뭔 말인지 감이 잘 안오지만 코드로 보면 간단하다. 우선 둘 모두 타입에 있는 애노테이션으로 AOP 적용 여부를 판단한다.

@target(hello.aop.member.annotation.ClassAop)
@within(hello.aop.member.annotation.ClassAop)

 

@ClassAop
class Target {

}

 

여기서 두 개의 차이는 다음과 같다. 

@target은 애노테이션이 달린 클래스의 부모 클래스의 메서드까지 어드바이스를 전부 적용하고, @within은 자기 자신의 클래스에 정의된 메서드만 어드바이스를 적용한다. 

 

 

그래서 한 문장으로 정리를 하자면 @target, @within 둘 모두 애노테이션으로 AOP를 적용하는데 @target의 경우 애노테이션이 달린 클래스와 그 상위 클래스의 메서드 모두에게 어드바이스를 적용하고 @within의 경우 애노테이션이 달린 클래스의 메서드에만 어드바이스를 적용한다.

 

AtTargetAtWithinTest.java

package com.example.aop.pointcut.annotation;

import com.example.aop.member.annotation.ClassAop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;


/**
 * 클래스에 붙이는 애노테이션을 기반으로 포인트컷을 만들 땐 @target, @within 을 사용할 수 있다.
 * */
@Slf4j
@Import(AtTargetAtWithinTest.Config.class)
@SpringBootTest
public class AtTargetAtWithinTest {

    @Autowired Child child;

    @Test
    void success() {
        log.info("child proxy = {}", child.getClass());

        child.childMethod();
        child.parentMethod();
    }

    static class Config {

        @Bean
        public Parent parent() { return new Parent(); }

        @Bean
        public Child child() { return new Child(); }

        @Bean
        public AtTargetAtWithinAspect atTargetAtWithinAspect() { return new AtTargetAtWithinAspect(); }
    }

    static class Parent {
        public void parentMethod() {
            log.info("[parentMethod] Start");
        }
    }

    @ClassAop
    static class Child extends Parent {
        public void childMethod() {
            log.info("[childMethod] Start");
        }
    }

    @Aspect
    static class AtTargetAtWithinAspect {

        // @target: 인스턴스 기준으로 모든 메서드의 조인 포인트를 선정 = 부모 타입의 메서드도 적용
        @Around("execution(* com.example.aop..*(..)) && @target(com.example.aop.member.annotation.ClassAop)")
        public Object atTarget(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@target] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        // @within: 선택된 클래스 내부에 있는 메서드만 조인 포인트로 선정 = 부모 타입의 메서드는 적용되지 않음
        @Around("execution(* com.example.aop..*(..)) && @within(com.example.aop.member.annotation.ClassAop)")
        public Object atWithin(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@within] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        // 참고로 @target, @args, args 이런 포인트컷 지시자는 단독으로 사용하면 안된다. 위 에제에서도 execution 과 같이 사용했는데
        // 그 이유는 스프링이 이런 포인트컷 지시자가 있으면 모든 스프링 빈에 AOP 를 적용하려고 시도하는데 스프링이 내부에서 사용하는 빈 중에는 final 로
        // 지정된 빈들도 있기 때문에 오류가 발생할 수 있다.
    }
}

 

 

우선 체크포인트는 다음과 같다.

 

  • Child, Parent 클래스가 있다. Child 클래스는 상위 클래스로 Parent 클래스가 있다. 
  • 두 클래스를 모두 스프링 빈으로 등록한다.
  • 에스팩트가 있고 두 개의 어드바이저가 있다. 하나는 @target 하나는 @within으로 만들어진 포인트컷이다.
  • @target@within 모두 같은 애노테이션인 ClassAop 애노테이션이 달린 클래스를 찾아 AOP로 적용한다.
  • 이 에스팩트 역시 스프링 빈으로 등록한다.
  • 스프링 빈으로 등록한 Child 클래스를 테스트 코드에서는 주입받는다.
  • 주입받은 Child 클래스의 childMethod(), parentMethod()를 각각 호출한다.
  • 결과는 childMethod() 호출 시, @target과 @within 모두 적용된다. parentMethod() 호출 시 @target만 적용되고 @within은 적용되지 않는다.

 

주의

args, @args, @target 이 포인트컷 지시자는 단독으로 사용할 수 없다. 그 이유는 이런 포인트컷이 있으면 스프링은 모든 스프링 빈에 AOP를 적용하려고 시도한다. 문제는 모든 스프링 빈에 AOP를 적용하려고 하면 스프링이 내부에서 사용하는 빈 중에는 final로 지정된 빈들도 있기 때문에 오류가 발생한다. 따라서 이런 포인트컷 지시자는 단독으로 사용하면 안되고 최대한 적용 대상을 축소하는 표현식과 함께 사용해야 한다.

 

 

@annotation, @args

@annotation: 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭

@args: 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트

 

@annotation은 종종 사용되니 이것을 집중해서 보자.

@annotation(hello.aop.member.annotation.MethodAop)

 

쉽게 말해 메서드에 지정한 애노테이션이 있으면 매칭한다. 다음 코드처럼.

public class MemberServiceImpl {
     @MethodAop("test value")
     public String hello(String param) {
         return "ok";
     }
}

 

AtAnnotationTest.java

package com.example.aop.pointcut.annotation;

import com.example.aop.member.MemberService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;


/**
 * 메서드에 붙이는 애노테이션을 기반으로 포인트컷을 만들 때 사용되는 @annotation
 *
 * ⭐이거는 좀 자주 사용된다.
 * */
@Slf4j
@Import(AtAnnotationTest.AtAnnotationAspect.class)
@SpringBootTest
public class AtAnnotationTest {

    @Autowired
    MemberService memberService;

    @Test
    void success() {
        log.info("memberService proxy = {}", memberService.getClass());
        memberService.hello("hello");
    }

    @Aspect
    static class AtAnnotationAspect {

        @Around("@annotation(com.example.aop.member.annotation.MethodAop)")
        public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@annotation] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

 

위 코드에서 에스팩트를 보면 @Around의 포인트컷 지시자로 @annotation을 사용한다. MethodAop 라는 애노테이션이 달린 메서드에 이 AOP가 적용된다. 그리고 만든 MemberServiceImpl의 hello()는 @MethodAop 애노테이션이 있다. 따라서 테스트 success()는 AOP가 적용된 hello()가 호출된다.

 

 

bean

스프링 전용 포인트컷 지시자. 빈의 이름으로 지정한다.

bean(orderService) || bean(*Repository)

 

바로 예시 코드로 확인해보자. 그리고 이 지시자 역시 자주 사용되는 지시자는 아니다.

 

BeanTest.java

package com.example.aop.pointcut;

import com.example.aop.order.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

@Slf4j
@Import(BeanTest.BeanAspect.class)
@SpringBootTest
public class BeanTest {

    @Autowired
    OrderService orderService;

    @Test
    void success() {
        orderService.orderItem("item");
    }

    @Aspect
    static class BeanAspect {

        @Around("bean(orderService) || bean(*Repository)")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[bean] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

 

BeanAspect를 보면 orderService라는 bean 또는 *Repository라는 bean을 포인트컷의 조건으로 어드바이스를 만든 모습을 확인할 수 있다. 그 후 테스트 success()는 orderService의 orderItem()을 호출한다. 그러므로 저 어드바이스가 적용된다.

 

 

매개변수 전달 (중요⭐️)

어드바이스 쪽에서 메서드의 파라미터를 전달받고 싶을 땐 어떻게 해야 할까? 예를 들어 다음 코드를 보자.

orderService.orderItem("item");

 

이런 코드가 있을 때, 어드바이스가 저 파라미터 'item'을 어떻게 받을 수 있을까? 이를 알아보자.

 

우선 이 경우 joinPoint를 사용하거나 다음 포인트컷 지시자를 활용한다.

  • args

가장 원시적인 방법을 먼저 확인해보자.

 

포인트컷

@Pointcut("execution(* hello.aop.member..*.*(..))")
private void allMember() {}

 

JoinPoint를 활용하기

 

@Around("allMember()")
public Object logArgs1(ProceedingJoinPoint joinPoint) throws Throwable {
    Object arg1 = joinPoint.getArgs()[0];
    log.info("[logArgs1]{}, arg={}", joinPoint.getSignature(), arg1);
    return joinPoint.proceed();
}

 

joinPoint를 활용하면 getArgs() 메서드를 사용할 수 있다. 허나, 이 방법은 배열에서 꺼내는 방식인데 그렇게 좋은 방식은 아닌것 같다.

 

args

@Around("allMember() && args(arg, ..)")
public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
    log.info("[logArgs2]{}, arg = {}", joinPoint.getSignature(), arg);
    return joinPoint.proceed();
}

 

포인트컷 지시자 'args'를 사용한다. args(arg, ..)은 첫번째 파라미터를 받고 그 이후에 파라미터는 있거나 없거나 신경쓰지 않는다는 뜻이다. 그리고 이 arg를 어드바이스의 파라미터로 이름 그대로(arg) 동일하게 받아야 한다. 

 

실제 파라미터의 타입은 String인데 그 상위 타입인 Object로 받아도 무방하다. 

 

위에서 @Around를 사용했는데 @Around는 ProceedingJoinPoint를 반드시 첫번째 파라미터로 받아야 하는 불편함이 있다. 굳이 코드 내에서 실제 객체를 호출하는 코드를 직접 호출해야 하는 경우가 아니라면 다음처럼 더 간략하게 사용할 수 있다.

@Before("allMember() && args(arg, ..)")
public void logArgs3(String arg) {
    log.info("[logArgs3] arg = {}", arg);
}

 

이번에는 상위 타입이 아닌 정확히 String으로 받아주었다. 물론 상위 타입도 상관없다.

 

 

@annotation으로 애노테이션이 가지고 있는 값들 꺼내오기

애노테이션 중에는 특정 값을 가지는 애노테이션이 있다. 다음이 그 예시다.

package com.example.aop.member.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAop {
    String value();
}

 

 

value() 라는 값을 가지는 애노테이션이다.

 

package com.example.aop.member;

import com.example.aop.member.annotation.ClassAop;
import com.example.aop.member.annotation.MethodAop;
import org.springframework.stereotype.Component;

@ClassAop
@Component
public class MemberServiceImpl implements MemberService {

    @Override
    @MethodAop(value = "test value")
    public String hello(String param) {
        return "ok";
    }

    public String internal(String param) {
        return "ok";
    }

    public String twoParams(String param1, String param2) {
        return "ok";
    }
}

 

그 애노테이션의 value 값으로 'test value'라는 값을 가지는 hello()가 있을 때 이 값은 어떻게 가져올까?

 

다음처럼 @annotation을 활용해서 애노테이션을 파라미터로 받으면 된다.

@Before("allMember() && @annotation(annotation)")
public void atAnnotationAcceptedArgs(JoinPoint joinPoint, MethodAop annotation) {
    log.info("[@annotation Accepted]{}, annotationValue = {}", joinPoint.getSignature(), annotation.value());
}

여기서, @annotation(annotation)이라고 썼으면 파라미터에서도 'annotation'이라는 이름으로 받아야 한다. 만약 @annotation(methodAop)로 썼으면 파라미터도 'methodAop'라는 이름으로 받으면 된다. 

 

그리고 한가지 더, 원래는 @annotation 지시자를 사용할 때 패키지명부터 쭉 써줘야 한다. 예를 들면 이렇게.

@annotation(com.example.aop.member.annotation.MethodAop)

 

근데 위에서처럼 저렇게 파라미터로 애노테이션 타입을 명시하면 이름으로 치환할 수 있다.

 

 

this, target

솔직히 이게 중요한지는 모르겠다. 근데 내용이 은근히 어렵다. 그래서 굳이라는 생각이 들지만 한번 정리해보겠다.

 

  • this: 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
  • target: Target 객체(스프링 AOP 프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트

 

설명

  • this, target은 다음과 같이 적용 타입 하나를 정확하게 지정해야 한다.
  • '*' 같은 패턴을 사용할 수 없다.
  • 부모 타입을 허용한다.
this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)

 

똑같이 생겨가지고 무슨 차이가 있을까?

 

스프링에서 AOP를 적용하면 실제 target 객체 대신에 프록시 객체가 스프링 빈으로 등록된다. 여기서,

  • this는 스프링 빈으로 등록되어 있는 프록시 객체를 대상으로 포인트컷을 매칭한다.
  • target은 실제 target 객체를 대상으로 포인트컷을 매칭한다.

그러니까 다음 코드 예시를 보면,

this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)

 

똑같이 MemberService를 조건으로 입력해도 this는 스프링 빈으로 등록된 프록시를, target은 스프링 빈으로 등록된 프록시가 참조하는 실제 객체를 바라본다는 뜻인데 이게 뭐 큰 의미가 있고 달라지나 싶을 수 있다. 그러나, JDK 동적 프록시와 CGLIB의 프록시 생성 방식이 다르기 때문에 차이점이 발생할 수 있다.

 

JDK 동적 프록시

이 방식은 인터페이스가 필수이고 인터페이스를 구현한 프록시 객체를 생성한다. 다음이 그 그림이다.

 

 

그럼 이 방식으로 프록시를 만들 때 this와 target 지시자가 어떻게 다른지 보자.

 

MemberService 인터페이스 지정

  • this(hello.aop.member.MemberService)
    • proxy 객체를 보고 판단한다. this는 부모 타입을 허용한다. 프록시는 인터페이스인 MemberService를 참조하므로 AOP가 적용된다.
  • target(hello.aop.member.MemberService)
    • target 객체를 보고 판단한다. target은 부모 타입을 허용한다. target이 상속받는 MemberService가 있으므로 AOP가 적용된다.

MemberServiceImpl 구체 클래스 지정

  • this(hello.aop.member.MemberServiceImpl)
    • proxy 객체를 보고 판단한다. 프록시 객체의 부모는 MemberService 인터페이스이다. 인터페이스 위에 있는 것은 없다. MemberServiceImpl에 대한 정보를 아예 알 수 없으므로 AOP 적용 대상이 아니다.
  • target(hello.aop.member.MemberServiceImpl)
    • target 객체를 보고 판단한다. target은 바로 MemberServiceImpl 구체 클래스이므로 AOP 적용 대상이다.

 

결론은 JDK 동적 프록시는 this로 구체 클래스를 받으면 AOP 적용 대상이 아니게 된다. 반면 CGLIB는 어떨까?

 

 

CGLIB 프록시

 

MemberService 인터페이스 지정

  • this(hello.aop.member.MemberService)
    • this는 proxy 객체를 바라본다. 프록시 객체는 구체 클래스인 MemberServiceImpl을 상속받는다. 그리고 이 구체 클래스의 부모인 MemberService 인터페이스가 있다. this는 부모 타입을 허용하므로 AOP 적용 대상이다.
  • target(hello.aop.member.MemberService)
    • target은 실제 target 객체를 바라본다. target 객체인 MemberServiceImpl의 부모인 MemberService가 있다. target은 부모 타입을 허용하므로 AOP 적용 대상이다.

MemberServiceImpl 구체 클래스 지정

  • this(hello.aop.member.MemberServiceImpl)
    • this는 proxy 객체를 바라본다. 프록시 객체는 구체 클래스인 MemberServiceImpl을 상속받는다. this는 부모 타입을 허용하므로 AOP 적용 대상이다.
  • target(hello.aop.member.MemberServiceImpl)
    • target은 실제 target 객체를 바라본다. target 객체가 MemberServiceImpl이므로 AOP 적용 대상이다.

 

결론은 CGLIB 프록시는 모든 경우에 AOP 적용 대상이 된다. 그리고 스프링은 기본으로 CGLIB로 프록시를 만들어낸다. 

 

 

실제로 AOP 적용을 위 설명처럼 하는지 확인해보자.

 

ThisTargetTest.java

package com.example.aop.pointcut;

import com.example.aop.member.MemberService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

@Slf4j
@Import(ThisTargetTest.ThisTargetAspect.class)
@SpringBootTest
public class ThisTargetTest {

    @Autowired
    MemberService memberService;

    @Test
    void success() {
        log.info("memberService Proxy = {}", memberService.getClass());
        memberService.hello("helloA");
    }

    @Aspect
    static class ThisTargetAspect {

        @Around("this(com.example.aop.member.MemberService)")
        public Object doThisInterface(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[this-interface] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        @Around("target(com.example.aop.member.MemberService)")
        public Object doTargetInterface(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[target-interface] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        @Around("this(com.example.aop.member.MemberServiceImpl)")
        public Object doThisConcrete(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[this-impl] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        @Around("target(com.example.aop.member.MemberServiceImpl)")
        public Object doTargetConcrete(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[target-impl] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

 

에스팩트에 4개의 어드바이저가 있다. 위 설명 대로 this에 인터페이스, 구체 클래스를 target에 인터페이스, 구체 클래스를 적용했을 때 AOP가 적용되는지에 대한 내용이다. 스프링은 기본으로 CGLIB 프록시로 프록시를 만들어내는데 그 설정 값은 다음과 같다.

spring:
  aop:
    proxy-target-class: true # true = CGLIB 를 기본으로 / false = JDK 동적 프록시를 기본으로

 

이 상태로 success() 테스트를 실행하면 모든 어드바이저가 적용된다.

 

이제 JDK 동적 프록시를 스프링 기본 프록시로 설정해보자. 

spring:
  aop:
    proxy-target-class: false

 

 

this-impl 로그가 찍히지 않았음을 확인할 수 있다.

 

 

728x90
반응형
LIST

'Spring Advanced' 카테고리의 다른 글

Redis를 사용해서 캐싱하기  (0) 2024.10.02
Mockito를 사용한 스프링 프로젝트 단위 테스트  (4) 2024.09.29
스프링 AOP Part.2  (0) 2024.01.02
스프링 AOP Part. 1  (0) 2023.12.29
AOP와 @Aspect  (0) 2023.12.29
728x90
반응형
SMALL

참고자료

 

스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런

김영한 | 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기

www.inflearn.com

이번 포스팅부터 스프링 AOP를 직접 만들고 사용해보면서 AOP에 대해 자세히 익혀보자!

 

라이브러리 추가

build.gradle

 implementation 'org.springframework.boot:spring-boot-starter-aop'

 

이 라이브러리를 추가한 후에 다운된 외부 라이브러리 목록을 보면 다음 라이브러리가 있어야 한다.

  • 이 라이브러리를 추가하면 스프링이 자동으로 무엇을 등록해준다고 했던가? 바로 빈 포스트 프로세서 중 AnnotationAwareAspectJAutoProxyCreator이 녀석을 등록해준다고 했다. 
  • 이 빈 포스트 프로세서는 빈으로 등록된 어드바이저, @Aspect 애노테이션이 붙은 빈(꼭 빈으로 등록해야 한다!)을 모두 찾아서 그 안에 포인트컷과 어드바이스를 통해 어드바이저로 만들어 둔 후, 모든 빈들에 대해 프록시가 적용될 수 있는지를 검토 후 적용해야 한다면 적용하여 프록시로 빈을 등록하거나 적용대상이 아니라면 빈을 그대로 빈으로 등록해주는 빈 포스트 프로세서다. 다시 복습 차원에서!

예제 프로젝트 만들기

AOP를 적용할 예제 프로젝트를 만들어보자. 지금까지 학습했던 내용과 비슷하다.

OrderRepository

package cwchoiit.springadvanced.aop.order;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;

@Slf4j
@Repository
public class OrderRepository {
    public String save(String itemId) {
        log.info("[orderRepository] 실행");
        if (itemId.equals("ex")) {
            throw new IllegalStateException("예외 발생!");
        }
        return "ok";
    }
}

 

OrderService

package cwchoiit.springadvanced.aop.order;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;

    public void orderItem(String itemId) {
        log.info("[orderService] 실행");
        orderRepository.save(itemId);
    }
}

 

AopTest - 테스트 코드

package cwchoiit.springadvanced.aop;

import cwchoiit.springadvanced.aop.order.OrderRepository;
import cwchoiit.springadvanced.aop.order.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.*;

@Slf4j
@SpringBootTest
public class AopTest {

    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    void aopInfo() {
        log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderService));
        log.info("isAopProxy, orderRepository = {}", AopUtils.isAopProxy(orderRepository));
    }

    @Test
    void success() {
        orderService.orderItem("itemA");
    }

    @Test
    void exception() {
        assertThatThrownBy(() -> orderService.orderItem("ex")).isInstanceOf(IllegalStateException.class);
    }
}
  • AopUtils.isAopProxy(...)를 통해서 AOP 프록시가 적용되었는지 확인할 수 있다. 현재 AOP 관련 코드를 작성하지 않았으므로 프록시가 적용되지 않고, 결과도 false를 반환해야 정상이다.
  • 여기서는 실제 결과를 검증하는 테스트가 아니라 학습 테스트를 진행한다. 앞으로 로그를 직접 보면서 AOP가 잘 작동하는지 확인해 볼 것이다. 테스트를 실행해서 잘 동작하면 다음으로 넘어가자.

 

스프링 AOP 구현1 - 시작

스프링 AOP를 구현하는 일반적인 방법은 앞서 학습한 @Aspect를 사용하는 방법이다. 이번 시간에는 @Aspect를 사용해서 가장 단순한 AOP를 구현해보자.

AspectV1

package cwchoiit.springadvanced.aop.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Slf4j
@Aspect
public class AspectV1 {

    @Around("execution(* cwchoiit.springadvanced.aop.order..*(..))")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }
}
  • @Around 애노테이션의 값인 "execution(* cwchoiit.springadvanced.aop.order..*(..))"는 포인트컷이 된다.
  • @Around 애노테이션의 메서드인 doLog는 어드바이스(Advice)가 된다.
  • "execution(* cwchoiit.springadvanced.aop.order..*(..))"cwchoiit.springadvanced.aop.order 패키지와 그 하위 패키지(..)를 지정하는 AspectJ 포인트컷 표현식이다. 앞으로는 간단히 포인트컷 표현식이라고 하겠다. 
  • 이제 OrderService, OrderRepository의 모든 메서드는 AOP 적용 대상이 된다. 위 포인트컷 조건을 만족하니까.
  • 이렇게만 만들었다고 해서 AOP가 바로 적용되는 것은 아니다. @Aspect 애노테이션이 달린 클래스를 스프링 빈으로 등록해줘야 한다.
참고로, 스프링 AOP는 AspectJ의 문법을 차용하고,  프록시 방식의 AOP를 제공한다. AspectJ를 직접 사용하는 것이 아니다. 스프링 AOP를 사용할 때는 @Aspect 애노테이션을 주로 사용하는데, 이 애노테이션도 AspectJ가 제공하는 애노테이션이다. 

 

또한, @Aspect를 포함한 `org.aspectj` 패키지 관련 기능은 aspectjweaver.jar 라이브러리가 제공하는 기능이다. 앞서, build.gradlespring-boot-starter-aop를 포함했는데 이렇게 하면 스프링의 AOP 관련 기능과 함께 aspectjweaver.jar도 함께 사용할 수 있게 의존 관계에 포함된다. 그런데 스프링에서는 AspectJ가 제공하는 애노테이션이나 관련 인터페이스만 사용하는 것이고 실제 AspectJ가 제공하는 컴파일, 로드타임 위버 등을 사용하는 것은 아니다. 스프링은 지금까지 우리가 학습한 것처럼 프록시 방식의 AOP를 사용한다.

 

AopTest - 테스트 코드

package cwchoiit.springadvanced.aop;

import cwchoiit.springadvanced.aop.aspect.AspectV1;
import cwchoiit.springadvanced.aop.order.OrderRepository;
import cwchoiit.springadvanced.aop.order.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

import static org.assertj.core.api.Assertions.*;

@Slf4j
@SpringBootTest
@Import(AspectV1.class) //@Import 만으로도 빈으로 등록하는것과 동일하다. 주로 @Configuration 에서 추가할 때 자주 사용됐지만, @Import 로도 그 안에 클래스들을 빈으로 등록한다.
public class AopTest {

    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    void aopInfo() {
        log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderService));
        log.info("isAopProxy, orderRepository = {}", AopUtils.isAopProxy(orderRepository));
    }

    @Test
    void success() {
        orderService.orderItem("itemA");
    }

    @Test
    void exception() {
        assertThatThrownBy(() -> orderService.orderItem("ex")).isInstanceOf(IllegalStateException.class);
    }
}
  • @Aspect는 애스팩트라는 표식이지, 컴포넌트 스캔이 되는 것은 아니다! 따라서 AspectV1을 AOP로 사용하려면 반드시 스프링 빈으로 등록을 해야 한다!
  • 스프링 빈으로 등록하는 방법은 여러가지가 있다.
    • @Bean을 사용해서 직접 등록
    • @Component 컴포넌트 스캔을 사용해서 자동 등록
    • @Import 주로 설정 파일을 추가할 때 사용하지만, 이 기능으로 스프링 빈도 등록할 수 있다.

실행 결과 - success()

[log] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String cwchoiit.springadvanced.aop.order.OrderRepository.save(String)
[orderRepository] 실행
  • 어드바이스 기능이 적용된 모습을 확인할 수 있을 것이다. 

 

스프링 AOP 구현2 - 포인트컷 분리

@Around에 포인트컷 표현식을 직접 넣을 수도 있지만, @Pointcut 애노테이션을 사용해서 별도로 분리할 수도 있다.

AspectV2

package cwchoiit.springadvanced.aop.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Slf4j
@Aspect
public class AspectV2 {

    /**
     * 반환 타입은 'void' 여야 한다.
     * 다른 곳에서 이 포인트컷을 사용하려면 public 이어야 하고 이 내부 안에서 사용하는 건 private 이어도 된다.
     * */
    @Pointcut("execution(* cwchoiit.springadvanced.aop.order..*(..))")
    private void allOrder() {} // pointcut signature

    @Around("allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }
}
  • @Pointcut에 포인트컷 표현식을 사용한다.
  • 메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처라 한다.
  • 메서드의 반환 타입은 void여야 한다.
  • 블록 내부는 비워둔다.
  • 포인트컷 시그니처는 allOrder()이다. 이름 그대로 주문과 관련된 모든 기능을 대상으로 하는 포인트컷이다.
  • @Around 어드바이스에서는 포인트컷을 직접 지정해도 되지만, 포인트컷 시그니처를 사용해도 된다. 여기서는 @Around("allOrder()")를 사용한다.
  • private, public 같은 접근 제어자는 내부에서만 사용하면 private을 사용해도 되지만, 다른 애스팩트에서 참고하려면 public을 사용해야 한다.

이렇게 포인트컷을 분리하여 얻는 이점은 다음과 같다.

  • 포인트컷에 의미를 부여할 수 있다. (모든 주문에 대해: allOrder())
  • 여러 어드바이스에서 해당 포인트컷을 가져다가 사용할 수 있다. (쉽게 말해 모듈화가 된다는 것)

이 애스팩트(AspectV2)를 임포트해서 테스트 코드를 돌려도 동일한 결과를 얻는다.

 

AopTest - 테스트 코드

package cwchoiit.springadvanced.aop;

import cwchoiit.springadvanced.aop.aspect.AspectV1;
import cwchoiit.springadvanced.aop.aspect.AspectV2;
import cwchoiit.springadvanced.aop.order.OrderRepository;
import cwchoiit.springadvanced.aop.order.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

import static org.assertj.core.api.Assertions.*;

@Slf4j
@SpringBootTest
@Import(AspectV2.class) //@Import 만으로도 빈으로 등록하는것과 동일하다. 주로 @Configuration 에서 추가할 때 자주 사용됐지만, @Import 로도 그 안에 클래스들을 빈으로 등록한다.
public class AopTest {

    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    void aopInfo() {
        log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderService));
        log.info("isAopProxy, orderRepository = {}", AopUtils.isAopProxy(orderRepository));
    }

    @Test
    void success() {
        orderService.orderItem("itemA");
    }

    @Test
    void exception() {
        assertThatThrownBy(() -> orderService.orderItem("ex")).isInstanceOf(IllegalStateException.class);
    }
}

실행 결과 - success()

[log] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String cwchoiit.springadvanced.aop.order.OrderRepository.save(String)
[orderRepository] 실행

 

스프링 AOP 구현3 - 어드바이스 추가

이번에는 어드바이스를 하나 더 추가해서 좀 더 복잡한 예제를 만들어보자.

앞서, 로그를 출력하는 기능에 추가로 트랜잭션을 적용하는 코드도 추가해보자. 여기서는 진짜 트랜잭션을 실행하는 것은 아니고 기능이 동작하는 것처럼 로그만 남겨보자.

 

AspectV3

package cwchoiit.springadvanced.aop.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Slf4j
@Aspect
public class AspectV3 {

    /**
     * 반환 타입은 'void' 여야 한다.
     * 다른 곳에서 이 포인트컷을 사용하려면 public 이어야 하고 이 내부 안에서 사용하는 건 private 이어도 된다.
     */
    @Pointcut("execution(* cwchoiit.springadvanced.aop.order..*(..))")
    private void allOrder() {
    } // pointcut signature

    // 클래스 이름 패턴이 *Service
    @Pointcut("execution(* *..*Service.*(..))")
    private void allService() {
    }

    @Around("allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }

    @Around("allOrder() && allService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
            Object result = joinPoint.proceed();
            log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
            return result;
        } catch (Exception e) {
            log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
            throw e;
        } finally {
            log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
        }
    }
}
  • allOrder() 포인트컷은 cwchoiit.springadvanced.aop.order 패키지와 그 하위 패키지를 대상으로 한다.
  • allService() 포인트컷은 타입 이름 패턴이 *Service를 대상으로 하는데 쉽게 이야기해서, XxxService처럼 Service로 끝나는 것을 대상으로 한다.
  • 여기서 타입 이름 패턴이라고 한 이유는 클래스, 인터페이스에 모두 적용되기 때문이다.
  • @Around("allOrder() && allService()")
    • 포인트컷은 이렇게 조합할 수도 있다. &&, ||, ! 3가지 조합이 가능하다.
    • cwchoiit.springadvanced.aop.order 패키지와 그 하위 패키지이면서 타입 이름 패턴이 *Service인 것을 대상으로 한다.
    • 결과적으로 doTransaction() 어드바이스는 OrderService에만 적용된다.
    • doLog() 어드바이스는 OrderService, OrderRepository에 모두 적용된다.

 

AopTest - 테스트 코드

package cwchoiit.springadvanced.aop;

import cwchoiit.springadvanced.aop.aspect.AspectV1;
import cwchoiit.springadvanced.aop.aspect.AspectV2;
import cwchoiit.springadvanced.aop.aspect.AspectV3;
import cwchoiit.springadvanced.aop.order.OrderRepository;
import cwchoiit.springadvanced.aop.order.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

import static org.assertj.core.api.Assertions.*;

@Slf4j
@SpringBootTest
@Import(AspectV3.class) //@Import 만으로도 빈으로 등록하는것과 동일하다. 주로 @Configuration 에서 추가할 때 자주 사용됐지만, @Import 로도 그 안에 클래스들을 빈으로 등록한다.
public class AopTest {

    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    void aopInfo() {
        log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderService));
        log.info("isAopProxy, orderRepository = {}", AopUtils.isAopProxy(orderRepository));
    }

    @Test
    void success() {
        orderService.orderItem("itemA");
    }

    @Test
    void exception() {
        assertThatThrownBy(() -> orderService.orderItem("ex")).isInstanceOf(IllegalStateException.class);
    }
}

실행 결과 - success()

[log] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
[트랜잭션 시작] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String cwchoiit.springadvanced.aop.order.OrderRepository.save(String)
[orderRepository] 실행
[트랜잭션 커밋] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
[리소스 릴리즈] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)

전체 실행 순서를 분석해보자.

 

AOP 적용 전

클라이언트 → orderService.orderItem()orderRepository.save()

 

AOP 적용 후

클라이언트 → [doLog()doTransaction()] → orderService.orderItem() → [doLog()] → orderRepository.save()

orderService에는 doLog(), doTransaction() 두가지 어드바이스가 적용되어 있고, orderRepository에는 doLog() 하나의 어드바이스만 적용된 것을 확인할 수 있다. 그런데, 여기에서 로그를 남기는 순서가 [doLog()  doTransaction()] 순서로 작동한다. 만약, 어드바이스가 적용되는 순서를 변경하고 싶으면 어떻게 하면 될까? 예를 들어서 실행 시간을 측정해야 하는데 트랜잭션과 관련된 시간을 제외하고 측정하고 싶다면 [doTransaction()  doLog()] 이렇게 트랜잭션 이후에 로그를 남겨야 할 것이다. 그 방법을 알아보자! 

 

스프링 AOP 구현4 - 포인트컷 참조

다음과 같이 포인트컷으르 공용으로 사용하기 위해 별도의 외부 클래스에 포인트컷들을 모아두어도 된다. 참고로 외부에서 호출할 때는 포인트컷의 접근 제어자를 public으로 열어두어야 한다.

 

Pointcuts

package cwchoiit.springadvanced.aop.aspect;

import org.aspectj.lang.annotation.Pointcut;

public class Pointcuts {

    @Pointcut("execution(* cwchoiit.springadvanced.aop.order..*(..))")
    public void allOrder() {
    }

    @Pointcut("execution(* *..*Service.*(..))")
    public void allService() {
    }

    @Pointcut("allOrder() && allService()")
    public void allOrderAndService() {
    }
}
  • allOrderAndSerivce()allOrder() 포인트컷과 allService() 포인트컷을 조합해서 새로운 포인트컷을 만들 수도 있다는 것을 보여주기 위함이다.

AspectV4Pointcut

package cwchoiit.springadvanced.aop.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Slf4j
@Aspect
public class AspectV4Pointcut {

    @Around("cwchoiit.springadvanced.aop.aspect.Pointcuts.allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }

    @Around("cwchoiit.springadvanced.aop.aspect.Pointcuts.allOrderAndService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
            Object result = joinPoint.proceed();
            log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
            return result;
        } catch (Exception e) {
            log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
            throw e;
        } finally {
            log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
        }
    }
}

 

  • 이 클래스가 실제로 외부의 포인트컷을 가져다가 사용하는 방식이다. @Around 애노테이션은 외부 포인트컷을 참조하면 된다. 살짝 불편한 부분은 패키지명까지 작성해줘야 한다는 것인데 이는 어쩔 수 없다. 문자로 입력해야 하기 때문에.
  • 이러한 에스팩트를 가지고 위에서 사용한 테스트 코드를 수행해도 여전히 동일하게 동작한다.

 

스프링 AOP 구현5 - 어드바이스 순서

어드바이스는 기본적으로 순서를 보장하지 않는다. 순서를 지정하고 싶으면 @Aspect 적용 단위로 org.springframework.core.annotation.Order 애노테이션을 적용해야 한다. 문제는 이것을 어드바이스 단위가 아니라 @Aspect 적용 단위, 즉, 클래스 단위로 적용할 수 있다는 점이다. 그래서 지금처럼 하나의 애스팩트에 여러 어드바이스가 있으면 순서를 보장받을 수 없고 애스팩트를 별도의 클래스로 분리해야 한다.

AspectV5Order

package cwchoiit.springadvanced.aop.aspect;

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.core.annotation.Order;

@Slf4j
public class AspectV5Order {

    @Aspect
    @Order(2)
    public static class LogAspect {
        @Around("cwchoiit.springadvanced.aop.aspect.Pointcuts.allOrder()")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[log] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }

    @Aspect
    @Order(1)
    public static class TxAspect {
        @Around("cwchoiit.springadvanced.aop.aspect.Pointcuts.allOrderAndService()")
        public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
            try {
                log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
                Object result = joinPoint.proceed();
                log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
                return result;
            } catch (Exception e) {
                log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
                throw e;
            } finally {
                log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
            }
        }
    }
}
  • 하나의 애스팩트 안에 있던 어드바이스들을 LogAspect, TxAspect 애스팩트로 각각 분리했다. 이렇게 내부 클래스로 만드는 것도 하나의 방법이다. 
  • 그리고 원하는 순서대로 @Order(..) 애노테이션을 적용하면 된다.

AopTest - 테스트 코드

package cwchoiit.springadvanced.aop;

import cwchoiit.springadvanced.aop.aspect.*;
import cwchoiit.springadvanced.aop.order.OrderRepository;
import cwchoiit.springadvanced.aop.order.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

import static org.assertj.core.api.Assertions.*;

@Slf4j
@SpringBootTest
@Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class}) //@Import 만으로도 빈으로 등록하는것과 동일하다. 주로 @Configuration 에서 추가할 때 자주 사용됐지만, @Import 로도 그 안에 클래스들을 빈으로 등록한다.
public class AopTest {

    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    void aopInfo() {
        log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderService));
        log.info("isAopProxy, orderRepository = {}", AopUtils.isAopProxy(orderRepository));
    }

    @Test
    void success() {
        orderService.orderItem("itemA");
    }

    @Test
    void exception() {
        assertThatThrownBy(() -> orderService.orderItem("ex")).isInstanceOf(IllegalStateException.class);
    }
}
  • @Aspect를 반드시 빈으로 등록해야 한다고 했다. 그렇기 때문에 각각의 내부 클래스를 빈으로 등록해야 한다. 위 @Import처럼.

실행 결과 - success()

 

[트랜잭션 시작] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
[log] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String cwchoiit.springadvanced.aop.order.OrderRepository.save(String)
[orderRepository] 실행
[트랜잭션 커밋] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
[리소스 릴리즈] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
  • 결과를 보면 트랜잭션이 먼저 실행된 모습이다. 이렇게 어드바이스 적용 순서도 지정할 수 있다.

 

 

스프링 AOP 구현6 - 어드바이스 종류

어드바이스는 앞서 살펴본 @Around 외에도 여러가지 종류가 있다.

 

어드바이스 종류

  • @Around메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환값 변환, 예외 변환 등이 가능
  • @Before: 조인 포인트 실행 이전에 실행
  • @AfterReturning: 조인 포인트가 정상 완료 후 실행
  • @AfterThrowing: 메서드가 예외를 던지는 경우 실행
  • @After: 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)

코드로 보면 확실히 이해가 된다. 코드를 보자.

AspectV6Advice

package cwchoiit.springadvanced.aop.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;

@Slf4j
@Aspect
public class AspectV6Advice {

    @Before("cwchoiit.springadvanced.aop.aspect.Pointcuts.allOrderAndService()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("[before] {}", joinPoint.getSignature());
    }

    @AfterReturning(value = "cwchoiit.springadvanced.aop.aspect.Pointcuts.allOrderAndService()", returning = "result")
    public void doAfterReturning(JoinPoint joinPoint, Object result) {
        log.info("[afterReturning] {}, return = {}", joinPoint.getSignature(), result);
    }

    @AfterThrowing(value = "cwchoiit.springadvanced.aop.aspect.Pointcuts.allOrderAndService()", throwing = "ex")
    public void doAfterThrowing(JoinPoint joinPoint, Exception ex) {
        log.info("[afterThrowing] {}, ex = ", joinPoint.getSignature(), ex);
    }

    @After("cwchoiit.springadvanced.aop.aspect.Pointcuts.allOrderAndService()")
    public void doAfter(JoinPoint joinPoint) {
        log.info("[after] {}", joinPoint.getSignature());
    }
}

 

  • 첫번째, 우선 모든 어드바이스는 JoinPoint를 첫번째 파라미터로 받을 수 있다. 생략도 가능하다. 그러나, @Around는 반드시 ProceedingJoinPoint를 받아야 한다. 
    • 그 이유는 @Around 같은 경우 개발자가 직접 타겟을 호출하는 코드를 작성해야 한다. joinPoint.proceed() 이 코드. 그 외 나머지 어드바이스는 개발자가 직접 타겟을 호출하지 않는다. 그래서 @AroundProceedingJoinPoint를 첫번째 파라미터로 받아야 하고 그 외 나머지 어드바이스는 JoinPoint를 받거나 생략할 수 있다. 
  • 두번째, @Before는 실제 타겟을 호출하는 코드를 작성안하지만 @Before의 모든 코드가 다 수행되면 자동으로 호출한다. 물론, 예외가 발생할 경우엔 다음 코드가 호출되지는 않는다.
  • 세번째, @AfterReturning, @AfterThrowing은 각각 실제 타겟 호출의 결과와 에러를 파라미터로 받고 그 파라미터의 이름은 애노테이션에서 작성한 이름과 동일해야 한다.
  • 네번째, @AfterReturning, @AfterThrowing에서 파라미터로 받는 실제 타겟 호출 반환값과 에러의 타입은 해당 타입과 일치하거나 그 상위 타입이어야 한다.
  • 다섯번째, @AfterReturning에서는 @Around와 다르게 실제 타겟 호출 반환값에 대한 변경이 불가능하다. 
    • 이는 단순하게 생각해보면 된다. @Around는 개발자가 직접 실제 타겟을 호출하여 돌려받는 결과를 리턴하는데 그렇기 때문에 리턴값에 변경이 가능한것이고 @AfterReturning은 그렇지 않기 때문에 불가능한 것. 다만, 이 반환값을 가지고 어떤 행동을 취할 순 있다. 그 반환값을 변경하지 못한다는 말이다.
  • 여섯번째, @AroundjoinPoint.proceed()를 여러번 호출할 수도 있다.
  • 일곱번째, @AroundjoinPoint.proceed()를 반드시 호출해야 한다. 그래야 다음 어드바이스 또는 실제 객체를 호출할 수 있다.
  • 여덟번째, @After는 메서드 실행이 정상적이든 예외가 발생하든 상관없이 종료되면 실행된다.
참고로, ProceedingJoinPointJoinPoint의 하위 타입이다.

 

JointPoint 인터페이스의 주요 기능

  • getArgs() → 메서드 인수를 반환한다.
  • getThis() → 프록시 객체를 반환한다.
  • getTarget() → 대상 객체를 반환한다.
  • getSignature() → 조인 포인트(스프링 AOP면 메서드에 한함)에 대한 여러 정보를 반환한다. 
  • toString() → 포인트컷에 대한 설명을 인쇄한다.

ProceedingJoinPoint 인터페이스의 주요 기능

  • proceed() → 다음 어드바이스나 타겟을 호출한다.

 

이 여러 어드바이스의 호출 순서는 다음과 같다.

  • 스프링은 5.2.7 버전부터 동일한 @Aspect 안에서 동일한 조인포인트의 우선순위를 정했다.
  • @Around -> @Before -> @After -> @AfterReturning -> @AfterThrowing
  • 적용되는 순서는 이렇게 적용되지만, 호출 순서와 리턴 순서는 반대라는 점을 알아두자. 위 그림을 보면 이해가 될 것이다.
  • 물론, @Aspect 안에 동일한 종류의 어드바이스가 2개 이상이면 순서가 보장되지 않는다. 이 경우에 보장된 순서를 원한다면 @Aspect를 분리해서 @Order를 적용해서 순서를 적용해야 한다.

 

그럼 왜 @Around만 사용하더라도 모든게 가능한데 이렇게 부분적으로 나뉘어진 어드바이스가 있을까?

이 부분에 대한 답은 이런것들이다. 다음 코드엔 심각한 문제가 있다.

@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
 public void doBefore(ProceedingJoinPoint joinPoint) {
     log.info("[before] {}", joinPoint.getSignature());
 }

 

  • 어떤 문제가 있을까? 바로 @Around 어드바이스인데도 실제 객체를 호출하지 않는다. 이 코드를 작성한 개발자의 의도는 실제 객체를 호출하기 전에 무언가를 로그로 출력하고 싶었던 것 뿐인데 @Around이기 때문에 실제 객체를 반드시 호출해야 한다. 

그럼 이 코드를 보자. 이 코드에는 문제가 전혀 없다.

@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
 public void doBefore(JoinPoint joinPoint) {
     log.info("[before] {}", joinPoint.getSignature());
 }

 

  • @Before이기 때문에 실제 객체를 호출하는 고민을 전혀 할 필요가 없다.

이 말은, @Around는 가장 넓은 기능을 제공하나 실수할 가능성이 있다. 반면, @Before, @After 같은 어드바이스는 기능은 적더라도 실수할 가능성이 적으며 코드가 단순해진다. 그리고 가장 중요한 부분은 이 코드를 작성한 의도가 분명해진다는 것이다. @Before 애노테이션을 본 순간 "아, 이 코드는 실제 객체를 호출하기 전에 무언가를 하기 위해 만들어진 어드바이스구나." 라고 자연스레 생각할 수 있다.

 

즉, 좋은 설계는 제약이 있는 것이다. 제약은 실수의 가능성을 줄여준다. 애시당초 @Around가 아니라 @Before를 사용하면 실제 객체를 호출할 고민조차 할 필요가 없기 때문에 그 부분을 고려하지 않아도 되는것이다. 

 

728x90
반응형
LIST

'Spring Advanced' 카테고리의 다른 글

Mockito를 사용한 스프링 프로젝트 단위 테스트  (4) 2024.09.29
스프링 AOP Part. 3 (포인트컷 문법)  (0) 2024.01.02
스프링 AOP Part. 1  (0) 2023.12.29
AOP와 @Aspect  (0) 2023.12.29
빈 후처리기(BeanPostProcessor)  (2) 2023.12.27
728x90
반응형
SMALL

참고자료

 

스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런

김영한 | 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기

www.inflearn.com

 

AOP 소개 - 핵심 기능과 부가 기능

애플리케이션 로직은 크게 핵심 기능부가 기능으로 나눌 수 있다.

  • 핵심 기능은 해당 객체가 제공하는 고유의 기능이다. 예를 들어서 OrderService의 핵심 기능은 주문 로직이다.
  • 부가 기능은 핵심 기능을 보조하기 위해 제공되는 기능이다. 예를 들어서 로그 추적 로직, 트랜잭션 기능이 있다. 이러한 부가 기능은 단독으로는 사용되지 않고, 핵심 기능과 함께 사용된다. 예를 들어서 로그 추적 기능은 어떤 핵심 기능이 호출되었는지 로그를 남기기 위해 사용한다. 그러니까 부가 기능은 이름 그대로 핵심 기능을 보조하기 위해 존재한다.

 

주문 로직을 실행하기 직전에 로그 추적 기능을 사용해야 하면, 핵심 기능인 주문 로직과 부가 기능인 로그 추적 로직이 하나의 객체 안에 섞여 들어가게 된다. 부가 기능이 필요한 경우 이렇게 둘을 합해서 하나의 로직을 완성한다. 이제 주문 서비스를 실행하면 핵심 기능인 주문 로직과 부가 기능인 로그 추적 로직이 함께 실행된다.

 

여러 곳에서 공통으로 사용하는 부가 기능

 

보통 부가 기능은 여러 클래스에 걸쳐서 함께 사용된다. 예를 들어서 모든 애플리케이션 호출을 로깅해야 하는 요구사항을 생각해보자. 이러한 부가 기능은 횡단 관심사(cross-cutting concerns)가 된다. 쉽게 이야기해서 하나의 부가 기능이 여러 곳에 동일하게 사용된다는 뜻이다.  

 

부가 기능 적용 문제

그런데 이런 부가 기능을 여러 곳에 적용하려면 너무 번거롭다. 예를 들어서 부가 기능을 적용해야 하는 클래스가 100개면 100개 모두에 동일한 코드를 추가해야 한다. 코드의 중복이라도 줄이고자 부가 기능을 별도의 유틸리티 클래스로 만든다고 해도 해당 유틸리티 클래스를 호출하는 코드가 결국 필요하다. 그리고 부가 기능이 구조적으로 단순 호출이 아니고 try - catch - finally 같은 구조가 필요하다면 더욱 복잡해진다. 더 큰 문제는 수정이다. 만약, 부가 기능에 수정이 발생하면, 100개의 클래스 모두를 하나씩 찾아가면서 수정해야 한다. 여기에 추가로 부가 기능이 적용되는 위치를 변경한다면 어떻게 될까? 예를 들어서 부가 기능을 모든 컨트롤러, 서비스, 리포지토리에 적용했다가, 로그가 너무 많이 남아서 서비스 계층에만 적용한다고 수정해야하면 어떻게 될까? 또 수 많은 코드를 고쳐야 할 거이다. 

 

요약하자면, 다음과 같은 문제점이 발생한다.

  • 부가 기능을 적용할 때 아주 많은 반복이 필요하다.
  • 부가 기능이 여러 곳에 퍼져서 중복 코드를 만들어낸다.
  • 부가 기능을 변경할 때 중복 때문에 많은 수정이 필요하다.
  • 부가 기능의 적용 대상을 변경할 때 많은 수정이 필요하다.

소프트웨어 개발에서 변경 지점은 하나가 될 수 있도록 잘 모듈화를 해야 한다. 그런데 부가 기능처럼 특정 로직을 애플리케이션 전반에 적용하는 문제는 일반적인 OOP 방식으로는 해결이 어렵다.


AOP 소개 - 애스펙트

핵심 기능과 부가 기능을 분리

누군가는 이러한 부가 기능 도입의 문제점들을 해결하기 위해 오랜기간 고민해왔다. 그 결과 부가 기능을 핵심 기능에서 분리하고 한 곳에서 관리하도록 했다. 그리고 해당 부가 기능을 어디에 적용할지 선택하는 기능도 만들었다. 이렇게 부가 기능과 부가 기능을 어디에 적용할지 선택하는 기능을 합해서 하나의 모듈로 만들었는데 이것이 바로 애스팩트(aspect)이다. 애스팩트는 쉽게 이야기해서 부가 기능과 해당 부가 기능을 어디에 적용할지 정의한 것이다. 예를 들어서 "로그 출력 기능을 모든 컨트롤러에 적용해라" 라는 것이 정의되어 있다.

 

그렇다 바로 우리가 이전에 알아본 @Aspect 바로 그것이다. 그리고 스프링이 제공하는 어드바이저도 어드바이스(부가 기능)과 포인트컷(적용 대상)을 가지고 있어서 개념상 하나의 애스팩트이다. 애스팩트는 우리말로 해석하면 관점이라는 뜻인데, 이름 그대로 애플리케이션을 바라보는 관점을 하나하나의 기능에서 횡단 관심사(cross-cutting concerns) 관점으로 달리 보는 것이다. 이렇게 애스팩트를 사용한 프로그래밍 방식을 관점 지향 프로그래밍(AOP, Aspect-Oriented Programming)이라 한다.

 

참고로, AOP는 OOP를 대체하기 위한 것이 아니라 횡단 관심사를 깔끔하게 처리하기 어려운 OOP의 부족한 부분을 보조하는 목적으로 개발되었다.

 

 

 

AspectJ 프레임워크

AOP의 대표적인 구현으로 AspectJ 프레임워크가 있다. 스프링은 AOP를 지원하고 있으나 대부분 AspectJ의 문법을 차용하고 AspectJ가 제공하는 기능의 일부만 제공한다. 그러나, 결론은 스프링 AOP를 사용하면 실무에서 어지간한 모든 기능을 다 사용할 수 있다. 이는 이후에 차차 알아보도록 하자.

 

AOP 적용 방식

AOP를 사용하면 핵심 기능과 부가 기능이 코드상 완전히 분리되어서 관리된다. 그렇다면 AOP를 사용할 때 부가 기능 로직은 어떤 방식으로 실제 로직에 추가될 수 있을까?

 

크게 3가지 방법이 있다.

  • 컴파일 시점
  • 클래스 로딩 시점
  • 런타임 시점(프록시)

컴파일 시점

.java 소스 코드를 컴파일러를 사용해서 .class를 만드는 시점에 부가 기능 로직을 추가할 수 있다. 이때는 AspectJ가 제공하는 특별한 컴파일러를 통해 이루어진다. .class를 디컴파일 해보면 에스팩트 관련 호출 코드가 들어간다. 쉽게 말해서 부가 기능 코드가 핵심 기능이 있는 컴파일된 코드 주변에 실제로 붙어 버린다고 생각하면 된다. AspectJ 컴파일러는 Aspect를 확인해서 해당 클래스가 적용 대상인지 먼저 확인한 후 적용 대상인 경우에 부가 기능 로직을 적용한다. 이렇게 원본 로직에 부가 기능 로직이 추가되는 것을 위빙(Weaving)이라고 한다. 이 방법의 단점은 특별한 컴파일러가 필요하다는 것 그 자체이고 사용하기에 굉장히 복잡하다. 그래서 결론적으로 이 방법은 잘 사용하지 않는다.

 

클래스 로딩 시점

 

자바를 실행하면 자바 언어는 .class 파일을 JVM 내부의 클래스 로더에 보관한다. 이때 중간에서 .class 파일을 조작한 다음 JVM에 올릴 수 있다. 자바 언어는 .class를 JVM에 저장하기 전에 조작할 수 있는 기능을 제공한다. 자세한 내용을 알려면 java instrumentation을 검색해 보면 된다. 참고로 수많은 모니터링 툴들이 이 방식을 사용한다. 이 시점에 에스팩트를 적용하는 것을 로드 타임 위빙이라고 한다. 이 방법의 단점은 자바를 실행할 때 특별한 옵션을 통해 클래스 로더 조작기를 지정해야 하는데 이 부분이 번거롭고 운영하기에 단점이 있다. 그래서 이 방법 역시 스프링 AOP에서 잘 사용되지 않는다. 

 

런타임 시점 ⭐️

런타임 시점은 컴파일도 다 끝나고, 클래스 로더에 클래스도 다 올라가서 이미 자바가 실행되고 난 다음을 말한다. 자바의 메인(main) 메서드가 이미 실행된 다음이다. 따라서 자바 언어가 제공하는 범위 안에서 부가 기능을 적용해야 한다. 스프링과 같은 컨테이너의 도움을 받고 프록시와 DI, 빈 포스트 프로세서 같은 개념들을 총 동원해야 한다. 이렇게 하면 최종적으로 프록시를 통해 스프링 빈에 부가 기능을 적용할 수 있다. 그렇다. 지금까지 우리가 학습한 것이 바로 프록시 방식의 AOP이다.

프록시를 사용하기 때문에 AOP 기능에 일부 제약이 있다. 하지만 특별한 컴파일러나 자바를 실행할 때 복잡한 옵션과 클래스 로더 조작기를 설정하지 않아도 된다. 스프링만 있으면 얼마든지 AOP를 적용할 수 있다.

 

부가 기능이 적용되는 차이를 정리하면 다음과 같다.

  • 컴파일 시점: 실제 대상 코드에 애스팩트를 통한 부가 기능 호출 코드가 포함된다. AspectJ를 직접 사용해야 한다.
  • 클래스 로딩 시점: 실제 대상 코드에 애스팩트를 통한 부가 기능 호출 코드가 포함된다. AspectJ를 직접 사용해야 한다.
  • 런타임 시점: 실제 대상 코드는 그대로 유지된다. 대신에 프록시를 통해 부가 기능이 적용된다. 따라서 항상 프록시를 통해야 부가 기능을 사용할 수 있다. 스프링 AOP는 이 방식을 사용한다.

 

AOP 적용 위치 (조인 포인트)

AOP는 지금까지 학습한 메서드 실행 위치뿐만 아니라 다음과 같은 다양한 위치에 적용할 수 있다. 

  • 생성자
  • 필드 값 접근
  • static 메서드 접근
  • 메서드 실행

이렇게 AOP를 적용할 수 있는 지점을 조인 포인트(Join point)라 한다. AspectJ를 사용해서 컴파일 시점과 클래스 로딩 시점에 적용하는 AOP는 바이트코드를 실제 조작하기 때문에 해당 기능을 모든 지점에 다 적용할 수 있다. 그러나, 프록시 방식을 사용하는 스프링 AOP는 메서드 실행 지점에만 AOP를 적용할 수 있다. 잘 생각해보자, 프록시는 실제 객체를 어떻게 호출할 수 있지? 메서드를 호출해서만 가능하다. 즉, 메서드 실행 지점에만 AOP를 적용할 수 밖에 없다. 프록시 방식을 사용하는 스프링 AOP는 스프링 컨테이너가 관리할 수 있는 스프링 빈에만 AOP를 적용할 수 있다. 스프링이 자동으로 등록해주는 빈 포스트 프로세서를 사용해야 하니까.

 

참고로, 스프링은 AspectJ의 문법을 차용하고 프록시 방식의 AOP를 적용한다. AspectJ를 직접 사용하는 것이 아니다.
중요!
그러면, 이러한 의문이 들 수 있다. "스프링 AOP 방식인 프록시보다 그냥 AspectJ를 사용하면 더 좋은 거 아니야?"라고 생각할 수 있다. 그러나, AspectJ 프레임워크를 사용하기 위해 공부해야 할 내용이 어마어마하게 많고 설정 방법도 굉장히 복잡하다고 알려져 있다. 반면 스프링 AOP는 별도의 추가 자바 설정 없이 스프링만 있으면 편리하게 AOP를 사용할 수 있고 실무에서는 스프링이 제공하는 AOP 기능만 사용해도 대부분의 문제를 해결할 수 있다. 게다가 개발자가 직접 해야할 일은 결국, 포인트컷과 어드바이스 만드는 것 밖에 없다 사실. 나머지는 스프링이 알아서 다 해주니까. 그러니 스프링 AOP가 제공하는 기능을 학습하는 것에 집중하자.

 

AOP 용어 정리

 

 

  • 조인 포인트(Join Point)
    • 어드바이스가 적용될 수 있는 위치. 메서드 실행, 생성자 호출, 필드 값 접근, static 메서드 접근 같은 프로그램 실행 중 지점
    • 조인 포인트는 추상적인 개념이다. AOP를 적용할 수 있는 모든 지점이라 생각하면 된다.
    • 스프링 AOP는 프록시 방식을 사용하므로 조인 포인트는 항상 메서드 실행 지점으로 제한된다.
  • 포인트컷(Pointcut)
    • 조인 포인트 중에서 어드바이스가 적용될 위치를 선별하는 기능
    • 주로 AspectJ 표현식을 사용해서 지정
    • 프록시를 사용하는 스프링 AOP는 메서드 실행 지점만 포인트컷으로 선별 가능
  • 타겟(Target)
    • 어드바이스를 받는 객체(프록시가 참조하는 실제 객체를 말한다). 포인트컷으로 결정된다.
  • 어드바이스(Advice)
    • 부가 기능
    • Around, Before, After와 같은 다양한 종류의 어드바이스가 있다.
  • 에스팩트(Aspect)
    • 어드바이스 + 포인트컷을 모듈화 한 것
    • @Aspect를 생각하면 된다.
    • 여러 어드바이스와 포인트 컷이 함께 존재
  • 어드바이저(Advisor)
    • 하나의 어드바이스와 하나의 포인트 컷으로 구성
    • 스프링 AOP에서만 사용되는 특별한 용어
  • 위빙(Weaving)
    • 포인트컷으로 결정한 타겟의 조인 포인트에 어드바이스를 적용하는 것
    • 위빙을 통해 핵심 기능 코드에 영향을 주지 않고 부가 기능을 추가할 수 있음
    • AOP 적용을 위해 에스팩트를 객체에 연결한 상태
      • 컴파일 타임
      • 로드 타임
      • 런타임(스프링 AOP는 런타임이고 프록시 방식이다)
  • AOP 프록시
    • AOP 기능을 구현하기 위해 만든 프록시 객체. 스프링에서 AOP 프록시는 JDK 동적 프록시 또는 CGLIB 프록시이다.

 

정리를 하자면

지금까지가 AOP, 스프링 AOP의 개념이었다. 그러니까 결론은 AOP는 여러 컴포넌트 단위에서 공통적으로 가지는 공통의 관심사를 처리하기 위한 방법으로 고안된 개념이다. 공통의 관심사에 대한 코드를 작성하기 위해 모든 컴포넌트(객체)에 같은 코드를 작성하는 것은 비효율적이고 중복 코드가 발생하며 유지보수에 적합하지 않기 때문에 모듈화 하여 모듈 하나를 관리하는 방식이 AOP라고 생각하면 될 것 같다. 스프링 AOP는 프록시 방식을 사용한다고 했고 그렇기에 조인 포인트는 메서드 실행 지점으로 제한된다. 그러나, 그렇다 한들 대부분의 문제를 해결할 수 있기 때문에 스프링 AOP를 사용하는 것만으로 충분하다. 이제 실제로 AOP를 구현해보자.

728x90
반응형
LIST

'Spring Advanced' 카테고리의 다른 글

스프링 AOP Part. 3 (포인트컷 문법)  (0) 2024.01.02
스프링 AOP Part.2  (0) 2024.01.02
AOP와 @Aspect  (0) 2023.12.29
빈 후처리기(BeanPostProcessor)  (2) 2023.12.27
Advisor, Advice, Pointcut  (0) 2023.12.15
728x90
반응형
SMALL
SMALL

참고자료

 

스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런

김영한 | 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기

www.inflearn.com

 

@Aspect 프록시 - 적용

스프링 애플리케이션에 프록시를 적용하려면 포인트컷과 어드바이스로 구성되어 있는 어드바이저(Advisor)를 만들어서 스프링 빈으로 등록하면 된다. 그러면 나머지는 앞서 배운 AnnotationAwareAspectJAutoProxyCreator 가 모두 자동으로 처리해준다. AnnotationAwareAspectJAutoProxyCreator는 스프링 빈으로 등록된 어드바이저들을 찾고, 스프링 빈들에 자동으로 프록시를 적용해준다.

 

스프링은 @Aspect 애노테이션으로 매우 편리하게 포인트컷과 어드바이스로 구성되어 있는 어드바이저 생성 기능을 지원한다. 지금까지 어드바이저를 직접 만들었던 부분을 @Aspect 애노테이션을 사용해서 만들어보자. 그러니까, @Aspect를 사용해서 빈으로 등록한 것들도 저 AnnotationAwareAspectJAutoProxyCreator이 녀석이 다 찾아서 프록시 적용 여부를 확인하고 적용 대상에는 프록시를 입혀 빈으로 등록해준다. 

 

참고로, @Aspect는 관점 지향 프로그래밍(AOP)을 가능하게 하는 AspectJ 프로젝트에서 제공하는 애노테이션이다. 스프링은 이것을 차용해서 프록시를 통한 AOP를 가능하게 한다. AOP와 AspectJ 관련된 자세한 내용은 이후에 나온다. 지금은 프록시에 초점을 맞추자. 우선 이 애노테이션을 사용해서 스프링이 편리하게 프록시를 만들어준다고 생각하면 된다.

 

LogTraceAspect

package cwchoiit.springadvanced.proxy.config.v6_aop.aspect;

import cwchoiit.springadvanced.trace.TraceStatus;
import cwchoiit.springadvanced.trace.logtrace.LogTrace;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Slf4j
@Aspect
public class LogTraceAspect {

    private final LogTrace trace;

    public LogTraceAspect(LogTrace trace) {
        this.trace = trace;
    }

    @Around("execution(* cwchoiit.springadvanced.proxy.app..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        TraceStatus status = null;
        Signature signature = joinPoint.getSignature();
        try {
            String message = signature.toShortString();
            status = trace.begin(message);
            Object result = joinPoint.proceed();
            trace.end(status);
            return result;
        } catch (Exception e) {
            trace.exception(status, e);
            throw e;
        }
    }
}
  • @Aspect → 애노테이션 기반 프록시를 적용할 때 필요하다.
  • @Around("execution(* cwchoiit.springadvanced.proxy.app..*(..))")
    • * → 모든 반환 타입을 의미. (원래 맨 앞에 접근 제어자가 가장 먼저인데 생략이 가능하다. 그래서 한 개만 있는 경우 반환 타입을 나타낸다)
    • cwchoiit.springadvanced.proxy.app.. → cwchoiit.springadvanced.proxy.app 패키지와 그 하위 모든 패키지(..)를 나타낸다.
    • *(..) → 모든 메서드(*)의 모든 파라미터(..)를 의미한다. 
  • 그래서 결론적으로 * cwchoiit.springadvanced.proxy.app..*(..) 은 해당 패키지부터 그 하위 모든 패키지의 모든 반환 타입의 모든 메서드의 어떠한 파라미터도 상관없이 해당되는 메서드들을 가리킨다.
  • 그리고 해당 메서드의 파라미터를 보면 ProceedingJoinPoint joinpoint를 받는다. 이건 예전 포스팅에서 어드바이스를 만들 때 MethodInvocation invocation과 유사하다. 내부에 실제 호출 대상, 전달 인자, 그리고 어떤 객체와 어떤 메서드가 호출되었는지 정보가 포함되어 있다.
  • 그래서 joinpoint.proceed()를 호출하는 게 실제 호출 대상(target)을 호출하는 것이다.

 

@Aspect 프록시 - 설명

우선 스프링의 AnnotationAwareAspectJAutoProxyCreator는 2가지 일을 한다.

  • 스프링 빈으로 등록된 Advisor를 가져와서 프록시 적용 대상 판단 후 적용
  • 스프링 빈으로 등록된 @Aspect를 보고 어드바이저로 변환하고 프록시 적용 대상 판단 후 적용

@Aspect 빈을 통해 어드바이저를 만드는 과정

 

 

1. 생성: 스프링이 빈으로 등록될 객체를 생성한다.(@Bean, 컴포넌트 스캔 모두 포함)

2. 전달: 생성된 객체를 빈 저장소에 등록하기 전 빈 후처리기에 전달한다.

3. 모든 Advisor 빈 조회: 스프링 컨테이너에서 Advisor 빈을 모두 조회한다.

3-1. 모든 @Aspect 빈 조회: @Aspect 어드바이저 빌더 내부에 저장된 Advisor를 모두 조회한다. (사실 이 과정 전에 먼저 빈으로 등록된 @Aspect 애노테이션이 달려있는 모든 클래스를 찾고 어드바이저로 만든 후 @Aspect 어드바이저 빌더에 저장하는 과정이 생략되어 있다)

4. 프록시 적용 대상 체크: 앞서 3, 3-1 에서 조회한 Advisor에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다. 이 때 객체의 클래스 정보는 물론이고, 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 매칭해본다. 그래서 조건이 하나라도 만족하면 프록시 적용 대상이 된다. 예를 들어, 메서드 하나만 포인트컷 조건에 만족해도 프록시 적용 대상이 된다.

5. 프록시 생성: 프록시 적용 대상이면 프록시를 생성하고 프록시를 반환한다. 그래서 프록시를 스프링 빈으로 등록한다. 만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록한다.

6. 빈 등록: 반환된 객체는 스프링 빈으로 등록된다.

 

정리를 하자면

@Aspect를 사용해서 이전 작업과는 비교도 안되게 편리하게 프록시를 적용할 수 있다. @Aspect 애노테이션이 달린 클래스를 빈으로 등록하면 스프링이 자동으로 만들어주는 AnnotationAwareAspectJAutoProxyCreator 빈 후처리기를 통해 알아서 포인트컷의 조건을 기반으로 프록시를 만들어준다. 실무에서는 프록시를 적용하려고 하면 대부분 이 방식을 사용한다. 

 

지금까지 우리가 진행한 애플리케이션 전반에 로그를 남기는 기능은 특정 기능 하나에 관심이 있는 기능이 아니다. 애플리케이션의 여러 기능들 사이에 걸쳐서 들어가는 공통 관심사이다. 이것을 바로 횡단 관심사(cross-cutting concerns)라고 한다. 우리가 지금까지 진행한 방법이 이렇게 여러곳에 걸쳐 있는 횡단 관심사의 문제를 해결하는 방법이었다. 

 

지금까지 프록시를 사용해서 이렇게 횡단 관심사를 어떻게 해결하는지 점진적으로 매우 깊이있게 학습하고 기반을 다져두었다. 이제 이 기반을 바탕으로 이러한 횡단 관심사를 전문으로 해결하는 스프링 AOP에 대해 본격적으로 알아보자!

728x90
반응형
LIST

'Spring Advanced' 카테고리의 다른 글

스프링 AOP Part.2  (0) 2024.01.02
스프링 AOP Part. 1  (0) 2023.12.29
빈 후처리기(BeanPostProcessor)  (2) 2023.12.27
Advisor, Advice, Pointcut  (0) 2023.12.15
CGLIB,스프링이 지원하는 ProxyFactory  (0) 2023.12.14
728x90
반응형
SMALL
SMALL

참고자료

 

스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런

김영한 | 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기

www.inflearn.com

 

@Aspect 애노테이션을 붙이고 그 안에 @Around, @AfterReturning, @Before, ... 등등의 애노테이션만 달아주면 어떻게 여기서 작성한 포인트컷에 해당하는 객체들이 프록시로 등록되고 그 안에 로직이 어드바이스가 될까? 즉, 결국 그 메서드가 어떻게 Advisor가 되어 프록시 팩토리에 의해 프록시를 만들까? 정답은 이 빈 후처리기다. 

 

빈 후처리기

스프링에서 @Bean이나 컴포넌트 스캔으로 스프링 빈을 등록하면 스프링은 대상 객체를 생성하고 스프링 컨테이너 내부의 빈 저장소에 등록한다. 그리고 이후에는 스프링 컨테이너를 통해 등록한 스프링 빈을 조회해서 사용하면 된다.

 

스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하고 싶다면 빈 후처리기를 사용하면 된다.  

BeanPostProcessor는 번역하면 빈 후처리기로, 이름 그대로 빈을 생성한 후에 무언가를 처리하는 용도로 사용한다.

 

빈 후처리기는 강력하다. 객체를 조작하는게 가능하고 완전히 다른 객체로 바꿔치기 하는 것도 가능하다. 빈 후처리기 과정을 자세히 살펴보자.

 

빈 후처리기 과정

 

1. 생성: 스프링 빈 대상이 되는 객체를 생성한다 (@Bean, 컴포넌트 스캔 모두 포함)

2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.

3. 후 처리 작업: 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바꿔치기 할 수 있다.

4. 등록: 빈 후처리기는 빈을 반환한다. 전달된 빈을 그대로 반환하면 해당 빈이 등록되고, 바꿔치기 하면 다른 객체가 빈 저장소에 등록된다.

 

여기서 '3. 후 처리 작업'을 보면 스프링 빈 객체를 조작 또는 바꿔치기 한다고 되어 있는데 이 말은 무슨 말일까? 다음 그림을 보자.

 

빈 후처리기에서 객체 A를 객체 B로 바꿔버린 모습을 볼 수 있다. 그리고 그 바꾼 객체 B를 스프링 빈 저장소에 전달하면 최초 객체 A가 객체 B로 최종 등록된다. 이것을 객체를 조작 또는 바꿔치기한다 말한다.

 

빈 후처리기를 사용하려면 BeanPostProcessor 인터페이스를 구현하고, 스프링 빈으로 등록하면 된다.

BeanPostProcessor 인터페이스는 두 개의 메서드를 제공한다. 

  • postProcessBeforeInitialization: 객체 생성 이후에 @PostConstruct 같은 초기화가 발생하기 전에 호출되는 포스트 프로세서
  • postProcessAfterInitialization: 객체 생성 이후에 @PostConstruct 같은 초기화가 발생한 다음에 호출되는 포스트 프로세서

이 빈후처리기를 통해 특정 객체를 다른 객체로 변경해버리는 예시 코드를 작성해보자.

 

빈 후처리기 - 예제 코드1

BeanPostProcessorTest

package cwchoiit.springadvanced.proxy.postprocessor;

import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import static org.assertj.core.api.Assertions.*;

@Slf4j
public class BasicTest {

    @Test
    void basicConfig() {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(BasicConfig.class);

        A a = applicationContext.getBean("beanA", A.class);
        a.helloA();

        assertThatThrownBy(() -> applicationContext.getBean(B.class))
                .isInstanceOf(NoSuchBeanDefinitionException.class);
    }

    @Configuration
    static class BasicConfig {
        @Bean(name = "beanA")
        public A a() {
            return new A();
        }
    }

    static class A {
        public void helloA() {
            log.info("helloA");
        }
    }

    static class B {
        public void helloB() {
            log.info("helloB");
        }
    }
}
  • new AnnotationConfigApplicationContext(BasicConfig.class) → 스프링 컨테이너를 생성하면서 BasicConfig.class를 넘겨주었다. BasicConfig.class 설정 파일은 스프링 빈으로 등록된다.

빈 등록

@Configuration
static class BasicConfig {
    @Bean(name = "beanA")
    public A a() {
        return new A();
    }
}
  • beanA라는 이름으로 A 객체를 스프링 빈으로 등록했다.

빈 조회

A a = applicationContext.getBean("beanA", A.class);
  • beanA라는 이름으로 A 타입의 스프링 빈을 찾는다. 당연히 잘 찾아질 것이다.
assertThatThrownBy(() -> applicationContext.getBean(B.class))
                .isInstanceOf(NoSuchBeanDefinitionException.class);
  • B 타입의 빈은 등록한 적이 없기 때문에 NoSuchBeanDefinitionException이 발생한다.

빈 후처리기 - 예제 코드2

이번에는 빈 후처리기를 통해서 A 객체를 B 객체로 바꿔치기 해보자. 

 

BeanPostProcessor - 스프링 제공

package org.springframework.beans.factory.config;

import org.springframework.beans.BeansException;
import org.springframework.lang.Nullable;

public interface BeanPostProcessor {
    @Nullable
    default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Nullable
    default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}
  • 빈 후처리기를 사용하려면, BeanPostProcessor 인터페이스를 구현하고, 스프링 빈으로 등록하면 된다.
  • 참고로, 스프링 부트를 사용하면 이미 수많은 빈 후처리기가 자동 등록된 상태이다. 우리도 필요하면 추가적으로 빈 후처리기를 이렇게 등록할 수 있다.
  • postProcessBeforeInitialization: 객체 생성 이후에 @PostConstruct 같은 초기화가 발생하기 전에 호출되는 포스트 프로세서이다.
  • postProcessAfterInitialization: 객체 생성 이후에 @PostConstruct 같은 초기화가 발생한 다음에 호출되는 포스트 프로세서이다.

BeanPostProcessorTest

package cwchoiit.springadvanced.proxy.postprocessor;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

@Slf4j
public class BeanPostProcessorTest {

    @Test
    void basicConfig() {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);

        B b = applicationContext.getBean("beanA", B.class);
        b.helloB();

        assertThatThrownBy(() -> applicationContext.getBean(A.class))
                .isInstanceOf(NoSuchBeanDefinitionException.class);
    }

    @Configuration
    static class BeanPostProcessorConfig {
        @Bean(name = "beanA")
        public A a() {
            return new A();
        }

        @Bean
        public AToBPostProcessor toBPostProcessor() {
            return new AToBPostProcessor();
        }
    }

    static class A {
        public void helloA() {
            log.info("helloA");
        }
    }

    static class B {
        public void helloB() {
            log.info("helloB");
        }
    }

    static class AToBPostProcessor implements BeanPostProcessor {
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            log.info("beanName = {}, bean = {}", beanName, bean);
            if (bean instanceof A) {
                return new B();
            }
            return bean;
        }
    }
}
  • 이번엔 빈 후처리기를 직접 구현해봤다. AToBPostProcessor는 인터페이스인 BeanPostProcessor를 구현하고, 스프링 빈으로 등록하면 스프링 컨테이너가 빈 후처리기로 인식하고 동작한다. 
  • 이 빈 후처리기는 A 객체를 새로운 B 객체로 바꿔치기한다. 파라미터로 넘어오는 빈 객체가 A의 인스턴스이면 새로운 B 객체를 생성해서 반환한다. 여기서 A 대신에 반환된 값인 B가 스프링 컨테이너에 등록된다. 다음 실행결과를 보면 beanName = beanA, bean = A 객체의 인스턴스가 빈 후처리기에 넘어온 것을 확인할 수 있다.

실행 결과

beanName = beanA, bean = cwchoiit.springadvanced.proxy.postprocessor.BeanPostProcessorTest$A@50916ff4
 ..B - hello B
  • 실행 결과를 보면, 최종적으로 beanA 라는 스프링 빈 이름에 A 객체 대신에 B 객체가 등록된 것을 확인할 수 있다. A는 스프링 빈으로 등록조차 되지 않는다. 이게 빈 후처리기이다.

정리

이렇게 빈 후처리기를 통해 스프링이 빈 저장소(스프링 컨테이너)에 등록할 객체를 강력한 방식으로 조작하고 변경할 수 있다. 여기서 조작이란 메서드를 호출함을 의미한다. 일반적으로 스프링 컨테이너가 등록하는, 특히 컴포넌트 스캔의 대상이 되는 빈들은 중간에 조작할 방법이 없는데 빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을 중간에 조작할 수 있게 된다. 이 말은 빈 객체를 프록시로 교체하는 것도 가능하다는 뜻이다. 약간 느낌이 애노테이션 프로세싱과 비슷하다. 중간에 후킹을 할 수 있는 어떤 포인트를 만들어주는 것이 유사하다. 

 

실제로 스프링은 AOP를 구현할 때 빈 후처리기를 통해 컴포넌트 스캔으로 등록되는 빈 중 프록시로 만들어져야 하는 객체를 포인트컷을 통해 찾아 프록시로 변경하여 등록해준다. 

 

참고로, @PostConstruct는 스프링 빈 생성 이후에 빈을 초기화 하는 역할을 한다. 그런데 생각해보면 빈의 초기화라는 것이 단순히 @PostConstruct 애노테이션이 붙은 초기화 메서드를 한번 호출만 하면 된다. 쉽게 이야기해서 생성된 빈을 한번 조작하는 것이다. 따라서 빈을 조작하는 행위를 하는 적절한 빈 후처리기가 있으면 될 것 같다. 스프링은 CommonAnnotationBeanPostProcessor라는 빈 후처리기를 자동으로 등록하는데, 여기에서 @PostConstruct 애노테이션이 붙은 메서드를 호출한다.

 

빈 후처리기 - 적용

빈 후처리기를 사용해서 실제 객체 대신 프록시를 스프링 빈으로 등록해보자. 이렇게 하면 수동으로 등록하는 빈은 물론이고, 컴포넌트 스캔을 사용하는 빈까지 모두 프록시를 적용할 수 있다. 더 나아가서 설정 파일에 있는 수많은 프록시 생성 코드도 한번에 제거할 수 있다.

 

일단 빈을 프록시로 바꿔치기하는 우리만의 빈 후처리기를 만들어보자.

PackageLogTracePostProcessor

package cwchoiit.springadvanced.proxy.config.v4_postprocessor.postprocessor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;

@Slf4j
public class PackageLogTracePostProcessor implements BeanPostProcessor {

    private final String basePackage;
    private final Advisor advisor;

    public PackageLogTracePostProcessor(String basePackage, Advisor advisor) {
        this.basePackage = basePackage;
        this.advisor = advisor;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        log.info("param beanName = {}, bean = {}", beanName, bean.getClass());

        String packageName = bean.getClass().getPackageName();
        if (!packageName.startsWith(this.basePackage)) {
            return bean;
        }

        ProxyFactory proxyFactory = new ProxyFactory(bean);
        proxyFactory.addAdvisor(this.advisor);
        Object proxy = proxyFactory.getProxy();
        log.info("target = {}, proxy = {}", bean.getClass(), proxy.getClass());
        return proxy;
    }
}
  • 이 빈 후처리기는 원본 객체를 프록시 객체로 변환하는 역할을 한다. 이때 프록시 팩토리를 사용한다. 프록시 팩토리는 advisor가 필수이기 때문에 이 부분은 외부에서 주입받도록 했다. 
  • 모든 스프링 빈들에 프록시를 적용할 필요도 해서도 안된다. 여기서는 특정 패키지와 그 하위에 위치한 스프링 빈들만 프록시를 적용한다. 그러기 위해 외부에서 특정 패키지 경로를 주입받는다. 외부에서 프록시가 적용되길 원하는 특정 패키지 경로를 주입받고 postProcessAfterInitialization 메서드에서 이 패키지 또는 이 패키지 하위 경로인지 체크한다. 

BeanPostProcessorConfig

package cwchoiit.springadvanced.proxy.config.v4_postprocessor;

import cwchoiit.springadvanced.proxy.config.AppV1Config;
import cwchoiit.springadvanced.proxy.config.AppV2Config;
import cwchoiit.springadvanced.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import cwchoiit.springadvanced.proxy.config.v4_postprocessor.postprocessor.PackageLogTracePostProcessor;
import cwchoiit.springadvanced.trace.logtrace.LogTrace;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Slf4j
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class BeanPostProcessorConfig {

    @Bean
    public PackageLogTracePostProcessor packageLogTracePostProcessor(LogTrace trace) {
        return new PackageLogTracePostProcessor("cwchoiit.springadvanced.proxy.app", getAdvisor(trace));
    }

    private Advisor getAdvisor(LogTrace trace) {
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");
        LogTraceAdvice logTraceAdvice = new LogTraceAdvice(trace);

        return new DefaultPointcutAdvisor(pointcut, logTraceAdvice);
    }
}
  • 빈 후처리기를 구현했으면 스프링 빈으로 등록해야 한다. 빈 후처리기는 스프링 빈으로 등록만 하면 자동으로 동작한다. 여기에 프록시를 적용할 패키지 정보와 어드바이저를 넘겨준다. 
  • 이제 프록시를 생성하는 코드가 설정 파일에는 필요가 없다. 순수한 빈 등록만 고민하면 된다. 프록시를 생성하고 프록시를 스프링 빈으로 등록하는 것은 빈 후처리기가 모두 처리해준다.

실행 결과

...
2025-01-09T16:03:55.688+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration, bean = class org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration
2025-01-09T16:03:55.688+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.http.client.HttpClientAutoConfiguration, bean = class org.springframework.boot.autoconfigure.http.client.HttpClientAutoConfiguration
2025-01-09T16:03:55.689+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = spring.http.client-org.springframework.boot.autoconfigure.http.client.HttpClientProperties, bean = class org.springframework.boot.autoconfigure.http.client.HttpClientProperties
2025-01-09T16:03:55.691+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = clientHttpRequestFactoryBuilder, bean = class org.springframework.boot.http.client.JdkClientHttpRequestFactoryBuilder
2025-01-09T16:03:55.692+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = clientHttpRequestFactorySettings, bean = class org.springframework.boot.http.client.ClientHttpRequestFactorySettings
2025-01-09T16:03:55.693+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = spring.info-org.springframework.boot.autoconfigure.info.ProjectInfoProperties, bean = class org.springframework.boot.autoconfigure.info.ProjectInfoProperties
2025-01-09T16:03:55.693+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration, bean = class org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration
2025-01-09T16:03:55.694+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration, bean = class org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration
2025-01-09T16:03:55.694+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = spring.sql.init-org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties, bean = class org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties
2025-01-09T16:03:55.695+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$ThreadPoolTaskSchedulerBuilderConfiguration, bean = class org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$ThreadPoolTaskSchedulerBuilderConfiguration
2025-01-09T16:03:55.695+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = spring.task.scheduling-org.springframework.boot.autoconfigure.task.TaskSchedulingProperties, bean = class org.springframework.boot.autoconfigure.task.TaskSchedulingProperties
2025-01-09T16:03:55.696+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = threadPoolTaskSchedulerBuilder, bean = class org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder
2025-01-09T16:03:55.697+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$SimpleAsyncTaskSchedulerBuilderConfiguration, bean = class org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$SimpleAsyncTaskSchedulerBuilderConfiguration
2025-01-09T16:03:55.698+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = simpleAsyncTaskSchedulerBuilder, bean = class org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder
2025-01-09T16:03:55.698+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration, bean = class org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration
2025-01-09T16:03:55.698+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration, bean = class org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration
2025-01-09T16:03:55.698+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = httpMessageConvertersRestClientCustomizer, bean = class org.springframework.boot.autoconfigure.web.client.HttpMessageConvertersRestClientCustomizer
2025-01-09T16:03:55.699+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = restClientSsl, bean = class org.springframework.boot.autoconfigure.web.client.AutoConfiguredRestClientSsl
2025-01-09T16:03:55.699+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = restClientBuilderConfigurer, bean = class org.springframework.boot.autoconfigure.web.client.RestClientBuilderConfigurer
2025-01-09T16:03:55.699+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration, bean = class org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration
2025-01-09T16:03:55.700+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration, bean = class org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration
2025-01-09T16:03:55.700+09:00  INFO 72763 --- [springadvanced] [           main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = multipartResolver, bean = class org.springframework.web.multipart.support.StandardServletMultipartResolver
...
  • 무수히 많은 로그가 찍힌다. 왜일까? 자동으로 등록되는 스프링 빈이 이렇게 많다는 것이다. 그리고 빈 후처리기는 그 모든 빈들에 대해서 호출되기 때문에 당연히 이렇게 많은 로그가 찍힌다. 그래서 위에서 특정 패키지로 제한한 것이다.
  • 우리의 프록시로 등록된 녀석들을 확인하기 위해 /v1/request, /v2/request, /v3/request 를 호출해보자. 아래와 같이 로그가 잘 찍히는 것을 확인할 수 있다. 
2025-01-09T16:04:03.824+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-1] c.s.trace.logtrace.ThreadLocalLogTrace   : [fee8454e] OrderControllerV1.request()
2025-01-09T16:04:03.824+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-1] c.s.trace.logtrace.ThreadLocalLogTrace   : [fee8454e] |--->OrderServiceV1.orderItem()
2025-01-09T16:04:03.824+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-1] c.s.trace.logtrace.ThreadLocalLogTrace   : [fee8454e] |    |--->OrderRepositoryV1.save()
2025-01-09T16:04:04.825+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-1] c.s.trace.logtrace.ThreadLocalLogTrace   : [fee8454e] |    |<---OrderRepositoryV1.save() time=1001ms
2025-01-09T16:04:04.826+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-1] c.s.trace.logtrace.ThreadLocalLogTrace   : [fee8454e] |<---OrderServiceV1.orderItem() time=1002ms
2025-01-09T16:04:04.826+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-1] c.s.trace.logtrace.ThreadLocalLogTrace   : [fee8454e] OrderControllerV1.request() time=1002ms
2025-01-09T16:04:15.400+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-3] c.s.trace.logtrace.ThreadLocalLogTrace   : [9f510d2e] OrderControllerV2.request()
2025-01-09T16:04:15.401+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-3] c.s.trace.logtrace.ThreadLocalLogTrace   : [9f510d2e] |--->OrderServiceV2.orderItem()
2025-01-09T16:04:15.401+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-3] c.s.trace.logtrace.ThreadLocalLogTrace   : [9f510d2e] |    |--->OrderRepositoryV2.save()
2025-01-09T16:04:16.402+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-3] c.s.trace.logtrace.ThreadLocalLogTrace   : [9f510d2e] |    |<---OrderRepositoryV2.save() time=1001ms
2025-01-09T16:04:16.402+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-3] c.s.trace.logtrace.ThreadLocalLogTrace   : [9f510d2e] |<---OrderServiceV2.orderItem() time=1001ms
2025-01-09T16:04:16.402+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-3] c.s.trace.logtrace.ThreadLocalLogTrace   : [9f510d2e] OrderControllerV2.request() time=1002ms
2025-01-09T16:04:23.327+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-4] c.s.trace.logtrace.ThreadLocalLogTrace   : [9e82fc0e] OrderControllerV3.request()
2025-01-09T16:04:23.327+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-4] c.s.trace.logtrace.ThreadLocalLogTrace   : [9e82fc0e] |--->OrderServiceV3.orderItem()
2025-01-09T16:04:23.327+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-4] c.s.trace.logtrace.ThreadLocalLogTrace   : [9e82fc0e] |    |--->OrderRepositoryV3.save()
2025-01-09T16:04:24.328+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-4] c.s.trace.logtrace.ThreadLocalLogTrace   : [9e82fc0e] |    |<---OrderRepositoryV3.save() time=1001ms
2025-01-09T16:04:24.328+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-4] c.s.trace.logtrace.ThreadLocalLogTrace   : [9e82fc0e] |<---OrderServiceV3.orderItem() time=1001ms
2025-01-09T16:04:24.328+09:00  INFO 72763 --- [springadvanced] [nio-8080-exec-4] c.s.trace.logtrace.ThreadLocalLogTrace   : [9e82fc0e] OrderControllerV3.request() time=1001ms

 

빈 후처리기 - 정리

빈 후처리기는 스프링 빈을 생성한 후에 초기화 하기 직전 또는 직후 이 빈을 조작하거나 바꿔치기할 수 있는 기술이다. 그리고 이 빈 후처리기를 우리가 직접 만들어서 어떤 문제를 해결했는지 보자.

 

문제1 - 너무 많은 설정

프록시를 직접 스프링 빈으로 등록하는 ProxyFactoryConfigV1, ProxyFactoryConfigV2와 같은 설정 파일은 프록시 관련 설정이 지나치게 많다는 문제가 있다. 예를 들어, 애플리케이션에 스프링 빈이 100개가 있다면 여기에 프록시를 통해 부가 기능을 적용하려면 100개의 프록시 설정 코드가 들어가야 한다. 

 

문제2 - 컴포넌트 스캔

V3처럼 컴포넌트 스캔을 사용하는 경우, 프록시 설정 코드를 직접 작성하는 것으로 프록시를 등록하지도 못한다. 왜냐하면 컴포넌트 스캔으로 이미 스프링 컨테이너에 실제 객체를 스프링 빈으로 등록을 다 해버린 상태이기 때문이다. 

 

문제 해결

빈 후처리기를 사용해서 프록시를 생성하는 부분을 하나로 집중할 수 있다. (바로 빈 후처리기를 작성하는 코드). 

그리고 컴포넌트 스캔처럼 스프링이 직접 대상을 빈으로 등록하는 경우에도 중간에 빈 등록 과정을 가로채서 원본 대신에 프록시를 스프링 빈으로 등록할 수 있다. 덕분에 애플리케이션에 수많은 스프링 빈이 추가되어도 프록시와 관련된 코드는 전혀 변경하지 않아도 된다. 그리고 컴포넌트 스캔을 사용해도 프록시가 모두 적용된다. 

 

하지만 개발자의 욕심은 끝이 없다.

스프링은 프록시를 생성하기 위한 빈 후처리기를 이미 만들어서 제공한다. 

 

중요!

프록시의 적용 대상 여부를 여기서는 간단하게 패키지를 기준으로 설정했다. 그런데 잘 생각해보면 포인트컷을 사용하면 더 깔끔할 것 같다. 포인트컷은 이미 클래스, 메서드 단위의 필터 기능을 가지고 있기 때문에, 프록시 적용 대상 여부를 정밀하게 설정할 수 있다. 참고로 어드바이저는 포인트컷을 가지고 있다. 따라서 어드바이저를 통해 포인트컷을 확인할 수 있다. 스프링 AOP는 포인트컷을 사용해서 프록시 적용 대상 여부를 체크한다. 결과적으로 포인트컷은 다음 두 곳에서 사용된다.

  • 프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용한다. (빈 후처리기 - 자동 프록시 생성)
  • 프록시의 어떤 메서드가 호출 되었을 때 어드바이스를 적용할 지 판단한다. (프록시 내부)

 

스프링이 제공하는 빈 후처리기1

결론을 먼저 말하자면, 저렇게 직접 빈 후처리기를 만들 일은 없다. 적어도 프록시를 만들기 위해서라면 말이다.

왜냐? 스프링이 저 역할을 하는 빈 후처리기를 이미 다 만들어서 제공해주기 때문이다. 그것들을 사용하려면 다음 라이브러리를 추가해야한다.

implementation 'org.springframework.boot:spring-boot-starter-aop'

 

  • 이 라이브러리를 추가하면 aspectjweaver 라는 aspectJ 관련 라이브러리를 등록하고, 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록한다. 스프링 부트가 없던 시절에는 @EnableAspectJAutoProxy를 직접 사용해야 했는데, 이 부분을 스프링 부트가 자동으로 처리해준다. 스프링 부트가 활성화하는 빈은 AopAutoConfiguration인데 이 빈을 활성화하면 자동 프록시 생성기라는 빈 후처리기가 스프링 빈에 자동으로 등록된다.

AnnotationAwareAspectJAutoProxyCreator

  • 이 녀석이 스프링 부트가 자동으로 스프링 빈으로 등록해주는 빈 후처리기다.
  • 이름 그대로 자동으로 프록시를 생성해주는 빈 후처리기. 이 빈 후처리기는 스프링 빈으로 등록된 Advisor들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.
  • Advisor안에는 PointcutAdvice가 이미 모두 포함되어 있다. 따라서 Advisor만 알고 있으면 그 안에 있는 Pointcut으로 어떤 스프링 빈에 프록시를 적용해야 할지 알 수 있다. 그리고 Advice로 부가 기능을 적용하면 된다.
  • 그리고 @Aspect도 자동으로 인식해서 프록시를 만들고 AOP를 적용해준다.

 

자동 프록시 생성기의 작동 과정

 

1. 생성: 스프링이 스프링 빈 대상이 되는 객체를 생성한다. (@Bean, 컴포넌트 스캔 모두 포함)

2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 저 AnnotationAwareAspectJAutoProxyCreator 빈 후처리기에 전달한다.

3. 모든 Advisor 빈 조회: AnnotationAwareAspectJAutoProxyCreator 빈 후처리기는 스프링 컨테이너에서 모든 Advisor를 조회한다.

4. 프록시 적용 대상 체크: 앞서 조회한 Advisor에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다. 이 때 객체의 클래스 정보는 물론이고, 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 매칭해본다. 하나라도 조건이 만족하면 프록시 적용 대상이 된다. 예를 들어 10개의 메서드 중에 하나만 포인트컷 조건에 만족해도 프록시 적용 대상이 된다.

5. 프록시 생성: 프록시 적용 대상이면 프록시 팩토리를 사용해 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록한다. 만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록한다.

6. 빈 등록: 반환된 객체는 스프링 빈으로 등록된다.

 

코드를 통해 바로 적용해보자.

AutoProxyConfig

package cwchoiit.springadvanced.proxy.config.v5_autoproxy;

import cwchoiit.springadvanced.proxy.config.AppV1Config;
import cwchoiit.springadvanced.proxy.config.AppV2Config;
import cwchoiit.springadvanced.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import cwchoiit.springadvanced.trace.logtrace.LogTrace;
import org.springframework.aop.Advisor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {

    @Bean
    public Advisor getAdvisor(LogTrace trace) {
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");
        LogTraceAdvice logTraceAdvice = new LogTraceAdvice(trace);

        return new DefaultPointcutAdvisor(pointcut, logTraceAdvice);
    }
}
  • 이전에 했던 방식을 잠깐 떠올려보자. 분명 내가 직접 만든 빈 후처리기를 빈으로 등록했다. 그런데 이제 그럴 필요가 없다. 왜냐? 스프링이 프록시를 만들어주는 빈 후처리기를 이미 자동으로 등록해주기 때문이다. 
  • 그래서 나는 Advisor만 빈으로 등록하면 된다. 그럼 이 안에 있는 포인트컷을 참고해서 프록시로 만들어야 할 녀석들이 누구인지 AnnotationAwareAspectJAutoProxyCreator는 알아서 판단 후 프록시로 만들어준다.

 

강조: 포인트컷은 2가지에 사용된다.

1. 프록시 적용 여부 판단 - 프록시를 생성하는 단계

  • AnnotationAwareAspectJAutoProxyCreator는 등록된 모든 Advisor, @Aspect를 찾고 그 안에 포인트컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지 없는지 체크한다.
  • 클래스 + 메서드 조건을 모두 비교한다. 이 때 모든 메서드를 체크하는데, 포인트컷 조건에 하나하나 매칭해본다. 만약 조건에 맞는 것이 하나라도 있으면 프록시를 생성하고, 조건에 맞는 것이 하나도 없을 땐 프록시를 생성하지 않는다.

2. 어드바이스 적용 여부 판단 - 프록시를 사용하는 단계

  • 프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할지 말지 포인트컷을 보고 판단한다. 즉, 특정 메서드가 호출됐을 때 그 메서드가 포인트컷 조건에 만족하는 메서드인지 확인 한다는 뜻이다.

 

프록시를 모든 곳에 생성하는 것은 비용 낭비라고 했고 AnnotationAwareAspectJAutoProxyCreator는 포인트컷으로 한번 필터링해서 어드바이스가 사용될 가능성이 있는 곳에만 프록시를 생성한다고 했다. 그럼 만약 여러개의 포인트컷 조건을 만족한다고 하면 프록시는 여러개가 생길까? 아니다. 프록시는 딱 하나만 생기고 그 안에 여러 어드바이저가 등록되는 것이다.

 

AnnotationAwareAspectJAutoProxyCreator 상황별 정리

  • advisor1의 포인트컷만 만족: 프록시 1개 생성, 프록시에 advisor1만 포함
  • advisor1, advisor2의 포인트컷을 모두 만족: 프록시 1개 생성, 프록시에 advisor1, advisor2 모두 포함
  • advisor1, advisor2의 포인트컷을 모두 만족하지 않음: 프록시가 생성되지 않음

 

스프링이 제공하는 빈 후처리기2

위 방식대로 애플리케이션을 실행하면 원하는 기능은 제대로 동작하는데 애플리케이션 로딩 로그를 자세히 보면 이상하다.

2025-01-09T18:17:51.287+09:00  INFO 79454 --- [springadvanced] [           main] c.s.trace.logtrace.ThreadLocalLogTrace   : [af9b2e0c] WebMvcConfigurationSupport.requestMappingHandlerMapping()
2025-01-09T18:17:51.290+09:00  INFO 79454 --- [springadvanced] [           main] c.s.trace.logtrace.ThreadLocalLogTrace   : [af9b2e0c] WebMvcConfigurationSupport.requestMappingHandlerMapping() time=3ms
2025-01-09T18:17:51.340+09:00  INFO 79454 --- [springadvanced] [           main] c.s.trace.logtrace.ThreadLocalLogTrace   : [196ef571] WebMvcConfigurationSupport.requestMappingHandlerAdapter()
2025-01-09T18:17:51.346+09:00  INFO 79454 --- [springadvanced] [           main] c.s.trace.logtrace.ThreadLocalLogTrace   : [196ef571] WebMvcConfigurationSupport.requestMappingHandlerAdapter() time=6ms
  • 기대하지 않은 로그들이 막 올라온다. 그 이유는 지금 사용한 포인트컷이 단순히 메서드 이름에 "request*", "order*", "save*"만 포함되어 있으면 매칭 된다고 판단하기 때문이다. 
  • 결국, 스프링이 내부에서 사용하는 자동으로 등록되는 빈에도 메서드 이름에 저 중 하나가 들어가 있으면 프록시로 만들어지고 어드바이스도 적용되는 것이다.
  • 결론적으로, 더 확실하고 매우 정밀한 포인트컷이 필요하다.

AspectJExpressionPointcut

AspectJ라는 AOP에 특화된 포인트컷 표현식을 적용할 수 있다. 이게 이제 실무에서 가장 많이 사용될 녀석이다. 

 

AutoProxyConfig

package cwchoiit.springadvanced.proxy.config.v5_autoproxy;

import cwchoiit.springadvanced.proxy.config.AppV1Config;
import cwchoiit.springadvanced.proxy.config.AppV2Config;
import cwchoiit.springadvanced.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import cwchoiit.springadvanced.trace.logtrace.LogTrace;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {

    @Bean
    public Advisor advisor2(LogTrace trace) {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* cwchoiit.springadvanced.proxy.app..*(..)) &&" +
                " !execution(* cwchoiit.springadvanced.proxy.app..noLog(..))");
        LogTraceAdvice logTraceAdvice = new LogTraceAdvice(trace);

        return new DefaultPointcutAdvisor(pointcut, logTraceAdvice);
    }
}
  • 위와 같이 작성하는 방법이 바로 AspectJExpressionPointcut이다. 매우 정밀하게 포인트컷을 작성할 수 있다.
  • 이 문법에 대한 내용은 아예 별도의 포스팅을 만들 예정이다.

 

728x90
반응형
LIST

'Spring Advanced' 카테고리의 다른 글

스프링 AOP Part. 1  (0) 2023.12.29
AOP와 @Aspect  (0) 2023.12.29
Advisor, Advice, Pointcut  (0) 2023.12.15
CGLIB,스프링이 지원하는 ProxyFactory  (0) 2023.12.14
Proxy/Decorator Pattern 2 (JDK 동적 프록시)  (0) 2023.12.14
728x90
반응형
SMALL

Vim(Vi Improved)는 텍스트 기반의 아주 아주 유명한 에디터인데, 은근 많이 사용되고 꽤나 유용하기 때문에 알아두면 좋을 것 같다. 근데 아무리 알아둔다고 해도 안 쓰고 시간이 지나면 자꾸 까먹어 가지고 정리를 하고자 한다.

SMALL

 

설정 파일 (없으면 만들면 된다)

~/.vimrc

 

컬러 스키마나 플러그인 관련 데이터 파일 경로

~/.vim/autoload ~/.vim/backup ~/.vim/colors ~/.vim/plugged
$ cd ~/.vim/colors

$ curl -o molokai.vim https://raw.githubusercontent.com/tomasr/molokai/master/colors/molokai.vim

 

 

우선 Vim은 여러 모드(Mode)가 있다. 이 여러가지 모드들부터 살펴보자.

Vim 여러 가지 모드

Mode Description
Normal Mode Vim 시작 시 모드
Insert Mode i, a, o 등의 명령어로 삽입 모드 진입 (쓰기 모드)
Visual Mode visual line/block 을 지정해서 동작하는 모드
Select Mode 선택 영역을 바로 수정 (거의 쓰이지 않음)
Command-line Mode ':', '/', '?', '!' 으로 명령 수행
Ex Mode Ex mode(Ex라는 툴의 기능을 말하는데 요즘은 거의 쓰이지 않음)
Terminal-job Mode :terminal 명령으로 vim 창에서 터미널 실행(예: bash)

 

여러 모드의 대표적인 예시를 하나씩 알아보자면 (이후에 더 자세히 알아볼거다) 우선, Normal Mode를 보자.

Normal Mode는 VI로 파일을 최초로 열었을 때의 모드이다. 다른 말로 아무런 모드도 아닌 모드이다.

 

Insert Mode는 이제 쓰기 모드가 된다. 입력, 수정, 삭제가 가능해지는 모드가 된다.

이 Insert Mode로 들어가려면 Normal Mode에서 'i'를 입력하면 하단에 --INSERT-- 라는 표시가 띄워진다. 이 상태가 Insert Mode이다. 다음 사진처럼 말이다.

 

Visual Mode는 Block을 지정해서 수정할 수 있는 모드라고 기억하면 좋을 것 같다. 예를 들어 여러 문장을 한번에 지우고 싶을 때 일반적으로 그 전체 라인을 마우스로 드래그&드랍을 하면 블록으로 포커싱이 되고 지울 수 있는 것처럼. 이 Visual Mode로 들어가려면 Normal Mode에서 'v'를 누르면 하단에 --VISUAL-- 이라고 표시가 된다. 이 때가 Visual Mode이다.

 

위 사진처럼 VISUAL 모드일 때 화살표로 위아래로 움직이면 블록으로 포커싱이 되는데, 이 블록으로 포커싱 됐을 때 만약 저 블록만큼 날리고 싶으면 'x'를 누르면 날라간다. 그리고 행한 작업에 대해 다시 돌리고 싶으면(Undo) 'u'를 누르면 다시 복원된다.

 

이제 Command-line Mode인데 이건 리눅스에서 여러 명령어를 사용하는 것처럼 이 Vim 에서도 사용할 수 있다. 예를 들면, 특정 키워드를 찾고 싶으면 /키워드 를 입력 후 엔터를 치면 그 키워드가 있는 경우 그 곳으로 포커싱된다. 다음 사진처럼 말이다.

 

 

이동

Vim 에디터에서 커서 이동화면 이동에 대한 내용이 있는데, 이 이동에 대한 단축키를 알면 꽤나 유용하다. 화살표보다 훨씬 유용하다.

여러 단축키가 있는데 내가 자주 사용하고 유용하다 생각하는 것만 정리하겠다.

 

커서 이동

Key Description
0 라인 시작으로 이동
f + '찾고자 하는 단어' (예: f + p, f + i, f + 3,...) 현재 라인에서 커서 기준 뒤쪽에 제일 가까이에 있는 '찾고자 하는 단어'로 커서 이동
F + '찾고자 하는 단어' 현재 라인에서 커서 기준 앞쪽에 제일 가까이에 있는 '찾고자 하는 단어'로 커서 이동
$ 라인 끝으로 이동
G 파일의 마지막 줄로 이동
gg 파일의 첫 줄로 이동
:라인넘버 입력한 라인넘버로 이동(예: ":55 + Enter" 하면 55번 라인으로)
/searchTerm 입력한 단어를 찾아준다. (예: "/provider" 입력하고 Enter하면 해당 단어가 있으면 찾아준다. 여러개 있는경우 n: Next N: Previous)

 

화면 이동

Key Description
ctrl + f 한 페이지 앞으로
ctrl + b 한 페이지 뒤로
ctrl + e 화면 한 줄씩 앞으로
ctrl + y 화면 한 줄씩 뒤로

 

Copy, Paste, Delete

Normal Mode에서

Key Description
dd 현재 커서의 문장 지우기
yy 현재 커서의 문장 복사
p or P P: 커서 앞에 붙여넣기
p: 커서 뒤에 붙여넣기

 

멀티 윈도우

Key Description
vim -O test.sh test2.sh Vim 실행 시 창 분할 (세로)
⌃+w v Vim 실행 후 창 분할 (세로)
⌃+w w 다음 창으로 커서 이동

 

728x90
반응형
LIST

'Linux' 카테고리의 다른 글

Hard Link / Soft Link  (0) 2023.10.04
사용자와 그룹과 파일 권한  (0) 2023.10.04
Linux란 ?  (0) 2023.10.04
[Linux]: 내가 쓰려고 기록한 유용한 Commands  (2) 2023.10.04

+ Recent posts