1차 Thymeleaf+Session · 2차 React+JWT · 3차 Spring Security · 심화 AOP
실제 코드에서 DI 전체 찾기 + Bean 의존성 그래프 완성
07a~07d에서 배운 DI 개념을 우리가 직접 만든 1차~3차 프로젝트 코드에서 찾아봅니다.
"아, 저 코드가 DI였구나"가 보이면 완성입니다.
마지막으로 1차~3차 전체 Bean 의존성 그래프를 완성합니다.
@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"; } }
@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 의존) → 마지막 생성 // ═══════════════════════════════════════════════════════
// ────────────────────────────────────────────────────── // 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); } }
UserService.jwtUtil과 JwtFilter.jwtUtil이 가리키는 건 Heap의 같은 JwtUtil 객체입니다.
Singleton이기 때문에 어디서든 같은 JwtUtil Bean을 씁니다.
@Value("${jwt.secret}")로 주입된 secretKey도 딱 한 번 주입되어 Heap의 JwtUtil 객체 안에 저장됩니다.
그래서 모든 토큰 생성/검증에서 같은 secretKey를 씁니다 (서명 일치 보장).
@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 재사용. }
// ────────────────────────────────────────────────────── // @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가 주입됨. 개발자는 신경 안 써도 됨.
DI는 ApplicationContext에 있는 걸 넣어줍니다.
AOP는 ApplicationContext에 원본 대신 Proxy를 등록합니다.
그래서 DI로 받은 Service Bean은 항상 Proxy입니다 (트랜잭션/AOP 대상이면).
개발자는 원본 코드만 작성하면, 스프링이 DI + AOP Proxy를 다 처리해줍니다.
| 번호 | 주입받는 클래스 | 주입받는 것 | 방식 | 등장 시점 | 역할 |
|---|---|---|---|---|---|
| ① | UserController | UserService | 생성자 @RequiredArgsConstructor | 1차~3차 | 회원 관련 비즈니스 로직 |
| ② | UserService | UserRepository | 생성자 @RequiredArgsConstructor | 1차~3차 | DB 접근 (JPA) |
| ③ | UserService | BCryptPasswordEncoder | 생성자 @RequiredArgsConstructor | 1차~3차 | 비밀번호 암호화 |
| ④ | UserService | JwtUtil | 생성자 @RequiredArgsConstructor | 2차~3차 | JWT 토큰 생성/검증 |
| ⑤ | JwtFilter (2차) | JwtUtil | 생성자 @RequiredArgsConstructor | 2차 | 요청마다 토큰 검증 |
| ⑥ | SecurityConfig | JwtAuthenticationFilter | 생성자 @RequiredArgsConstructor | 3차 | FilterChain에 필터 등록 |
| ⑦ | JwtAuthFilter (3차) | JwtUtil | 생성자 @RequiredArgsConstructor | 3차 | 토큰 검증 |
| ⑧ | JwtAuthFilter (3차) | CustomUserDetailsService | 생성자 @RequiredArgsConstructor | 3차 | DB에서 유저 로드 |
| ⑨ | CustomUserDetailsService | UserRepository | 생성자 @RequiredArgsConstructor | 3차 | DB 접근 |
| ⑩ | JwtUtil | jwt.secret (프로퍼티) | @Value 필드 주입 | 2차~3차 | JWT 서명 키 |
| ⑪ | filterChain() | HttpSecurity | @Bean 메서드 매개변수 DI | 3차 | Security 설정 객체 |
1차~3차 프로젝트에서 @Service, @Controller, @Component 붙은 클래스의 private final 필드는
전부 DI입니다.
코드에 new가 보이지 않아도 스프링이 기동 시 의존성 그래프를 분석하고,
올바른 순서로 Bean을 Heap에 만들고, 생성자를 통해 연결합니다.
@RequiredArgsConstructor가 그 생성자 코드를 자동으로 만들어줄 뿐입니다.
@RequiredArgsConstructor + private final 필드 = 전부 DI.