본문 바로가기
디자인패턴

AOP와 DI 구분

by 대박플머 2024. 9. 24.

소프트웨어 개발에서 모듈화와 유연성은 매우 중요한 개념입니다. 이를 달성하기 위해 자주 사용되는 두 가지 패턴이 AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)DI(Dependency Injection, 의존성 주입)입니다. 이 두 패턴은 근본적으로 서로 다른 문제를 해결하지만, 현대 개발 프레임워크에서는 보완적으로 사용되는 경우가 많습니다. 이번 글에서는 AOP와 DI의 개념을 명확히 구분하고, TypeScript를 사용해 각각의 실질적인 예를 살펴보며 두 패턴이 어떻게 협력하는지 알아보겠습니다.


AOP와 DI의 기본 개념

AOP (Aspect-Oriented Programming, 관점 지향 프로그래밍)

AOP는 소프트웨어 모듈에서 공통적인 관심사를 분리해 코드 중복을 줄이고, 시스템의 유지보수성을 향상시키는 패턴입니다. 예를 들어, 로깅이나 트랜잭션 관리와 같은 공통적인 기능은 다양한 비즈니스 로직에서 필요하지만, 이를 매번 구현한다면 코드 중복이 발생하고 관리가 어려워집니다. AOP는 이 문제를 해결하기 위해, 이러한 공통 관심사를 별도로 정의하고, 실행 시 특정 지점에서 자동으로 적용될 수 있도록 합니다.

AOP의 주요 구성 요소:
  • Aspect: 공통 관심사를 정의한 모듈입니다. 예를 들어, 로깅을 Aspect로 정의할 수 있습니다.
  • Join Point: 비즈니스 로직 실행 중 공통 관심사가 적용될 수 있는 지점입니다. 메서드 호출, 예외 처리, 필드 접근 등이 해당합니다.
  • Advice: Join Point에서 실행되는 코드, 즉 특정 지점에서 수행되는 부가 기능을 정의합니다.
  • Pointcut: Advice가 적용될 Join Point를 명시하는 표현식입니다.
  • Weaving: Aspect와 비즈니스 로직을 결합하는 과정입니다.

AOP를 사용하는 이유는 로직을 모듈화하여 코드 중복을 줄이고, 유지보수성과 가독성을 높이기 위함입니다.

DI (Dependency Injection, 의존성 주입)

DI는 객체가 자신이 필요로 하는 의존성을 외부에서 주입받도록 하는 설계 패턴입니다. 이 패턴은 객체 간의 결합도를 줄여 유연성과 확장성을 높여주며, 테스트를 용이하게 합니다. 객체는 직접 의존성을 생성하는 대신 외부에서 주입받아, 자신의 핵심 기능에만 집중할 수 있습니다.

DI의 주요 유형:
  • 생성자 주입(Constructor Injection): 객체 생성 시 의존성을 주입받습니다.
  • 세터 주입(Setter Injection): 객체 생성 후 세터 메서드를 통해 의존성을 주입받습니다.
  • 필드 주입(Field Injection): 필드에 직접 주입하는 방식입니다. (TypeScript에서는 거의 사용되지 않음)

DI의 목표는 객체 간의 의존성을 줄이고, 더 유연한 객체 설계를 가능하게 하는 데 있습니다.


AOP와 DI의 차이점

AOPDI는 서로 다른 문제를 해결하는 패턴입니다. AOP는 공통적인 관심사(횡단 관심사)를 비즈니스 로직과 분리하고, DI는 객체 간의 의존성을 관리합니다. 그럼에도 불구하고, 이 둘은 소프트웨어 설계에서 자주 함께 사용되며, 상호 보완적인 관계를 가집니다.

1. 목적

  • AOP: 횡단 관심사(로깅, 트랜잭션 관리, 보안 등)를 모듈화하여 비즈니스 로직과 분리.
  • DI: 객체 간의 결합도를 줄이고, 유연성을 확보하여 의존성을 외부에서 주입.

2. 작동 방식

  • AOP: 비즈니스 로직과는 독립적으로 동작하는 코드(Aspect)를 특정 시점에 결합(Weaving)하여 로직의 흐름을 변경.
  • DI: 객체 생성 시 필요한 의존성을 외부에서 주입하여 결합도를 낮춤.

3. 결합과 확장성

  • AOP: 로깅, 트랜잭션, 보안 등 특정 관심사를 별도로 관리함으로써 여러 곳에 일괄적으로 적용 가능.
  • DI: 주입되는 의존성을 쉽게 교체할 수 있어 확장성이 뛰어남.

4. 테스트 용이성

  • AOP: 비즈니스 로직과 횡단 관심사를 분리함으로써 핵심 로직에 집중한 테스트가 가능.
  • DI: 객체의 의존성을 Mock으로 대체하여 테스트가 용이.

AOP와 DI의 관계

AOP와 DI는 서로 다른 문제를 해결하는 패턴이지만, 실제로는 상호 보완적으로 사용될 수 있습니다. AOP에서 사용되는 Aspect는 DI를 통해 주입될 수 있으며, DI를 사용해 주입받은 객체들은 AOP로 횡단 관심사를 처리할 수 있습니다.

예를 들어, 로깅과 같은 횡단 관심사를 AOP로 처리하는 상황에서, 로깅을 담당하는 LoggerService는 DI를 통해 주입될 수 있습니다. 이런 식으로 두 패턴이 결합되어 시스템의 모듈성을 높이고, 코드의 유연성을 향상시킬 수 있습니다.


TypeScript 예시로 보는 AOP와 DI

이제 TypeScript를 사용해 AOP와 DI가 어떻게 결합되어 사용되는지 구체적인 예시를 살펴보겠습니다.

1. AOP 예시: 로깅 인터셉터

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

// AOP의 Aspect로 사용되는 LoggingInterceptor
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...'); // Join Point 전 실행

    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => console.log(`After... ${Date.now() - now}ms`)), // Join Point 후 실행
      );
  }
}

이 코드에서는 AOP의 핵심 개념인 Join PointAdvice를 활용하여 요청 전후로 로깅을 처리합니다.

2. DI 예시: 서비스 주입

import { Injectable } from '@nestjs/common';

// 의존성 주입할 서비스
@Injectable()
export class LoggerService {
  log(message: string) {
    console.log(message);
  }
}

// 주입을 받는 클래스
@Injectable()
export class UserService {
  constructor(private readonly loggerService: LoggerService) {}

  findUser(id: number) {
    this.loggerService.log(`Finding user with id: ${id}`);
    // 비즈니스 로직 처리...
  }
}

위 예시에서 UserServiceLoggerService에 의존하고 있으며, DI를 통해 LoggerService를 주입받습니다.

3. AOP와 DI 결합 예시

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  constructor(private readonly loggerService: LoggerService) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    this.loggerService.log('Before...'); // DI를 통해 주입받은 LoggerService 사용

    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => this.loggerService.log(`After... ${Date.now() - now}ms`)), // Join Point 후 처리
      );
  }
}

이 예시에서 AOP와 DI가 결합되었습니다. LoggingInterceptor는 AOP의 역할을 하고, 필요한 의존성(LoggerService)은 DI를 통해 주입받아 사용됩니다. 이는 두 패턴이 서로를 보완하는 좋은 예입니다.


결론

AOP와 DI는 서로 다른 문제를 해결하는 독립적인 패턴이지만, 함께 사용되었을 때 더 큰 효과를 발휘합니다. AOP는 공통 관심사를 모듈화하여 코드 중복을 줄이고, DI는 의존성을 외부에서 주입하여 유연성과 확장성을 높입니다. 이 두 패턴이 결합되면, 더 모듈화된 시스템 설계와 유지보수성을 보장할 수 있습니다.

TypeScript로 구현한 AOP와 DI의 예시는 이러한 개념이 어떻게 실제 코드에 적용되는지 보여주며, 특히 Nest.js와 같은 프레임워크에서 두 패턴이 어떻게 보완적으로 사용되는지를 이해하는 데 도움을 줍니다.