@Service 하나 붙였을 뿐인데 객체가 생기고, 생성자에 값이 들어오고, 어떤 메서드는 호출 앞뒤로 추가 동작까지 붙어요.
처음 Spring Boot 코드를 보면 이상한 장면이 자주 나와요.
내가 new OrderService()를 쓴 적이 없는데 OrderService 객체가 쓰이고요. OrderRepository를 직접 넘긴 적도 없는데 생성자에 들어와요. 나중에는 @Transactional 같은 Annotation을 붙였을 뿐인데 트랜잭션이 시작되고 끝나요.
이쯤에서 이런 생각이 들어요.
“객체는 누가 만든 거지?"오늘 볼 제어의 역전(IoC), 의존성 주입(dependency injection), 관점 지향 프로그래밍(AOP)은 이 질문들에 답하는 세 가지 기준점이에요.
"필요한 객체는 누가 넣어준 거지?"
"메서드 안에 없는 동작은 어디서 끼어든 거지?”
오늘은 IoC, DI, AOP의 모든 내부 구현을 파고들지는 않아요. 목표는 Spring Boot 코드를 읽을 때 “이건 객체 생성 책임 이야기구나”, “이건 연결 방식 이야기구나”, “이건 메서드 주변에 붙는 공통 규칙 이야기구나” 하고 구분하는 거예요. 다만 각 개념이 실무에서 어디서 깨지는지도 함께 볼게요.
먼저 직접 만든 코드부터 볼게요
주문 목록을 조회하는 작은 코드를 직접 조립한다고 해볼게요.
작을 때는 괜찮아요. 하지만 테스트에서 가짜 저장소를 넣고 싶거나, 저장소 구현이 바뀌거나, 여러 서비스가 같은 객체를 공유해야 하면 금방 불편해져요.
그래서 Spring은 질문을 바꿔요.
“컨트롤러가 객체를 직접 만들 필요가 있을까?”
IoC는 “내가 하던 제어를 컨테이너가 가져가는 것”이에요
제어의 역전(inversion of control, IoC)이라는 말은 처음 들으면 너무 거창해요. 여기서는 이렇게 잡아볼게요.내가 직접 하던 객체 생성과 연결의 제어를 Spring 컨테이너에게 넘기는 것.직접 제어할 때는 흐름이 이래요.
OrderService는 이제 OrderRepository를 직접 만들지 않아요. 대신 “나는 OrderRepository가 필요해요”라고 생성자로 말해요.
Spring 컨테이너는 애플리케이션을 시작할 때 관리할 객체들을 만들고, 필요한 관계를 맞춰요. 이렇게 Spring이 관리하는 객체를 빈(bean)이라고 불러요.
이 그림에서 방향이 바뀐 게 핵심이에요. 예전에는 개발자 코드가 객체를 직접 만들었고, 이제는 개발자 코드가 필요한 관계를 표현하면 컨테이너가 실제 객체 그래프를 만들어요.
DI는 IoC가 코드에 나타나는 가장 흔한 모양이에요
IoC가 큰 원칙이라면, 의존성 주입(dependency injection, DI)은 그 원칙이 코드에 나타나는 대표적인 방식이에요. 의존성이라는 말도 어렵게 들리지만 사실 단순해요.OrderService가 일을 하려면 OrderRepository가 필요하죠. 그러면 OrderService는 OrderRepository에 의존한다고 말해요.
OrderService는 저장소를 직접 만들지 않고, 생성자로 받아요. Spring 컨테이너가 OrderRepository 빈을 찾아서 OrderService를 만들 때 넣어줘요.
이게 의존성 주입이에요.
Spring Boot 코드에서 생성자만 있고
new가 없는데 객체가 들어온다면, 대부분 이 흐름을 보고 있는 거예요.
”직접 만들지 않는다”는 게 왜 중요할까요?
겉으로 보면 직접new를 쓰는 편이 더 명확해 보일 수 있어요.
사실은, 애플리케이션이 커질수록 직접 만드는 코드가 오히려 중요한 결정을 숨겨요.
예를 들어 결제 서비스가 이런 모양이라고 해볼게요.
new PaymentClient()를 숨어서 호출하면, 테스트와 변경이 어려워져요. 외부 결제 API를 진짜로 부르지 않고 테스트하고 싶은데, 클래스가 이미 직접 만들어버렸으니까요.
DI를 쓰면 클래스는 “필요한 것”을 공개하고, 실행 환경은 “무엇을 넣을지”를 결정할 수 있어요.
이 구조에서는 OrderService가 조립 담당자가 아니에요. 서비스는 주문 규칙에 집중하고, Spring 컨테이너가 필요한 협력 객체를 넣어줘요.
AOP는 “여러 곳에 반복되는 규칙”을 메서드 주변에 붙여요
이제 세 번째 기준점인 관점 지향 프로그래밍(AOP)을 볼게요. 주문을 저장할 때 이런 일이 필요하다고 해볼게요.- 메서드가 시작되면 트랜잭션을 열어요.
- 메서드가 정상 종료되면 커밋해요.
- 예외가 나면 롤백해요.
- 실행 시간을 기록해요.
- 권한이 있는지 확인해요.
saveOrder()인데, 주변 코드가 훨씬 커져요. 게다가 이 패턴은 주문, 결제, 배송, 회원 서비스 곳곳에 반복돼요.
AOP는 이런 반복 규칙을 “업무 코드 바깥의 관점”으로 분리하려고 해요.
여기서 중요한 건 AOP가 비즈니스 로직을 대신 작성한다는 뜻이 아니라는 점이에요.
업무 메서드는 주문을 만들고, AOP는 그 메서드 주변에 공통 규칙을 붙여요.
Spring AOP는 보통 프록시(proxy)를 통해 동작해요. 호출자가 실제 서비스 객체를 바로 부르는 것처럼 보여도, 중간에 프록시가 서서 메서드 앞뒤의 공통 규칙을 처리할 수 있어요.
그래서 Annotation 하나만 보고 끝내면 부족해요
Spring Boot를 쓰다 보면 Annotation 하나가 많은 일을 하는 것처럼 보여요.@Transactional이 메서드 안으로 들어와서 코드를 바꾸는 것처럼 느껴질 수 있어요.
사실은 그렇게 이해하면 나중에 헷갈려요. 핵심은 “메서드 내부가 바뀐다”가 아니라 “그 메서드를 호출하는 경로에 프록시가 끼어 공통 규칙을 적용한다”에 가까워요.
그래서 나중에 이런 질문들이 생겨요.
- 왜 같은 클래스 안에서 자기 메서드를 호출하면 트랜잭션이 기대처럼 안 걸릴 수 있을까요?
- 왜
final클래스나 메서드가 프록시와 충돌할 수 있을까요? - 왜 인터페이스가 있느냐 없느냐에 따라 프록시 방식 이야기가 나올까요?
AOP를 만나면 “메서드 안에 없는 공통 동작이 호출 경로 어디에 붙었을까?”를 물어보면 돼요.
실무에서는 경계가 더 중요해져요
처음에는 IoC, DI, AOP를 각각 한 문장으로 이해해도 좋아요.- IoC: 객체 생성 제어가 컨테이너로 이동해요.
- DI: 필요한 객체가 바깥에서 들어와요.
- AOP: 메서드 주변에 공통 규칙이 붙어요.
예를 들어
@Transactional은 초보자에게는 “DB 작업을 하나로 묶어줘요”로 시작해도 충분해요. 그런데 실무에서는 이 설명만으로 부족해지는 순간이 와요.
saveOrder()에 @Transactional이 붙어 있어요. 그런데 createOrder()가 같은 객체 안에서 saveOrder()를 직접 부르면, 호출이 프록시를 지나가지 않는 구조가 될 수 있어요. 그러면 기대한 트랜잭션 경계가 생기지 않을 수 있죠.
이걸 외우는 규칙으로만 받아들이면 금방 헷갈려요. 원리는 더 단순해요.
AOP 기반 기능은 “Annotation이 붙은 메서드가 있느냐”만 보지 말고, “그 메서드 호출이 프록시를 통과하느냐”를 같이 봐야 해요.트랜잭션 글에서는 여기에 롤백 규칙, 전파(propagation), 격리(isolation), 영속성 컨텍스트까지 붙어서 더 깊어질 거예요. 오늘은 그 전에 Spring을 읽는 기준점만 세우는 단계예요.
세 단어를 한 화면에 놓아볼게요
IoC, DI, AOP는 서로 같은 말이 아니에요. 하지만 Spring Boot 코드를 읽을 때는 자주 붙어서 보여요.
한 번 더 코드에서 바로 보이는 기준으로 놓아볼게요.
이 세 가지를 같이 보면 Spring Boot의 “내가 안 한 일”이 조금씩 나뉘어요.
이 그림은 정확한 실행 순서를 모두 표현한 내부 구조도는 아니에요. 대신 Spring Boot 코드를 읽을 때 질문을 나누는 지도에 가까워요.
Spring Boot 프로젝트에서는 어디서 처음 만나게 될까요?
지난 글에서 본OrderApplication을 다시 떠올려볼게요.
SpringApplication.run(...)은 그냥 객체 하나를 실행하는 호출이 아니에요. 뒤 글에서 더 자세히 보겠지만, 애플리케이션을 준비하고 Spring 컨테이너를 만들고 빈을 등록하는 흐름의 입구예요.
@SpringBootApplication은 자동 설정(auto-configuration), 컴포넌트 스캔(component scan), 설정 클래스 역할을 한곳에 묶어주는 대표 Annotation이에요.
그래서 우리가 만든 클래스가 이런 식으로 같은 루트 패키지 아래에 있으면요.
자, 정리해볼까요?
- IoC는 객체 생성과 연결의 제어가 개발자 코드에서 Spring 컨테이너로 이동하는 흐름이에요.
- DI는 필요한 객체를 클래스 안에서 직접 만들지 않고, 생성자 같은 통로로 바깥에서 넣어주는 방식이에요.
- AOP는 트랜잭션, 보안, 로깅처럼 여러 곳에 반복되는 공통 규칙을 메서드 주변에 붙이는 방식이에요.
- Spring Boot 코드를 읽을 때는 “누가 만들었지?”, “누가 넣어줬지?”, “메서드 밖의 동작은 어디서 붙었지?”를 나눠서 보면 덜 헷갈려요.
- 한 단계 깊게 보면 IoC는 생성 시점, DI는 선택 가능한 빈, AOP는 프록시를 지나는 호출 경계를 함께 읽어야 해요.
@Service, 생성자, @Transactional 같은 조각들이 조금 다르게 보일 거예요.
다음 글에서는 이 기준점을 들고 main 메서드로 들어가 볼게요. SpringApplication.run(...) 한 줄이 어떻게 설정을 읽고, 컨테이너를 만들고, 실행 중 애플리케이션으로 이어지는지 살펴볼 차례예요.