컨테이너부터 AOP까지 — 연결된 흐름으로 이해하는 스프링 핵심 개념
스프링은 "공장(컨테이너)"이 부품(빈)을 찍어내고, 조립해서, 제공하는 공장 시스템이다. 아래 흐름이 스프링의 전부다.
각 개념을 비유, 설명, 코드 예제와 함께 완전히 이해하자.
클래스는 객체를 만들기 위한 설계도다. 어떤 필드(상태)와 메서드(행동)를 가질지 정의하지만, 그 자체로는 메모리에 존재하지 않는다. 스프링 없이도 존재하는 순수 자바 개념.
// 클래스 = 설계도. 아직 실체(객체)가 없다. public class UserService { private final UserRepository userRepository; UserService(UserRepository repo) { this.userRepository = repo; } public User findUser(Long id) { return userRepository.findById(id); } }
클래스(설계도)를 기반으로 new 키워드로 메모리에 실제 생성된 실체. 같은 클래스로 여러 객체를 만들 수 있다. 스프링이 없다면 개발자가 직접 new로 생성하고 관리해야 한다.
// 객체 = 클래스를 new해서 실체화한 것 UserRepository repo = new UserRepository(); UserService service = new UserService(repo); // 문제: 개발자가 직접 의존성을 연결해야 함 😫 // 객체가 늘어날수록 연결이 복잡해짐
스프링의 핵심 심장. 빈의 생성·의존성 주입·생명주기 관리를 모두 담당한다. 개발자가 new로 직접 객체를 만들 필요가 없어진다. IoC(제어의 역전)는 "내가 객체를 만드는 것"이 아니라 "컨테이너가 만들어주는 것"을 뜻한다.
// 스프링 컨테이너 생성 및 시작 ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); // 컨테이너가 만든 빈을 꺼내서 사용 UserService svc = ctx.getBean(UserService.class); // → 컨테이너가 UserRepository도 알아서 주입해준 상태
컨테이너가 관리하는 객체. 일반 자바 객체와 기술적으로 같지만, 스프링 컨테이너에 등록되어 관리 받는다는 점이 핵심 차이. 기본적으로 싱글톤(하나만 생성)이다.
// 방법 1: Java Config로 직접 등록 @Configuration public class AppConfig { @Bean // 이 메서드가 반환하는 객체를 빈으로 등록 public UserService userService() { return new UserService(userRepository()); } } // 방법 2: 어노테이션으로 자동 등록 (더 많이 씀) @Service // 이 클래스를 빈으로 자동 등록! public class UserService { ... }
@Component는 "이 클래스를 스프링 빈으로 등록해줘"라는 마커 어노테이션. 컨테이너가 시작할 때 지정된 패키지를 스캔(Component Scan)하여 어노테이션이 붙은 클래스를 자동으로 빈으로 등록한다.
// @Component의 특화된 자식들 @Component // 일반 컴포넌트 (기반) @Service // 비즈니스 로직 레이어 (= @Component) @Repository // 데이터 접근 레이어 (예외 변환 추가) @Controller // 웹 요청 처리 레이어 @RestController // @Controller + @ResponseBody // 사용 예시 @Service public class OrderService { // 스프링이 자동으로 빈 등록 완료! }
객체가 필요로 하는 의존 객체를 직접 생성하지 않고 외부(컨테이너)에서 주입받는 패턴. 결합도를 낮추고 테스트를 쉽게 만든다. 3가지 방식이 있으며 생성자 주입이 권장된다.
// ✅ 권장: 생성자 주입 (불변, 테스트 용이) @Service public class OrderService { private final PaymentService paymentService; public OrderService(PaymentService ps) { this.paymentService = ps; // 컨테이너가 주입 } } // ⚠️ 필드 주입 (편하지만 비권장) @Autowired private PaymentService paymentService; // ⚠️ 수정자 주입 (선택적 의존성에 사용) @Autowired public void setPaymentService(PaymentService ps) { this.paymentService = ps; }
로깅, 트랜잭션, 보안 등 여러 곳에 반복되는 공통 관심사를 핵심 비즈니스 로직과 분리하는 기법. @Transactional이 대표적 AOP 활용 사례.
// AOP 없이: 모든 메서드마다 로그 코드 복붙 😫 public Order createOrder(...) { log.info("메서드 시작"); // 비즈니스 로직 log.info("메서드 끝"); } // AOP로: 어노테이션 하나면 끝 ✅ @Aspect @Component public class LoggingAspect { @Around("execution(* com.example.service.*.*(..))") public Object log(ProceedingJoinPoint pjp) throws Throwable { log.info("시작: {}", pjp.getSignature()); Object result = pjp.proceed(); // 실제 메서드 실행 log.info("완료"); return result; } }
빈이 몇 개의 인스턴스로 존재할지, 얼마나 살아있을지 결정하는 설정.
@Component @Scope("prototype") // 기본은 singleton public class MyPrototypeBean { ... }
컨테이너가 빈을 관리하는 전체 과정. 초기화/소멸 시점에 커스텀 로직(DB 연결, 자원 해제 등)을 끼워 넣을 수 있다.
@Component public class DatabaseConnector { @PostConstruct // 빈 생성 + DI 완료 후 호출 public void init() { // DB 커넥션 풀 초기화 System.out.println("초기화 완료!"); } @PreDestroy // 컨테이너 종료 전 호출 public void destroy() { // DB 연결 종료, 자원 반납 System.out.println("자원 반납!"); } }
스프링 컨테이너 시작부터 종료까지 빈이 어떤 과정을 거치는지 순서대로 따라가 보자.
헷갈리는 개념들을 한 표로 비교해서 차이를 확실히 잡자.
| 개념 | 무엇인가 | 스프링과의 관계 | 핵심 키워드 |
|---|---|---|---|
| Class | 설계도 | 순수 Java 개념. 스프링과 무관하게 존재 | 설계, 청사진, 타입 |
| Object | 설계도로 만든 실체 | new로 생성. 컨테이너 없이도 존재 가능 |
인스턴스, new, 메모리 |
| Bean | 컨테이너가 관리하는 객체 | 컨테이너에 등록된 특별한 객체. 싱글톤 기본 | 등록, 관리, 싱글톤 |
| @Component | 빈 자동 등록 마커 | 클래스를 빈으로 자동 등록하는 어노테이션 | 어노테이션, 스캔, 자동화 |
| Container | 빈 공장 + 관리자 | 스프링의 핵심. 빈 생성·DI·생명주기 전담 | IoC, ApplicationContext |
| DI | 의존 객체 자동 배달 | 컨테이너가 빈 간의 의존성을 자동 연결 | @Autowired, 결합도 낮춤 |
| AOP | 공통 관심사 분리 | 로깅·트랜잭션을 핵심 로직과 분리하는 기법 | @Aspect, @Around, 프록시 |
회원가입 시나리오로 클래스 → 컴포넌트 → 빈 → DI → 서비스 전 과정을 코드로 따라가보자.
/* ─── 1. Repository: DB 접근 담당 ─── */ @Repository // @Component + 예외 변환 public class UserRepository { public void save(User user) { // JPA / JDBC로 DB 저장 } } /* ─── 2. Service: 비즈니스 로직 ─── */ @Service // @Component의 특화 버전 public class UserService { private final UserRepository userRepository; // 생성자 주입 (권장 방식) // @Autowired 생략 가능 (생성자 1개면 자동) public UserService(UserRepository repo) { this.userRepository = repo; } @Transactional // AOP! 트랜잭션 자동 처리 public void join(User user) { // 비즈니스: 중복 검사 등 userRepository.save(user); // 예외 발생 시 자동 rollback } } /* ─── 3. Controller: 웹 요청 처리 ─── */ @RestController @RequestMapping("/users") public class UserController { private final UserService userService; public UserController(UserService svc) { this.userService = svc; // 컨테이너가 주입 } @PostMapping public ResponseEntity join(@RequestBody User user) { userService.join(user); // AOP로 트랜잭션 자동! return ResponseEntity.ok("가입 완료"); } } /* 개발자가 신경쓸 필요 없는 것들: - UserRepository 객체 생성 (new) → 컨테이너가 - UserService에 Repository 주입 → 컨테이너가 - UserController에 Service 주입 → 컨테이너가 - 트랜잭션 시작/커밋/롤백 → AOP(@Transactional)가 ✅ 개발자는 비즈니스 로직에만 집중! */