AOP Proxy 객체 생성 순서 · @Around 실행 흐름 · @Transactional 커밋/롤백 · @Builder 객체 생성 · Stream 내부 실행 순서
@Aspect @Component가 붙은 LogAspect Bean이 Heap에 생성.execution(* com.example.service.*.*(..)) 분석 →
service 패키지의 모든 메서드가 대상임을 확인.
UserService가 Pointcut 대상 → CGLIB이 UserService를 상속한 Proxy 클래스 생성.new UserService() 객체 대신 new UserService$$SpringCGLIB() Proxy를 Heap에 생성.@Autowired UserService userService하면 이 Proxy를 받게 됩니다.
// Controller에서 userService.login() 호출 // → 실제로는 Proxy.login() 호출 // ① Stack: Proxy.login() 프레임 push // ② Proxy가 @Around Advice 실행 → LogAspect.logAround() 호출 @Around("execution(* com.example.service.*.*(..))") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { // ③ Stack: logAround() 프레임 push // ④ @Before 부분 실행 String methodName = joinPoint.getSignature().getName(); // "login" → Stack 지역변수 System.out.println("[LOG] " + methodName + " 시작"); long start = System.currentTimeMillis(); Object result = null; try { // ⑤ 핵심: proceed() — 실제 UserService.login() 호출 result = joinPoint.proceed(); // Stack: UserService.login() 프레임 push // → 실제 로그인 로직 실행 // → 완료 후 Stack: UserService.login() 프레임 pop // result → 반환값 (JWT 토큰 문자열) } catch (Exception e) { // ⑥ 예외 발생 시 @AfterThrowing 처리 System.out.println("[ERROR] " + e.getMessage()); throw e; // 예외 다시 던짐 } finally { // ⑦ @After 부분 — 항상 실행 long elapsed = System.currentTimeMillis() - start; System.out.println("[LOG] " + methodName + " 종료 (" + elapsed + "ms)"); } // ⑧ Stack: logAround() 프레임 pop return result; // Controller에 최종 반환 } // 전체 Stack 흐름: // Controller.login() → Proxy.login() → logAround() → UserService.login() // 완료 후: UserService.login() pop → logAround() pop → Proxy.login() pop
AOP 없음: Controller.write() → UserService.login() → Stack 2 프레임
AOP 있음: Controller.write() → Proxy.login() → logAround() → UserService.login() → Stack 4 프레임
AOP는 코드를 건드리지 않고 Stack에 프레임을 끼워 넣는 것입니다.
@Transactional이 붙은 메서드도 AOP Proxy가 감쌉니다.
개발자가 작성한 deleteUser()를 Proxy가 가로채서 트랜잭션 시작/커밋/롤백을 자동 처리합니다.
adminService는 실제로 AdminService$$SpringCGLIB Proxy.deleteUser() 호출 → Proxy의 트랜잭션 Advice가 먼저 실행됩니다.
BEGIN 실행 →// [1] Thread-Local에서 커넥션 꺼내서 SELECT 실행 User user = userRepository.findById(userId)...; // [2] Thread-Local의 같은 커넥션으로 DELETE 실행 boardRepository.deleteByUser(user); // SQL: DELETE FROM board WHERE user_id = ? // DB에 보내지만 아직 확정 안 됨 (트랜잭션 내) // [3] 같은 커넥션으로 DELETE 실행 userRepository.delete(user); // SQL: DELETE FROM users WHERE id = ? // 아직 확정 안 됨
COMMIT 실행 → 두 DELETE 모두 DB에 확정 →
Thread-Local에서 커넥션 제거 → HikariPool에 반납.
ROLLBACK 실행 →
두 DELETE 모두 취소 →
DB는 deleteUser() 호출 전 상태로 복구 →
커넥션 반납.CheckedException(IOException 등)은 기본적으로 롤백 안 됨.
@Transactional(rollbackFor = Exception.class)로 설정 가능.
@Builder가 붙은 클래스를 Lombok이 컴파일 시 처리 →Board.BoardBuilder 내부 클래스 자동 생성 →Board board = Board.builder() .title(boardDto.getTitle()) .content(boardDto.getContent()) .user(user) .build(); // 실행 순서: // [1] Board.builder() → Heap에 new BoardBuilder() 생성 // [2] .title(...) → builder.title 필드에 값 저장 (Heap) // [3] .content(...) → builder.content 필드에 값 저장 (Heap) // [4] .user(...) → builder.user 필드에 값 저장 (Heap) // [5] .build() → new Board(builder) 호출 // → Heap에 최종 Board 객체 생성 // → BoardBuilder 객체는 GC 대상 // [6] board → Stack 지역변수 (Heap의 Board 참조)
생성자: new Board(title, content, user) → Heap에 Board 1개만 생성. 순서 실수 위험.
setter: new Board() 후 set 반복 → 불완전한 객체가 잠깐 존재 (title 없는 Board).
Builder: new BoardBuilder()로 먼저 담고 → build()로 완성된 Board 한 번에 생성.
Builder 객체는 임시로 Heap 사용 후 GC. 완성 보장, 가독성 높음.
public List<UserDto> getAllUsers() { return userRepository.findAll() // [1] SELECT * FROM users → List<User> (Heap) .stream() // [2] List → Stream 객체 생성 (Heap) // 아직 map, collect 실행 안 됨 (Lazy) .map(user -> // [3] 중간 연산 등록 — 람다 객체(Heap) UserDto.builder() // 실제 실행은 collect() 시점에 .id(user.getId()) .username(user.getUsername()) .nickname(user.getNickname()) .role(user.getRole()) .build() // Heap에 UserDto 생성 ) .collect(Collectors.toList()); // [4] 최종 연산 — 여기서 map 람다 실행 // User 하나씩 꺼내 UserDto 생성 // 새 List<UserDto> 생성 (Heap) // 원본 List<User>, Stream 객체 → GC 대상 }
collect()가 호출되면 내부적으로:List<UserDto> 완성.for 루프:
Stream: