DI 3방식📚 전체 맵JPA 영속성
07e · DI 완전 이해 · 시리즈 5/5 · 완결

PROJECT
DI MAP

1차 Thymeleaf+Session · 2차 React+JWT · 3차 Spring Security · 심화 AOP
실제 코드에서 DI 전체 찾기 + Bean 의존성 그래프 완성

Big Picture First
최종 정리 — 전체 시리즈 마무리
07a 조감도
07b 클래스·필드·생성자
07c IoC·Container
07d DI 3방식
07e 실제 코드 ← 지금 여기 (완결)
📌 07e의 목표

07a~07d에서 배운 DI 개념을 우리가 직접 만든 1차~3차 프로젝트 코드에서 찾아봅니다.
"아, 저 코드가 DI였구나"가 보이면 완성입니다.
마지막으로 1차~3차 전체 Bean 의존성 그래프를 완성합니다.

Project 1
1차 프로젝트 — Thymeleaf + Session에서 DI 찾기
1
1차 · Thymeleaf + Session · 포트 8081
브랜치: master · 서버 사이드 렌더링 · HTTP 세션 인증
UserController — DI 완전 분석
UserController.java — DI 발생 지점 전체 주석
@Controller
// ↑ @Component 계열 → 스프링이 스캔 → Bean 후보 등록
//   @Controller = 웹 요청 처리 + @Component 기능 포함
@RequiredArgsConstructor
// ↑ Lombok: final 필드를 모두 받는 생성자 자동 생성
//   → 컴파일 후 .class에 생성자 포함
//   → 스프링이 그 생성자로 DI 처리
public class UserController {

    private final UserService userService;
    // ↑ DI 지점 ①
    //   final → 생성자 주입 방식
    //   스프링이 UserService Bean을 Heap에 만들어서 여기 연결
    //   UserController 라이프사이클 동안 불변
    //
    // 기동 시 Heap 상태:
    // UserService Bean(0x1A2B)이 먼저 Heap에 올라감
    // → UserController 생성자(UserService) 호출
    // → this.userService = 0x1A2B
    // → UserController Bean(0xABCD) Heap에 완성

    @GetMapping("/register")
    public String registerForm() {
        return "register";
    }

    @PostMapping("/register")
    public String register(UserDto userDto, Model model) {
        userService.register(userDto);
        // ↑ DI 사용 지점
        //   this.userService(0x1A2B)의 register() 호출
        //   Stack: register() 프레임 → userService.register() 프레임
        return "redirect:/login";
    }
}
UserService — DI 완전 분석
UserService.java — DI 발생 지점 전체 주석
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    // ↑ DI 지점 ②
    //   UserRepository = JPA Repository
    //   스프링이 UserRepository 구현체를 Heap에 생성해서 주입
    //   (우리는 interface만 선언. 스프링이 구현체 자동 생성)

    private final BCryptPasswordEncoder passwordEncoder;
    // ↑ DI 지점 ③
    //   SecurityConfig의 @Bean 메서드로 등록된 Bean
    //   또는 @Component로 등록
    //   비밀번호 암호화에 사용

    public void register(UserDto userDto) {
        // Stack: register() 프레임
        User user = User.builder()
            .username(userDto.getUsername())
            .password(passwordEncoder.encode(userDto.getPassword()))
            //                 ↑ DI로 주입받은 passwordEncoder Bean 사용
            .build();
        userRepository.save(user);
        //  ↑ DI로 주입받은 userRepository Bean 사용
    }
}

// ═══ 1차 DI 흐름 요약 ═══════════════════════════════════
// BCryptPasswordEncoder Bean (의존 없음) → 먼저 생성
// UserRepository 구현체 Bean (의존 없음) → 먼저 생성
// UserService Bean (위 2개 의존) → 그 다음 생성
// UserController Bean (UserService 의존) → 마지막 생성
// ═══════════════════════════════════════════════════════
1차 Bean 의존성 그래프 — 생성 순서 ①→②→③→④
Layer 4 — Controller (마지막 생성)
UserController
BoardController
↓ 의존
Layer 3 — Service
UserService
BoardService
↓ 의존
Layer 2 — Repository + Util (먼저 생성)
UserRepository
BoardRepository
BCryptPasswordEncoder
↓ 의존 (JPA 인프라)
Layer 1 — 인프라 (JVM 시작 시 자동)
DataSource (DB 커넥션풀)
EntityManagerFactory
Project 2
2차 프로젝트 — React + JWT에서 DI 찾기
2
2차 · React + JWT · 포트 8082/3001
브랜치: feature/react · REST API · JWT 토큰 인증
2차 추가 DI — JwtUtil 주입
2차 새로 추가된 DI 구조 — JwtUtil 주입 흐름
// ──────────────────────────────────────────────────────
// JwtUtil — @Component, 의존성 없음 → 제일 먼저 생성
// ──────────────────────────────────────────────────────
@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secretKey;
    // ↑ @Value = 프로퍼티 값 주입 (DI의 한 종류)
    //   application.properties의 jwt.secret 값을 주입
    //   Bean 생성 후 스프링이 필드에 값 주입 (필드 주입 방식의 프로퍼티 버전)

    public String generateToken(String username) { ... }
    public boolean validateToken(String token) { ... }
    public String getUsername(String token) { ... }
}

// ──────────────────────────────────────────────────────
// UserService — JwtUtil DI 추가됨
// ──────────────────────────────────────────────────────
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;    // DI ②
    private final BCryptPasswordEncoder passwordEncoder; // DI ③
    private final JwtUtil jwtUtil;
    // ↑ DI 지점 ④ (2차에서 추가)
    //   JwtUtil Bean이 먼저 Heap에 올라간 후 여기 연결

    public String login(LoginDto dto) {
        User user = userRepository.findByUsername(dto.getUsername())
            .orElseThrow(...);
        if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) {
            throw new RuntimeException("비밀번호 불일치");
        }
        return jwtUtil.generateToken(user.getUsername());
        //              ↑ DI로 주입받은 JwtUtil Bean의 메서드 호출
        //                Heap의 JwtUtil Bean에 접근 → 토큰 생성
    }
}

// ──────────────────────────────────────────────────────
// JwtFilter — JwtUtil DI (2차 핵심)
// ──────────────────────────────────────────────────────
@Component
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    // ↑ DI 지점 ⑤
    //   같은 JwtUtil Bean (Singleton)
    //   UserService.jwtUtil과 JwtFilter.jwtUtil은 Heap의 같은 객체를 가리킴!
    //   → Singleton이기 때문에 공유

    @Override
    protected void doFilterInternal(...) {
        String token = ...getHeader("Authorization");
        if (jwtUtil.validateToken(token)) {
            // ↑ DI로 받은 JwtUtil Bean으로 토큰 검증
            String username = jwtUtil.getUsername(token);
            request.setAttribute("username", username);
        }
        filterChain.doFilter(request, response);
    }
}
📌 2차 핵심 DI 포인트 — Singleton 공유

UserService.jwtUtilJwtFilter.jwtUtil이 가리키는 건 Heap의 같은 JwtUtil 객체입니다.
Singleton이기 때문에 어디서든 같은 JwtUtil Bean을 씁니다.

@Value("${jwt.secret}")로 주입된 secretKey도 딱 한 번 주입되어 Heap의 JwtUtil 객체 안에 저장됩니다.
그래서 모든 토큰 생성/검증에서 같은 secretKey를 씁니다 (서명 일치 보장).

Project 3
3차 프로젝트 — Spring Security에서 DI 찾기
3
3차 · Spring Security · 포트 8083/3002
브랜치: feature/security · JWT + Security FilterChain · 관리자 권한
SecurityConfig — @Configuration + @Bean DI 구조
SecurityConfig.java — DI + @Bean 주입 전체 분석
@Configuration
// ↑ Bean 설정 클래스. 스프링이 스캔. CGLIB Proxy로 만들어짐.
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    // ↑ DI 지점 ⑥ (3차 추가)
    //   JwtAuthenticationFilter Bean이 Heap에 먼저 생성됨
    //   (JwtAuthFilter는 @Component로 등록됨)
    //   SecurityConfig 생성자에 주입됨

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        // @Bean 메서드 → 스프링이 딱 한 번 호출
        // 반환값 Heap에 저장 → BCryptPasswordEncoder Bean 등록
        // 이후 UserService 등 여러 곳에 DI로 주입됨
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // ↑ HttpSecurity = 메서드 매개변수 DI
        //   스프링 Security가 HttpSecurity Bean을 만들어서 여기 주입
        //   → 이것도 DI!

        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(s ->
                s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/user/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .addFilterBefore(
                jwtAuthenticationFilter,
                //  ↑ this.jwtAuthenticationFilter (DI로 받은 Bean)
                //    FilterChain에 등록 = Heap의 JwtAuthFilter 참조를
                //    SecurityFilterChain 객체 안에 연결
                UsernamePasswordAuthenticationFilter.class);

        return http.build();
        // ↑ SecurityFilterChain 객체 생성 → Heap에 저장 → Bean 등록
    }
}

// ──────────────────────────────────────────────────────
// JwtAuthenticationFilter — DI 심층 분석
// ──────────────────────────────────────────────────────
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    // ↑ DI ⑦ — 2차와 같은 JwtUtil Singleton Bean

    private final CustomUserDetailsService customUserDetailsService;
    // ↑ DI ⑧ — 3차 새로 추가
    //   DB에서 유저 정보 로드하는 서비스
}

// ──────────────────────────────────────────────────────
// CustomUserDetailsService — DI 구조
// ──────────────────────────────────────────────────────
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;
    // ↑ DI ⑨ — UserRepository Singleton Bean
    //   1차부터 이미 있던 Bean. 3차에서도 같은 Bean 재사용.
}
3차 Bean 의존성 그래프 — 생성 순서 (아래 → 위)
Layer 5 — Controller (마지막)
UserController
BoardController
AdminController
↓ 의존
Layer 4 — Service + Security Config
UserService
BoardService
AdminService
SecurityConfig
↓ 의존
Layer 3 — Filter + UserDetailsService
JwtAuthenticationFilter
CustomUserDetailsService
BCryptPasswordEncoder
↓ 의존
Layer 2 — Repository + Util (의존성 없음, 먼저 생성)
UserRepository
BoardRepository
AdminRepository
JwtUtil
↓ 의존 (인프라)
Layer 1 — 인프라 (자동)
DataSource
EntityManagerFactory
HttpSecurity
Advanced
심화 — AOP와 @Transactional에서 DI는 어떻게 작동하나
+
심화 · AOP + @Transactional · DI 관계
Proxy Bean이 만들어지는 과정과 DI의 관계
AOP + @Transactional — DI와의 관계 완전 분석
// ──────────────────────────────────────────────────────
// @Transactional이 붙은 Service
// ──────────────────────────────────────────────────────
@Service
@RequiredArgsConstructor
public class AdminService {

    private final UserRepository userRepository;
    private final BoardRepository boardRepository;

    @Transactional
    public void deleteUser(Long userId) {
        // 트랜잭션 처리 — AOP Proxy가 담당
    }
}

// ──────────────────────────────────────────────────────
// 기동 시 DI와 AOP가 만나는 순서
// ──────────────────────────────────────────────────────

// [1단계] DI 처리 (07c에서 배운 것)
// UserRepository Bean → Heap 0x1A2B
// BoardRepository Bean → Heap 0x2B3C
// AdminService(userRepo, boardRepo) → Heap 0x3C4D
//   → this.userRepository = 0x1A2B
//   → this.boardRepository = 0x2B3C
//   → AdminService 원본 Bean 완성

// [2단계] AOP BeanPostProcessor 처리
// @Transactional 감지 → CGLIB으로 AdminService를 상속한 Proxy 생성
// AdminService$$SpringCGLIB$$0 → Heap 0x5E6F
//   → 내부에 원본 AdminService(0x3C4D) 참조 포함
//   → ApplicationContext에 0x3C4D 대신 0x5E6F 등록!

// [3단계] AdminController DI
// AdminController(AdminService adminService)
//   → 스프링이 ApplicationContext에서 "adminService" 꺼냄
//   → 0x5E6F (Proxy) 반환!
//   → this.adminService = 0x5E6F (Proxy가 주입됨)
//
// AdminController는 Proxy인지 모름
// → adminService.deleteUser() 호출 = Proxy.deleteUser() 호출
// → Proxy가 트랜잭션 시작 → 원본 AdminService.deleteUser() 호출

// ── 핵심 포인트 ────────────────────────────────────────
// DI는 항상 "ApplicationContext에 등록된 Bean"을 주입
// @Transactional/@Aspect 대상이면 Proxy가 등록됨
// → 자동으로 Proxy가 주입됨. 개발자는 신경 안 써도 됨.
📌 AOP + DI 관계 핵심 한 줄

DI는 ApplicationContext에 있는 걸 넣어줍니다.
AOP는 ApplicationContext에 원본 대신 Proxy를 등록합니다.
그래서 DI로 받은 Service Bean은 항상 Proxy입니다 (트랜잭션/AOP 대상이면).
개발자는 원본 코드만 작성하면, 스프링이 DI + AOP Proxy를 다 처리해줍니다.

Complete List
1차~3차 전체 DI 지점 완전 목록
번호주입받는 클래스주입받는 것방식등장 시점역할
UserControllerUserService생성자 @RequiredArgsConstructor1차~3차회원 관련 비즈니스 로직
UserServiceUserRepository생성자 @RequiredArgsConstructor1차~3차DB 접근 (JPA)
UserServiceBCryptPasswordEncoder생성자 @RequiredArgsConstructor1차~3차비밀번호 암호화
UserServiceJwtUtil생성자 @RequiredArgsConstructor2차~3차JWT 토큰 생성/검증
JwtFilter (2차)JwtUtil생성자 @RequiredArgsConstructor2차요청마다 토큰 검증
SecurityConfigJwtAuthenticationFilter생성자 @RequiredArgsConstructor3차FilterChain에 필터 등록
JwtAuthFilter (3차)JwtUtil생성자 @RequiredArgsConstructor3차토큰 검증
JwtAuthFilter (3차)CustomUserDetailsService생성자 @RequiredArgsConstructor3차DB에서 유저 로드
CustomUserDetailsServiceUserRepository생성자 @RequiredArgsConstructor3차DB 접근
JwtUtiljwt.secret (프로퍼티)@Value 필드 주입2차~3차JWT 서명 키
filterChain()HttpSecurity@Bean 메서드 매개변수 DI3차Security 설정 객체
📌 전체를 꿰뚫는 한 줄

1차~3차 프로젝트에서 @Service, @Controller, @Component 붙은 클래스의 private final 필드
전부 DI입니다.
코드에 new가 보이지 않아도 스프링이 기동 시 의존성 그래프를 분석하고,
올바른 순서로 Bean을 Heap에 만들고, 생성자를 통해 연결합니다.
@RequiredArgsConstructor가 그 생성자 코드를 자동으로 만들어줄 뿐입니다.

Series Complete
07 시리즈 완결 — 전체 흐름 최종 정리
07a — 왜 DI? 전체 조감도
07b — 클래스·필드·생성자·final·this
07c — IoC·Container·Bean 생성 순서
07d — DI 3방식·@RequiredArgsConstructor·순환참조
07e — 실제 코드 전체 DI 지점 완성 ✅
📌 DI 완전 이해 최종 요약
  • 파일 읽기 단계: 클래스 구조 → Method Area. 실행 아님.
  • IoC: 제어권이 개발자 → 스프링으로 역전. DI는 IoC의 구현.
  • ComponentScan: @Service/@Component 클래스 발견 → Bean 후보 등록.
  • 의존성 그래프: 생성자 분석 → 잎 노드 먼저 → Heap에 순서대로 생성.
  • 생성자 주입 + final: 생성 즉시 Heap 연결 완성. 불변 보장. 테스트 용이.
  • @RequiredArgsConstructor: Lombok이 final 필드 생성자 자동 생성. JVM 동작 동일.
  • Singleton: ApplicationContext의 Map에 Bean 하나. 요청마다 재사용.
  • AOP + DI: @Transactional 대상은 Proxy가 Bean으로 등록 → Proxy가 주입됨.
  • 1차~3차 전체: @RequiredArgsConstructor + private final 필드 = 전부 DI.
DI 3방식📚 전체 맵JPA 영속성