AOP(Aspect-Oriented Programming)

Zain·2025년 2월 20일
post-thumbnail

우연히 Velog 포스트를 보다가 AOP라는 생소한 단어를 접하게 되었다. 이에 대해 알아보고 정리하는 시간을 가져보려고 한다.

AOP?

AOP(Aspect-Oriented Programming)는 관점 지향 프로그래밍. 프로그램의 핵심 비즈니스 로직공통적으로 사용되는 기능(횡단 관심사, Cross-cutting Concern) 을 분리해서 관리하는 프로그래밍 패러다임.


횡단 관심사란?

AOP의 핵심 개념 중 하나인 횡단 관심사(Cross-cutting Concern)는 애플리케이션의 여러 부분에서 공통적으로 필요한 기능을 의미.

핵심 관심사(Core Concern) vs 횡단 관심사(Cross-cutting Concern)

  • 핵심 관심사(Core Concern): 애플리케이션의 비즈니스 로직 (예: 주문 처리, 회원 가입 등)
  • 횡단 관심사(Cross-cutting Concern): 여러 핵심 관심사에 걸쳐 반복적으로 사용되는 기능 (예: 로깅, 보안, 트랜잭션 등)

즉, 핵심 기능과 직접적인 관련은 없지만 필수적인 기능이 횡단 관심사.


왜 횡단 관심사를 AOP로 모듈화해야 할까?

문제점 (AOP를 적용하지 않은 경우)

  • 각 기능(로깅, 트랜잭션, 보안 등)을 모든 메서드에 개별적으로 추가해야 함
  • 코드 중복 증가 → 유지보수 어려움
  • 핵심 비즈니스 로직이 공통 기능과 섞여 가독성 저하

AOP를 활용한 해결

  • 횡단 관심사를 Aspect(Middleware)로 분리하여 핵심 비즈니스 로직과 독립적으로 관리
  • 코드 중복을 줄이고 유지보수를 쉽게 만듦
  • 기존 코드 변경 없이 공통 기능을 추가 가능

AOP의 특징

  • 비즈니스 로직에 직접 관여하지 않음
    - AOP는 핵심 비즈니스 로직과 별도로 동작하며, 코드 내부에서 직접 호출되지 않음.
    - 주요 기능(예: 회원 가입, 주문 처리 등)보조 기능(로깅, 보안 검사 등)분리하여 가독성을 높임.

  • 소스 코드 수정 없이 기능 추가 가능
    - 기존 비즈니스 로직을 수정하지 않고도 새로운 기능을 추가할 수 있음.
    - 예를 들어, 로깅 기능을 추가하려면 각 메서드에 console.log()를 넣는 대신, AOP를 활용해 모든 메서드 실행 시 자동으로 로깅할 수 있음.

  • 자동으로 실행됨 (개발자가 직접 호출할 필요 없음)
    - AOP는 미리 정의된 규칙(Pointcut)에 따라 자동으로 적용됨.
    - 개발자가 직접 관련 로직을 호출할 필요 없이, 특정 조건을 만족하는 경우 자동으로 실행됨.

  • 반복적인 기능을 모듈화하여 유지보수 용이
    - 공통적으로 사용되는 기능을 한 곳(Aspect)에서 관리하여 코드 중복을 최소화하고 유지보수를 쉽게 함.
    - ✅ 로깅(Logging) - 특정 메서드 실행 정보를 자동으로 기록
    - ✅ 트랜잭션 관리(Transaction Management) - 데이터 변경 작업에서 오류 발생 시 롤백 처리
    - ✅ 유효성 검사(Validation) - API 요청 데이터의 형식 및 값 검증
    - ✅ 보안(Security) - 사용자의 권한 확인 후 요청을 처리


AOP 적용해보기

// 사용하려는 아이템 목록을 검증하고 구한다.
$items = app(InventoryService::class)->getAndVerifyItems(
    itemsRequest: $request->items,
    functionGroup: '먹이',
);


비즈니스로직 ....

아이템 사용시 공통적으로 수행되는 로직

public function getAndVerifyItems(
        array  $itemsRequest,
        string $functionGroup = '',
)
{
/** @var Collection<int, MemberInventory> $itemsResult */
$itemsResult = MemberInventory::whereIn('GMI_IDX', array_column($itemsRequest, 'inventoryId'))->get();

$items = [];

foreach ($itemsRequest as ['inventoryId' => $inventoryId, 'quantity' => $quantity]) {

    	/** @var MemberInventory */
    	$item = $itemsResult->firstWhere('GMI_IDX', $inventoryId);

    	if (!$item) {
        	throw new NotFoundHttpException('id ' . $inventoryId . ' 아이템이 없습니다.');
    	}

    	if ($item->MEMBER_IDX !== auth()->user()->MEMBER_IDX) {
        	throw new AccessDeniedHttpException('id ' . $inventoryId . ' 아이템에 대한 권한이 없습니다.');
    	}

    	if ($item->GMI_QTY < $quantity) {
        	throw new UnprocessableEntityHttpException('id ' . $inventoryId . ' 아이템 수량이 부족합니다.');
    	}

    	if ($functionGroup && $item->Item->GI_GROUP_FUNCTION !== $functionGroup) {
        	throw new BadRequestHttpException($functionGroup . ' 아이템만 사용할 수 있습니다.');
    	}
    	$items[] = $item;
	}
	return collect($items);
}
profile
Vue, Laravel | TS, JS, PHP, MySQL

0개의 댓글