[Spring] IoC, DI, AOP

 

Spring Boot의 목적은 여러가지 기술을 이용하여 복잡한 요소들은 스프링 프레임워크에 위임하고, 개발자는 비즈니스 로직 개발에만 집중하는 것이다.

그중 핵심 개념인  IoC(제어의 역전), DI(의존성 주입), AOP(관점 지향 프로그래밍) 에 대해서 알아보쟈

 

 

1. IoC (Inversion of Control) : 제어의 역전


 

개념

 

IoC는 사용할 객체를 직접 생성하지 않고, 객체의 생명주기 관리를 외부(스프링 컨테이너)에 위임하는 것이다.

 

일반적으로 자바에서는 아래와 같이 객체를 생성했다.

@RestController
public class NoneDIController {
    private MyService service = new MyServiceImpl();

}

 

위와 같이 객체를 직접 생성한 후 그 객체에서 제공하는 기능을 사용했다.

즉, 객체를 생성하고 사용하는 작업을 개발자가 직접 제어하는 것이다.

 

그러나 스프링은 기본적으로 제어 역전(IoC)을 이용하여 개발자가 직접 객체를 생성하지 않고, 객체의 생명 주기 관리를 외부에 위임한다. 여기서 외부는 스프링에서 관리하는 Spring Container 또는 Ioc  Container라고 한다.

이렇게 객체의 관리를 컨테이너에 맡겨 제어권이 넘어간 것제어의 역전이라고 한다.

 

쉽게 말하면 개발자가 new 키워드를 사용하여 객체를 직접 생성하는 것이 아니라,

스프링 컨테이너가 객체(Bean)을 생성하고 관리하는 방식

 

사용하는 이유

 

애플리케이션에서 객체 간의 결합도를 낮춰 유지보수성을 높이고 확장성을 고려한 개발이 가능

객체 생성과 생명 주기를 컨테이너가 담당하므로 더 효율적인 자원 관리가 가능

 

예제

IoC 없이 객체 생성하는 경우

public class Car {
    private Engine engine;

    public Car() {
        this.engine = new Engine();  // 직접 객체 생성 (강한 결합)
    }
}

 

  • Car 클래스 내부에서 Engine 객체를 직접 생성하고 있어,
    Engine 클래스가 변경되면 Car 클래스도 수정해야 하는 강한 결합 (Tightly Coupled) 구조.

 

IoC 적용

@Component
public class Engine {
    public void start() {
        System.out.println("Engine started!");
    }
}

@Component
public class Car {
    private final Engine engine;

    @Autowired
    public Car(Engine engine) {  // 의존성 주입 (DI)
        this.engine = engine;
    }
}

 

 

  • @Component를 사용하여 스프링이 객체를 생성하고 관리하도록 설정.
  • Car 클래스는 Engine을 직접 생성하지 않고 외부에서 주입받음 (DI 적용).
  • 결과적으로 Car와 Engine 간의 결합도가 낮아지고 유지보수가 쉬워짐.

 

 

 

 

2. DI(Dependency Injection) : 의존성 주입


 

개념

 

의존성 주입은 제어 역전의 방법 중의 하나로, 사용할 객체를 직접 생성하지 않고 외부 컨테이너가 생성한 객체를 전달받아 사용하는 방식을 말한다.

IoC 개념을 실현하는 방법 중 하나이다.

 

의존성 주입 방법들

1. 생성자를 통한 의존성 주입

  • 스프링에서는 @Autowired라는 어노테이션을 통해 의존성 주입을 명시할 수 있다.
  • 스프링 4.3 이후 버전은 생성자 주입시 @Autowired 생략 가능
  • final 키워드를 사용 가능해서 불변성이 유지되므로 권장되는 방법

 

@Component
public class ServiceImpl implements Service {
	
	private final MemberRepository memberRepository;

	// 생성자 주입
	@Autowired // 생성자가 1개만 있으면 생략 가능(스프링에서만 가능)
	pulic ServiceImpl(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}
}

 

 

2. 필드 객체 선언을 통한 의존성 주입

@Component
public class ServiceImpl implements Service {

	@Autowired
	private MmeberRepository membeRepository;
}

 

 

3. setter를 통한 의존성 주입

@Component
public class ServiceImpl implements Service {
	
	private MemberRepository memberRepository;

	@Autowired
	public void setMemberRepository(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}
}

 

 

4. 일반 메서드를 통한 주입

 

@Component
public class ServiceImpl implements Service {
	
	private MemberRepository memberRepository;

	@Autowired
	public void init(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}
}

 

 

스프링 공식 문서는 생성자를 통한 주입을 권장한다.

다른 방식과는 다르게 레퍼런스 객체 없이는 객체를 초기화할 수 없도록 설계하기 때문이다.

 

사용하는 이유

 

  • 객체 간 결합도를 낮출 수 있어 유지보수가 용이함.
  • 테스트가 쉬워짐 (Mock 객체를 쉽게 주입할 수 있음).
  • 객체 생성을 스프링 컨테이너가 담당하므로 코드가 간결해짐.

 

 

 

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


 

개념

 

AOP관점을 기준으로 묶어서 개발하는 방식을 의미한다.

어떤 기능을 구현할 때, 그 기능을 "핵심 기능"과 "부가 기능"으로 구분해 각각을 하나의 관점으로 보는 것이다.

 

상품 정보를 등록하고, 등록한 상품 정보를 조회하는 로직을 예로 들어볼때,

클라이언트로부터 상품 정보 등록 요청을 받아서,

1. 상품 정보를 데이터베이스에 저장하고, 2. 저장된 상품 정보를 조회하는 두 가지가 핵심 기능이다.

 

그러나 해당 기능을 개발할 때, 위의 핵심 기능 사이사이에 부가 기능을 추가할 상황이 생긴다.

 

예를 들면, 아래 그림과 같이 비즈니스 로직 사이사이, 데이터베이스 접근 시에 로깅과 트랜잭션을 추가해야 할 수 있다.

 

 

이때, 핵심 로직 사이사이에 존재하는 부가기능인 로깅, 트랜잭션 등의 코드들은 핵심 기능과는 달리 동일한 기능을 수행할 확률이 높다.

 

AOP의 관점에서는 위와 같은 부가 기능들은 핵심 기능들과 상관없이 로직이 수행되기 전 또는 후에 수행되기만 하면 된다.

 

위의 코드에서 하나의 로직에서 로깅은 3번 트랜잭션은 1번 수행된다.

이러한 반복되는 부가 기능을 하나의 공통 로직으로 처리하도록 모듈화해 삽입하는 방식을 AOP라고 한다.

 

스프링은 디자인 패턴 중 하나인 프락시 패턴을 통해 AOP 기능을 제공하고 있다.

스프링 AOP의 목적은 OOP(객체 지향 프로그래밍)과 마찬가지로 모듈화해서 재사용이 가능한 구성을 만드는 것이고,

이 또한 개발자가 비즈니스 로직 구현에만 집중할 수 있도록 도와준다.

 

사용하는 이유

 

  • 비즈니스 로직과 부가 기능(예: 로깅, 트랜잭션)을 분리하여 코드의 가독성을 높임.
  • 중복 코드를 제거하여 유지보수를 쉽게 만듦.
  • 핵심 로직 변경 없이 공통 기능을 쉽게 추가 가능.

 

적용방법

@Aspect + @Before, @After, @Around 등의 애노테이션을 사용하여 적용

 

예제

로그 기능이 포함된 기존 코드 (AOP 미적용)

public class PaymentService {
    public void processPayment() {
        System.out.println("로그: 결제 프로세스 시작");
        // 결제 처리 로직
        System.out.println("로그: 결제 완료");
    }
}
  • 비즈니스 로직과 로그 코드가 섞여 있어 중복이 발생하고 유지보수가 어려움.

 AOP를 사용한 개선 코드

@Aspect
@Component
public class LoggingAspect {
    @Before("execution(* com.example.service.PaymentService.*(..))")
    public void beforeLog(JoinPoint joinPoint) {
        System.out.println("로그: " + joinPoint.getSignature().getName() + " 실행 전");
    }

    @After("execution(* com.example.service.PaymentService.*(..))")
    public void afterLog(JoinPoint joinPoint) {
        System.out.println("로그: " + joinPoint.getSignature().getName() + " 실행 후");
    }
}

 

  • @Before → 메서드 실행 전에 로그 출력.
  • @After → 메서드 실행 후 로그 출력.
  • 핵심 로직(PaymentService)과 부가 기능(로깅)이 완전히 분리됨.

 

 

 

4. 정리

 

개념 설명 역할
IoC 객체 생성 및 관리를 스프링 컨테이너가 담당 객체 생명 주기 관리
DI 객체간의 의존 관계를 외부에서 주입해 결합도 낮춤 IoC 실현 방법
AOP 공토 기능을 분리하여 적용 부가 기능 모듈화

 

 

IoC(제어의 역전)

객체 생성과 관리를 스프링 컨테이너가 담당하는 개념

DI(의존성 주입)

의존성을 직접 생성하지 않고 외부에서 주입하는 방식

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

공통 기능(로깅, 트랜잭션 등)을 핵심 로직과 분리하여 모듈화하는 기법

 

스프링은 IoC와 DI를 통해 객체 관리를 자동화하고, AOP를 활용해 부가 기능을 분리하여 효율적인 개발을 지원!