목차
스프링 프레임워크는 자바 엔터프라이즈 개발을 위한 오픈소스 애플리케이션 프레임워크이다. 스프링은 느슨한 결합, 의존성 주입(DI) 제어의 역전(IoC) 및 관점 지향 프로그래밍(AOP)과 같은 기술을 사용하여 개발자가 애플리케이션을 더 쉽게 개발하고 유지 관리할 수 있도록 지원.
목차
AOP(Aspect Oriented Programming)
객체가 만들어지는 흐름
스프링 컨테이너는 설정을 읽어 객체(빈)를 만들고 필요한 객체끼리 연결한다. 예를 들어 주문 서비스가 결제 기능을 필요로 한다면 생성자에서 그 의존성을 받는다.
@Service
public class OrderService {
private final PaymentClient paymentClient;
public OrderService(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
}
flowchart LR 설정[설정과 컴포넌트 검색] --> 컨테이너[스프링 컨테이너] 컨테이너 --> 결제[PaymentClient 빈] 결제 --> 주문[OrderService 빈에 주입]
서비스가 구현체를 직접 new로 만들지 않아 교체와 테스트가 쉬워진다. 인터페이스를 사용할지는 구현을 바꿔야 하는 실제 요구에 따라 결정하면 된다.
AOP는 여러 곳에 반복되는 횡단 관심사를 분리할 때 쓴다. 트랜잭션 처리가 대표적이다. 다만 프록시를 거치는 호출 방식에 따라 동작이 달라질 수 있으므로 적용 지점과 호출 경로를 확인해야 한다. 처음 학습할 때는 위 링크의 IoC/DI를 이해한 뒤 AOP를 보는 순서가 자연스럽다.
요청 하나를 따라가기
웹 요청이 들어오면 컨트롤러가 입력을 받고, 서비스가 업무 규칙을 처리하고, 저장소가 데이터를 읽거나 쓴다. 이 역할을 나누면 화면 응답 형식과 업무 규칙이 한 클래스에 뒤섞이지 않는다.
flowchart LR 요청[HTTP 요청] --> 컨트롤러[Controller] 컨트롤러 --> 서비스[Service] 서비스 --> 저장소[Repository] 저장소 --> DB[(DB)]
스프링이 이 구조를 강제하는 것은 아니다. 어떤 계층이 필요한지는 기능의 복잡도에 따라 결정한다. 중요한 것은 각 객체가 필요로 하는 협력자를 생성자에 드러내고, 트랜잭션 경계와 오류 처리 책임을 명확히 하는 것이다. 예를 들어 주문을 저장하면서 재고를 차감해야 한다면 두 변경이 함께 성공하거나 실패해야 하는지 먼저 정하고, 그 업무 흐름을 담당하는 서비스에 트랜잭션 경계를 둔다.
컨테이너가 빈을 찾지 못하는 오류가 나면 해당 클래스가 빈으로 등록됐는지, 컴포넌트 검색 범위 안에 있는지, 생성자에 필요한 의존성이 등록됐는지 차례로 확인한다. 이 순서로 보면 의존성 주입을 추상적인 용어보다 실제 객체 연결 문제로 이해할 수 있다.
컨테이너가 해결하는 문제와 남는 문제
Spring이 OrderService에 PaymentClient를 넣는다고 결제 요청이 안전해지는 것은 아니다. 결제는 외부 시스템 호출이므로 타임아웃, 재시도 시 중복 결제 방지, 실패한 주문 상태 처리가 필요하다. DI는 클라이언트의 생성·교체 문제를 다루고, AOP는 반복되는 경계 동작을 분리하지만 업무의 성공 조건은 서비스가 정한다.
sequenceDiagram participant User as 사용자 participant Ctrl as OrderController participant Svc as OrderService participant Repo as OrderRepository participant Pay as PaymentClient User->>Ctrl: 주문 요청 Ctrl->>Svc: place(command) Svc->>Repo: 주문 상태 저장 Svc->>Pay: 결제 요청 Pay-->>Svc: 성공 또는 실패 Svc-->>Ctrl: 결과 Ctrl-->>User: HTTP 응답
그림에서 DB 저장과 외부 결제는 같은 자원에 속하지 않는다. @Transactional을 붙였다고 외부 결제까지 DB 트랜잭션으로 되돌릴 수 없다. 실제 서비스는 결제 요청 식별자, 주문 상태 전이, 실패 후 보상 절차를 정의해야 한다. 작은 예제에서는 결제 호출 없이 저장만 하는 흐름부터 시작하고, 외부 연동이 들어올 때 실패 상태를 추가한다.
빈의 수명과 요청의 수명
Spring 빈은 기본적으로 싱글턴 스코프를 쓴다. 주문 서비스의 필드에 현재 사용자의 주문 ID를 저장하면 동시에 들어온 요청끼리 값이 섞일 수 있다. 요청마다 바뀌는 값은 메서드 인자나 요청 범위의 객체로 전달한다. 프록시가 필요한 요청 범위 빈을 싱글턴에 주입하는 경우에는 스코프 경계도 이해해야 한다.
| 문제 | 먼저 볼 부분 |
|---|---|
| 애플리케이션 시작 실패 | 빈 등록·스캔·생성자 후보 |
| 시작은 되지만 AOP 미적용 | 프록시를 통과하는 호출인지 |
| 동시 요청 값 혼합 | 싱글턴 필드의 가변 상태 |
| 저장 후 외부 API 실패 | 트랜잭션 경계와 실패 상태 |
| 테스트가 느림 | 컨테이너 없이 검사 가능한 업무 규칙인지 |
처음 학습할 때는 생성자 주입으로 조립하고, 저장소에서 한 건을 읽는 요청을 끝까지 따라가 보는 것이 좋다. 실패가 어디에서 발생했는지 분리해 본 뒤 트랜잭션·AOP·비동기 처리 등을 필요한 곳에 추가한다.
참고: Spring IoC, 빈 스코프, Spring AOP.