IT

DI 의존성 주입 및 AOP 관점 지향 프로그래밍

lilililililiiii 2025. 5. 4. 15:38

소프트웨어 개발의 핵심 개념인 DI(Dependency Injection, 의존성 주입)와 AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)에 대해 자세히 알아보겠습니다. DI(의존성 주입)와 AOP(관점 지향 프로그래밍)는 현대 소프트웨어 개발, 특히 객체 지향 프로그래밍에서 매우 중요한 핵심 개념입니다. 각 개념의 정의, 목적, 장점과 함께 실제 코드 예시를 통해 이해를 돕고, 왜 이 개념들이 중요한지에 대해 설명해 드리겠습니다.

 

1. DI (Dependency Injection, 의존성 주입)

1.1. DI란 무엇인가?

DI는 객체 간의 결합도를 낮추는 데 중점을 둡니다. 의존하는 객체를 직접 생성하거나 찾는 대신 외부에서 주입받음으로써, 코드의 유연성과 재사용성을 높이고 테스트 용이성을 향상시킵니다. 예를 들어, 데이터베이스 연결이나 외부 서비스와의 연동 방식을 변경해야 할 때, DI를 사용하면 핵심 로직의 수정 없이 주입되는 객체만 교체하여 쉽게 대응할 수 있습니다. 생성자 주입, 수정자 주입 등 다양한 방식을 통해 의존성을 관리할 수 있으며, 특히 생성자 주입은 불변성과 의존성 누락 방지에 유리하여 권장됩니다.

  • 정의: 객체가 직접 의존하는 다른 객체를 생성하거나 찾는 대신, 외부(컨테이너 또는 다른 객체)로부터 의존 객체를 전달받는 디자인 패턴입니다. 즉, 의존 관계를 외부에서 결정하고 주입해주는 방식입니다.
  • 목적: 객체 간의 **결합도(Coupling)**를 낮추고 코드의 재사용성을 높이는 것입니다.
  • 장점
    • 코드 재사용성 증가
    • 객체 간 결합도 감소
    • 테스트 용이성 향상 (Mock 객체 사용 용이)
    • 유지보수성 향상
    • 코드의 유연성 및 확장성 증대

 

1.2. DI 적용 예시

예시 1: 데이터베이스 연결

UserService는 데이터베이스 연결(DatabaseConnection)이 필요하지만, 어떤 종류의 데이터베이스 연결(MySQL, Oracle 등)을 사용할지는 외부에서 결정하여 주입합니다.

// 인터페이스: 데이터베이스 연결 추상화
public interface DatabaseConnection {
    void connect();
}

// 구현체 1: MySQL 연결
public class MySqlConnection implements DatabaseConnection {
    @Override
    public void connect() {
        System.out.println("MySQL 데이터베이스에 연결됨");
    }
}

// 구현체 2: Oracle 연결 (확장 예시)
public class OracleConnection implements DatabaseConnection {
    @Override
    public void connect() {
        System.out.println("Oracle 데이터베이스에 연결됨");
    }
}

// 서비스 클래스: DatabaseConnection에 의존
public class UserService {
    private final DatabaseConnection dbConnection; // final 키워드로 불변성 확보

    // 생성자 주입 (Constructor Injection): 의존성을 생성 시점에 주입받음
    public UserService(DatabaseConnection dbConnection) {
        this.dbConnection = dbConnection;
    }

    public void saveUser() {
        System.out.println("사용자 정보 저장 시도...");
        dbConnection.connect(); // 주입받은 객체 사용
        System.out.println("사용자 정보 저장 완료");
    }
}

// 사용 예
public class Main {
    public static void main(String[] args) {
        // 외부에서 의존성(MySqlConnection) 생성
        DatabaseConnection mySqlConnection = new MySqlConnection();
        // 생성자를 통해 의존성 주입
        UserService userService = new UserService(mySqlConnection);
        userService.saveUser();

        System.out.println("---");

        // 데이터베이스 변경 시, UserService 코드 수정 없이 주입 객체만 변경
        DatabaseConnection oracleConnection = new OracleConnection();
        UserService userServiceOracle = new UserService(oracleConnection);
        userServiceOracle.saveUser();
    }
}

설명: UserService는 DatabaseConnection 인터페이스에만 의존합니다. 실제 사용할 구현체(MySqlConnection 또는 OracleConnection)는 외부(여기서는 Main 클래스)에서 생성하여 UserService의 생성자를 통해 주입합니다. 이를 통해 데이터베이스 종류가 변경되어도 UserService 코드를 수정할 필요 없이 유연하게 대응할 수 있습니다.

 

예시 2: 이메일 서비스

NotificationService는 이메일 발송 기능(EmailService)이 필요하며, 세터(Setter) 메서드를 통해 의존성을 주입받습니다.

// 인터페이스: 이메일 서비스 추상화
public interface EmailService {
    void sendEmail(String to, String subject, String body);
}

// 구현체: SMTP 이메일 서비스
public class SmtpEmailService implements EmailService {
    @Override
    public void sendEmail(String to, String subject, String body) {
        System.out.println("SMTP를 사용하여 이메일 전송: " + to + ", 제목: " + subject);
        // 실제 이메일 발송 로직...
    }
}

// 서비스 클래스: EmailService에 의존
public class NotificationService {
    private EmailService emailService;

    // 세터 주입 (Setter Injection): 세터 메서드를 통해 의존성 주입
    public void setEmailService(EmailService emailService) {
        System.out.println("EmailService 의존성 주입됨: " + emailService.getClass().getSimpleName());
        this.emailService = emailService;
    }

    public void notifyUser(String user, String message) {
        if (emailService == null) {
            System.out.println("오류: EmailService가 설정되지 않았습니다.");
            return;
        }
        emailService.sendEmail(user, "알림", message);
    }
}

// 사용 예
public class Main {
    public static void main(String[] args) {
        // 외부에서 의존성(SmtpEmailService) 생성
        EmailService smtpService = new SmtpEmailService();

        // NotificationService 생성
        NotificationService notificationService = new NotificationService();
        // 세터 메서드를 통해 의존성 주입
        notificationService.setEmailService(smtpService);

        // 서비스 사용
        notificationService.notifyUser("user@example.com", "새로운 메시지가 도착했습니다.");
    }
}

설명: 세터 주입은 객체가 생성된 후에도 의존성을 변경하거나 주입할 수 있는 유연성을 제공합니다. (단, 의존성이 누락될 수 있으므로 주의 필요)

 

1.3. DI 주입 방식

생성자 주입 (Constructor Injection): 가장 권장되는 방식. 객체 생성 시점에 모든 의존성이 주입되어 불변성을 확보하고 누락을 방지할 수 있습니다.

수정자 주입 (Setter Injection): 선택적 의존성이나 의존성 변경이 필요할 때 사용됩니다.

필드 주입 (Field Injection): 코드가 간결해 보이지만, 외부에서 접근이 어렵고 테스트가 불편하며 DI 컨테이너에 강하게 의존하게 되어 권장되지 않습니다. (예: Spring의 @Autowired 필드 주입)

 

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

2.1. AOP란 무엇인가?

AOP는 애플리케이션 전반에 걸쳐 반복적으로 나타나는 공통 부가 기능(횡단 관심사), 예를 들어 로깅, 트랜잭션 관리, 보안, 성능 측정 등을 핵심 비즈니스 로직과 분리하는 데 목적을 둡니다. 이를 통해 개발자는 핵심 로직에만 집중할 수 있으며, 부가 기능은 'Aspect'라는 모듈화된 단위로 관리됩니다. 결과적으로 코드 중복이 줄어들고, 핵심 로직의 가독성과 유지보수성이 크게 향상됩니다. Spring 프레임워크와 같은 환경에서는 AOP를 통해 선언적으로 부가 기능을 적용할 수 있어 매우 강력한 도구가 됩니다.

  • 정의: 애플리케이션의 핵심 비즈니스 로직과 공통 부가 기능(Cross-Cutting Concerns)을 분리하여 모듈화하는 프로그래밍 패러다임입니다.
  • 공통 부가 기능 (Cross-Cutting Concerns): 로깅, 트랜잭션 관리, 보안, 성능 측정 등 여러 모듈에 걸쳐 반복적으로 나타나는 기능.
  • 목적: 핵심 로직과 부가 기능을 분리하여 코드의 중복을 제거하고, 핵심 로직의 가독성과 유지보수성을 높이는 것입니다.

2.2. AOP 주요 용어

  • Aspect: 공통 부가 기능을 모듈화한 단위 (예: 로깅 Aspect, 트랜잭션 Aspect). Advice와 Pointcut의 조합.
  • JoinPoint: Aspect가 적용될 수 있는 프로그램 실행 지점 (예: 메서드 호출, 필드 접근).
  • Pointcut: 수많은 JoinPoint 중에서 Aspect를 적용할 특정 지점을 선별하는 표현식. (예: execution(* com.example.service.*.*(..)))
  • Advice: Aspect가 Pointcut에서 실행해야 할 실제 부가 기능 로직. (예: @Before, @After, @Around)
  • Target: Aspect가 적용되는 대상 객체 (핵심 비즈니스 로직을 담고 있는 객체).
  • Weaving: Aspect를 Target 객체의 특정 JoinPoint에 연결하여 부가 기능을 적용하는 과정. (컴파일 시, 로드 시, 런타임 시 발생 가능)

2.3. AOP 적용 예시 (Spring AOP 기반)

예시 1: 메서드 실행 시간 측정

서비스 계층의 모든 메서드 실행 시간을 측정하여 출력합니다.

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect // 이 클래스가 Aspect임을 선언
@Component // Spring Bean으로 등록
public class PerformanceAspect {

    // Pointcut: com.example.service 패키지 내의 모든 클래스의 모든 메서드 실행 시점
    @Around("execution(* com.example.service.*.*(..))")
    public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis(); // 메서드 실행 전 시간 측정

        Object result = joinPoint.proceed(); // 대상 메서드 실행

        long endTime = System.currentTimeMillis(); // 메서드 실행 후 시간 측정
        long executionTime = endTime - startTime;

        System.out.println("[성능 측정] " + joinPoint.getSignature() + " 실행 시간: " + executionTime + "ms");

        return result; // 메서드 실행 결과 반환
    }
}

설명: @Around Advice는 대상 메서드 실행 전후에 로직을 추가할 수 있습니다. joinPoint.proceed()를 호출하여 실제 타겟 메서드를 실행합니다. 이를 통해 핵심 서비스 로직 코드 수정 없이 성능 측정 기능을 추가했습니다.

 

예시 2: 로깅

컨트롤러 메서드 실행 전과 서비스 메서드 실행 후에 로그를 남깁니다.

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect

 

 

결론적으로, DI와 AOP는 서로 보완적인 역할을 수행하며 함께 사용될 때 시너지 효과를 발휘합니다. DI가 객체 간의 관계를 유연하게 관리하는 기반을 제공한다면, AOP는 이렇게 관리되는 객체들에 공통 기능을 효과적으로 적용할 수 있게 해줍니다. 이 두 개념을 적절히 활용하는 것은 모듈성, 유지보수성, 테스트 용이성이 높은 견고하고 확장 가능한 애플리케이션을 구축하는 데 필수적입니다. 따라서 DI와 AOP에 대한 깊은 이해는 뛰어난 소프트웨어 아키텍처를 설계하고 구현하는 데 중요한 역량이라고 할 수 있습니다.