실행흐름 3차📚 전체 맵실행흐름 완결
06e · 심화 · AOP · Transactional · Builder · Stream

ADVANCED
FLOW

AOP Proxy 객체 생성 순서 · @Around 실행 흐름 · @Transactional 커밋/롤백 · @Builder 객체 생성 · Stream 내부 실행 순서

AOP
AOP — Proxy 객체가 만들어지고 동작하는 순서
A
AOP Proxy 생성 — 기동 시 무슨 일이 일어나나
1
Spring Container · 기동 시
@Aspect 클래스 감지 → Pointcut 분석
@Aspect @Component가 붙은 LogAspect Bean이 Heap에 생성.
Pointcut 표현식 execution(* com.example.service.*.*(..)) 분석 → service 패키지의 모든 메서드가 대상임을 확인.
HEAP — LogAspect Bean 생성
2
Spring Container · CGLIB
대상 Bean을 Proxy로 교체 — Heap에서 일어남
UserService가 Pointcut 대상 → CGLIB이 UserService를 상속한 Proxy 클래스 생성.
원래 new UserService() 객체 대신 new UserService$$SpringCGLIB() Proxy를 Heap에 생성.
ApplicationContext에는 이 Proxy 객체가 등록됩니다.
@Autowired UserService userService하면 이 Proxy를 받게 됩니다.
HEAP — UserService$$SpringCGLIB (Proxy 객체) 생성 METHOD AREA — Proxy 클래스 바이트코드 (런타임 생성)
B
@Around 실행 흐름 — 메서드 호출 시 실제 코드 실행 순서
LogAspect.java + UserService.login() 호출 시 실행 순서
// 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가 없으면 vs 있으면 Stack 비교

AOP 없음: Controller.write() → UserService.login() → Stack 2 프레임
AOP 있음: Controller.write() → Proxy.login() → logAround() → UserService.login() → Stack 4 프레임

AOP는 코드를 건드리지 않고 Stack에 프레임을 끼워 넣는 것입니다.

@Transactional
@Transactional — 커밋/롤백 코드 실행 흐름
📌 @Transactional도 AOP Proxy다

@Transactional이 붙은 메서드도 AOP Proxy가 감쌉니다.
개발자가 작성한 deleteUser()를 Proxy가 가로채서 트랜잭션 시작/커밋/롤백을 자동 처리합니다.

1
Controller → @Transactional Proxy
adminService.deleteUser(id) 호출 → Proxy가 가로챔
adminService는 실제로 AdminService$$SpringCGLIB Proxy.
deleteUser() 호출 → Proxy의 트랜잭션 Advice가 먼저 실행됩니다.
HEAP — AdminService Proxy Bean (기동 시 생성)
2
Proxy · TransactionInterceptor
트랜잭션 시작 — DB 커넥션 획득 + BEGIN
HikariPool에서 DB 커넥션 하나 꺼냄 → BEGIN 실행 →
현재 스레드의 Thread-Local에 커넥션 저장
(이후 같은 스레드의 Repository들이 이 커넥션 공유)
HEAP — DB Connection (커넥션 풀에서 꺼냄) THREAD-LOCAL — Connection 저장
3
AdminService.deleteUser() 실행
실제 코드 실행 — DELETE 쿼리들 실행 (아직 커밋 안 됨)
실행 순서
// [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 = ?
// 아직 확정 안 됨
4a
정상 종료 → COMMIT
deleteUser() 정상 종료 → Proxy가 COMMIT 실행
예외 없이 메서드 끝남 → COMMIT 실행 → 두 DELETE 모두 DB에 확정 → Thread-Local에서 커넥션 제거 → HikariPool에 반납.
HEAP — DB Connection → 커넥션 풀로 반납
4b
예외 발생 → ROLLBACK
RuntimeException 발생 → Proxy가 ROLLBACK 실행
RuntimeException (또는 Error) 발생 → ROLLBACK 실행 → 두 DELETE 모두 취소 → DB는 deleteUser() 호출 전 상태로 복구 → 커넥션 반납.

주의: CheckedException(IOException 등)은 기본적으로 롤백 안 됨. @Transactional(rollbackFor = Exception.class)로 설정 가능.
ROLLBACK — 두 DELETE 모두 취소
@Builder
@Builder — 객체 생성 코드 실행 순서
1
컴파일 시 — Method Area
Lombok이 Builder 클래스 자동 생성
@Builder가 붙은 클래스를 Lombok이 컴파일 시 처리 →
Board.BoardBuilder 내부 클래스 자동 생성 →
.class 파일에 포함 → 실행 시 Method Area에 로딩.
METHOD AREA — Board.BoardBuilder 클래스
2
런타임 · Heap
Board.builder() 호출 → 실행 순서
Builder 패턴 실행 순서
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 참조)
HEAP — new BoardBuilder() → new Board() (Builder는 GC) STACK — board 지역변수 (Board 참조)
📌 Builder vs 생성자 vs setter — 메모리 차이

생성자: new Board(title, content, user) → Heap에 Board 1개만 생성. 순서 실수 위험.
setter: new Board() 후 set 반복 → 불완전한 객체가 잠깐 존재 (title 없는 Board).
Builder: new BoardBuilder()로 먼저 담고 → build()로 완성된 Board 한 번에 생성.
Builder 객체는 임시로 Heap 사용 후 GC. 완성 보장, 가독성 높음.

Stream API
Stream API — 내부 실행 순서와 메모리
1
Stream 생성 — 게으른 평가(Lazy Evaluation)
stream() 호출 — 아직 아무것도 실행 안 됨
AdminService.java — getAllUsers() 실행 순서
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 대상
}
2
collect() 실행 시 — 실제 처리 순서
User[0] 처리 → User[1] 처리 → ... → List 완성
collect()가 호출되면 내부적으로:
User[0] 꺼냄 → map 람다 실행 → UserDto[0] 생성 (Heap) →
User[1] 꺼냄 → map 람다 실행 → UserDto[1] 생성 (Heap) →
... 반복 → 최종 List<UserDto> 완성.

중간 연산이 여러 개여도 각 원소를 파이프라인으로 한 번에 처리합니다.
(filter → map → limit이면 원소 하나가 filter→map→limit을 통과한 후 다음 원소 처리)
HEAP — UserDto 객체들, 최종 List<UserDto>
📌 Stream vs for 루프 — 메모리 비교

for 루프:

  • 명시적으로 새 List 생성 → 하나씩 add
  • 중간 결과 List가 필요할 때 명시적으로 생성
  • 메모리 흐름이 눈에 보임

Stream:

  • Stream/Spliterator 내부 객체들이 Heap에 생성됨 (오버헤드)
  • collect() 시점에 한꺼번에 처리 — 중간 List 불필요
  • 데이터 변환 파이프라인이 선언적으로 표현 → 가독성
  • 대용량에서는 parallelStream()으로 멀티스레드 처리 가능
실행흐름 3차📚 전체 맵실행흐름 완결