3차 프로젝트📚 전체 맵심화 1 AOP
Spring Boot Board Project

자바 프로젝트 심화과정

3차 프로젝트(Spring Security) 기반 심화 학습 — 실무 수준의 코드로 완성한다

AOP 로깅 · 컬렉션 · 예외처리 · @Transactional · @Builder

3차 심화는 전체 여정 어디에 있나?

3차(Spring Security)까지 완성하면 기능은 다 만들었다. 3차 심화는 기능을 추가하는 게 아니라 코드 품질을 실무 수준으로 끌어올리는 단계다. 이 파일은 심화과정의 5단계 전체를 한눈에 조망하는 커리큘럼 맵이다.

📍 전체 프로젝트 진화 흐름 — 지금 여기
1차 ✅
Thymeleaf
+ Session
기본 CRUD
세션 인증
2차 ✅
React
+ JWT
프론트/백 분리
토큰 인증
3차 ✅
Spring
Security
ROLE 권한
FilterChain
📌 지금 여기 · 심화
AOP
리팩토링
코드 품질↑
실무 패턴 적용
3차까지 완성 후 남은 문제
  • · 모든 Controller에 try/catch 반복 — 에러 형식 제각각
  • · Service 로그 코드 없음 — 어디서 얼마나 걸리는지 모름
  • · 조회 메서드에 @Transactional readOnly 최적화 없음
  • · DTO 변환이 Setter 방식 — 장황하고 실수하기 쉬움
3차 심화가 해결하는 것
  • · AOP LogAspect 1파일 → 모든 Service 자동 로깅
  • · GlobalExceptionHandler → try/catch 전부 제거
  • · @Transactional(readOnly=true) → 조회 성능 향상
  • · @Builder 패턴 → DTO 생성 명확하고 안전하게
이 파일의 역할: 심화과정 STEP 1~5 전체 커리큘럼 조감도. 각 STEP 상세 내용은 05a~05e 파일에서 깊게 다룬다.
왜 이 심화과정을 하는가

이 심화과정은 하나의 질문에서 시작되었다.

출발점이 된 질문
QUESTION
"1차~4차 프로젝트를 다 익히면
Java 문법과 Spring 개념을 다 이해할 수 있는 거지?"
ANSWER
핵심 70~80%는 커버됩니다.
하지만 아직 다루지 않은 중요한 개념들이 있습니다.
이것들을 3차 코드에 직접 적용하면서 배우는 것이 이 심화과정의 목적입니다.
목적 01
이미 썼지만 모르는 것 정리
컬렉션, @Transactional, 예외처리 등은 1~4차에서 이미 사용됨. 의식하지 못하고 쓴 것을 이론부터 체계적으로 정리한다.
목적 02
3차 코드 완성도 높이기
AOP 로깅, 커스텀 예외처리, @Builder 적용으로 단순 CRUD를 넘어 실무 패턴이 적용된 코드로 업그레이드한다.
목적 03
AI 개발로 나아가기 위한 기반
Spring AI, OpenAI API 연동 등 AI 관련 개발을 위한 탄탄한 Java/Spring 기반을 마련한다.
1차~4차 완성도 분석

무엇을 익혔고 무엇이 부족한지 파악해야 심화과정의 방향이 잡힌다.

✅ 익힌 것
Java 문법Spring Boot
클래스/인터페이스/상속Controller/Service/Repository
접근제어자, 어노테이션JPA, Entity, DTO
Lombok, Optional세션/JWT/Security 인증
Generic, Lambda, StreamCORS, ResponseEntity
try/catch 기본Docker 배포 (4차)
❌ 아직 부족한 것
개념왜 중요한가
AOP로깅/트랜잭션 반복 코드 분리
컬렉션 심화이미 썼지만 이론 정리 안됨
예외처리 심화API 안정성, 일관된 에러 응답
@Transactional 심화트랜잭션 전파, 롤백 조건
@Builder 패턴소개만 했고 실제 활용 부족
차수핵심 기술브랜치포트상태
1차Thymeleaf + 세션 인증master8081✅ 완성
2차React + JWT 인증feature/react8082 / 3001✅ 완성
3차Spring Security + 관리자feature/security8083 / 3002✅ 완성
3차 심화AOP · 컬렉션 · 예외 · 트랜잭션 · Builderfeature/advanced-⬜ 진행 중
4차 (선택)Docker 컨테이너화 + AWS 배포-8080 / 3000선택과정
심화과정 전체 커리큘럼

3차 코드(feature/security)를 기반으로 5단계를 순서대로 진행한다. 각 단계는 이론 → 실제 코드 적용 → 커밋 순으로 진행.

5단계 진행 흐름 — 클릭하면 해당 상세 파일로 이동
STEP 1
LogAspect.java 작성 → 모든 Service 메서드에 자동 로그 적용
실행시간 측정 + 예외 발생 시 자동 알림 · 기존 코드 수정 0줄
STEP 2
List / Map / Set / Iterator / 정렬 / Collections 이론 완전 정리
우리 프로젝트에서 쓰인 컬렉션 전체 연결
STEP 3
CustomException 클래스 + @RestControllerAdvice 전역 예외처리
모든 에러가 일관된 JSON 형식으로 반환되도록 · try/catch 전부 제거
STEP 4
트랜잭션 전파(Propagation) / 롤백 조건 / 더티체킹 심화
우리 프로젝트 Service 코드 기반으로 완전 분석 + readOnly 최적화
STEP 5
Builder 패턴 이론 + DTO에 직접 적용
더 읽기 쉽고 유지보수하기 좋은 코드로 · Setter 방식 완전 교체
진행 원칙
📖
이론 먼저, 코드 나중
개념을 완전히 이해한 뒤 코드 작성. 왜 이렇게 하는지 모르고 복붙하는 것은 의미 없음.
🔗
우리 프로젝트와 연결
추상적인 예시가 아닌 실제 BoardService, UserService 코드로 이해. 익숙한 코드가 가장 빠른 학습.
직접 코드 추가/실행
읽기만 하는 것이 아니라 직접 타이핑하고 실행. 각 STEP 완료 후 git commit으로 기록.
작업 위치
새 브랜치 생성 후 작업
# 3차 코드 기반으로 심화 브랜치 생성
git checkout feature/security
git checkout -b feature/advanced

# 각 STEP 완료 시 커밋
git commit -m "STEP1: AOP 로깅 추가 (LogAspect.java)"
git commit -m "STEP3: 전역 예외처리 추가 (GlobalExceptionHandler)"
git commit -m "STEP5: Builder 패턴 적용"
AOP 로깅 적용

반복되는 로그 코드를 한 곳에 모아 자동으로 처리한다. 기존 코드 한 줄도 수정하지 않는다.

❌ AOP 없을 때
BoardService.java
public void write(BoardDto dto) {
  // 😩 모든 메서드마다 반복
  log.info("write 시작");
  long s = System.currentTimeMillis();
  // 핵심 로직...
  log.info("write 완료: {}ms", now()-s);
}
public void update(...) {
  // 😩 또 반복
  log.info("update 시작");
  long s = System.currentTimeMillis();
  // 핵심 로직...
  log.info("update 완료: {}ms", now()-s);
}
✅ AOP 적용 후
BoardService.java — 핵심만!
public void write(BoardDto dto) {
  // 핵심 로직만!
  boardRepository.save(board);
}
public void update(...) {
  // 핵심 로직만!
  board.setTitle(dto.getTitle());
}
LogAspect.java — 딱 한 번만!
@Aspect @Component @Slf4j
public class LogAspect {
  @Around("execution(* ..service.*.*(..))")
  public Object log(ProceedingJoinPoint jp)
      throws Throwable {
    // 모든 Service에 자동 적용!
  }
}
AOP 핵심 용어 — 딱 4가지만 알면 된다
용어우리 코드 예시
Aspect 부가 기능을 모아둔 클래스 LogAspect.java — 로깅 기능 전체
Pointcut 어디에 적용할지 지정 (표현식) execution(* com.example.board.service.*.*(..))
→ service 패키지의 모든 클래스, 모든 메서드
Advice 언제 실행할지 + 실행할 코드 @Around 메서드 전후 / @AfterThrowing 예외 발생 시
JoinPoint 실제로 Advice가 적용된 지점 BoardService.write() 호출 시점
Advice 종류 — 언제 실행되나
어노테이션실행 시점주요 용도
@Around메서드 실행 전 + 후 모두실행시간 측정, 로깅 (가장 많이 사용)
@Before메서드 실행 전파라미터 검증, 권한 체크
@After메서드 실행 후 (성공/실패 무관)리소스 정리
@AfterReturning메서드 정상 완료 후반환값 로깅
@AfterThrowing예외 발생 시에만예외 로깅, 알림
Pointcut 표현식 — 읽는 법
Pointcut 표현식 분석
// execution( 반환타입  패키지.클래스.메서드(파라미터) )
"execution(* com.example.board.service.*.*(..))"

// 분석:
// *                         → 반환타입 무관 (void, String, List 전부)
// com.example.board.service → 이 패키지 안에 있는
// *                         → 모든 클래스 (BoardService, UserService, AdminService)
// .*                        → 모든 메서드 (write, update, delete, getList ...)
// (..)                      → 파라미터 무관 (0개든 100개든)

// 결과: service 패키지의 모든 메서드에 자동 적용!

// 다른 예시들
"execution(* com.example.board.service.BoardService.*(..))"  // BoardService만
"execution(* com.example.board.service.*.get*(..))"          // get으로 시작하는 메서드만
"execution(public * com.example.board.service.*.*(..))"       // public 메서드만
@Around 완전 해부 — jp.proceed()가 핵심

@Around는 메서드 실행 전후를 모두 감싸는 Advice다. 반드시 jp.proceed()를 호출해야 실제 메서드가 실행된다. 이걸 호출하지 않으면 실제 로직이 실행되지 않는다.

@Around 동작 원리
@Around("serviceLayer()")
public Object logExecutionTime(ProceedingJoinPoint jp) throws Throwable {

    // ① 실제 메서드 실행 전 코드
    log.info("메서드 시작");
    long start = System.currentTimeMillis();

    // ② jp.proceed() = 실제 메서드 실행 (필수!)
    //    BoardService.write() 같은 실제 비즈니스 로직이 여기서 실행됨
    Object result = jp.proceed();

    // ③ 실제 메서드 실행 후 코드
    long end = System.currentTimeMillis();
    log.info("메서드 완료: {}ms", end - start);

    // ④ 반드시 result 반환 (메서드의 실제 반환값)
    return result;
}

// 실행 흐름: 클라이언트가 boardService.write() 호출
// → AOP가 가로챔 → ① 실행 전 코드 → ② jp.proceed() → ③ 실행 후 코드 → ④ 결과 반환
JoinPoint API — 실행 정보 꺼내는 법
JoinPoint 사용법
// ProceedingJoinPoint jp 에서 꺼낼 수 있는 정보들

jp.getTarget().getClass().getSimpleName()
// → "BoardService" (어떤 클래스의 메서드인지)

jp.getSignature().getName()
// → "write" (메서드 이름)

jp.getArgs()
// → [BoardDto(title=테스트), User(id=1)] (파라미터들)

Arrays.toString(jp.getArgs())
// → "[BoardDto(title=테스트, content=내용), User(id=1)]" (문자열 변환)
build.gradle — AOP 의존성 추가

Spring Boot 프로젝트에서 AOP를 사용하려면 spring-boot-starter-aop 의존성을 추가해야 한다. 현재 3차 코드에는 이 의존성이 없으므로 추가가 필요하다.

build.gradle
dependencies {
    implementation "org.springframework.boot:spring-boot-starter-data-jpa"
    implementation "org.springframework.boot:spring-boot-starter-webmvc"
    implementation "org.springframework.boot:spring-boot-starter-security"
    // ↓ 추가!
    implementation "org.springframework.boot:spring-boot-starter-aop"
    implementation "io.jsonwebtoken:jjwt-api:0.12.3"
    runtimeOnly "io.jsonwebtoken:jjwt-impl:0.12.3"
    runtimeOnly "io.jsonwebtoken:jjwt-jackson:0.12.3"
    compileOnly "org.projectlombok:lombok"
    runtimeOnly "org.mariadb.jdbc:mariadb-java-client"
    annotationProcessor "org.projectlombok:lombok"
    testImplementation "org.springframework.boot:spring-boot-starter-test"
}
LogAspect.java 전체 코드
src/main/java/com/example/board/aspect/LogAspect.java
@Slf4j           // log.info() 자동 생성 (Lombok)
@Aspect          // 이 클래스가 AOP 클래스임을 선언
@Component       // Spring Bean으로 등록
public class LogAspect {

    // service 패키지의 모든 클래스, 모든 메서드에 적용
    @Pointcut("execution(* com.example.board.service.*.*(..))")
    public void serviceLayer() {}

    // ① 실행 전후 처리 — 실행시간 측정
    @Around("serviceLayer()")
    public Object logExecutionTime(ProceedingJoinPoint jp) throws Throwable {
        String className = jp.getTarget().getClass().getSimpleName();
        String methodName = jp.getSignature().getName();

        log.info("[START] {}.{}() - 파라미터: {}",
            className, methodName, Arrays.toString(jp.getArgs()));

        long start = System.currentTimeMillis();
        Object result = jp.proceed();   // 실제 메서드 실행 (필수!)
        long time = System.currentTimeMillis() - start;

        log.info("[END] {}.{}() - 실행시간: {}ms", className, methodName, time);
        return result;
    }

    // ② 예외 발생 시에만 실행
    @AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
    public void logException(JoinPoint jp, Exception ex) {
        log.error("[EXCEPTION] {}.{}() - {} : {}",
            jp.getTarget().getClass().getSimpleName(),
            jp.getSignature().getName(),
            ex.getClass().getSimpleName(),
            ex.getMessage());
    }
}
실행 결과 — 콘솔에 자동으로 찍히는 로그
Console Output
// 게시글 작성 API 호출 시
[START] BoardService.write() - 파라미터: [BoardDto(title=테스트, content=내용), User(id=1)]
[END]   BoardService.write() - 실행시간: 23ms

// 로그인 시
[START] UserService.login() - 파라미터: [UserDto(username=hong)]
[END]   UserService.login() - 실행시간: 156ms

// 없는 게시글 조회 시
[START] BoardService.getDetail() - 파라미터: [999]
[EXCEPTION] BoardService.getDetail() - IllegalArgumentException : 없는 게시글입니다.
📁 STEP 1 완료 후 파일 구조

src/main/java/com/example/board/
├── aspect/ ← 새로 만든 폴더
│ └── LogAspect.java ← 새로 만든 파일
├── config/
├── controller/
├── service/ ← 이 안의 모든 메서드에 자동 적용됨
│ ├── BoardService.java
│ ├── UserService.java
│ └── AdminService.java
└── ...

💡 왜 기존 코드를 한 줄도 안 건드려도 되나?

Spring이 @Aspect와 @Component를 보고 자동으로 BoardService, UserService, AdminService 앞뒤에 LogAspect의 코드를 끼워 넣는다. 이게 AOP의 핵심이다. 비즈니스 로직(Service)은 자신이 감시당하는지도 모른다.

Java 컬렉션 완전 정복

우리 프로젝트에서 이미 쓰고 있었지만 이론 정리가 안 되어있던 부분. List, Map, Set부터 정렬, 유틸까지 완전히 정리한다.

컬렉션특징언제 쓰나우리 프로젝트 사용 예
List<T>순서 O, 중복 O게시글 목록처럼 순서 있는 데이터List<BoardDto>, List<GrantedAuthority>
Map<K,V>키-값 쌍API 응답 메시지, 설정값Map.of("message","성공","token",token)
Set<T>순서 X, 중복 X중복 제거가 필요한 데이터태그 기능 등 (현재 미사용)
Page<T>JPA 페이징게시판 목록 페이징Page<Board> boards
List
순서 있는 목록
add/get/remove/size/contains 기본 메서드. Stream과 함께 쓰면 강력.
List<String> list = new ArrayList<>();
list.add("홍길동");
list.get(0);       // "홍길동"
list.size();       // 1
list.contains("홍길동"); // true

// 우리 프로젝트
List.of(new SimpleGrantedAuthority(role));
Map
키-값 쌍
put/get/containsKey/remove. API 응답 JSON에 가장 많이 사용.
Map<String, String> map = new HashMap<>();
map.put("token", token);
map.get("token");    // token 값

// 우리 프로젝트
Map.of("message", "로그인 성공",
       "token", token);
Set
중복 없는 집합
add/contains/remove. 중복 자동 제거. 순서 보장 없음.
Set<String> set = new HashSet<>();
set.add("Java");
set.add("Java"); // 중복! 무시됨
set.size();    // 1

// List 중복 제거 활용
new HashSet<>(listWithDup);
정렬
Comparator
외부에서 정렬 기준 지정. 여러 조건 조합 가능. 실무에서 많이 사용.
// 최신순 정렬
boards.sort(Comparator
    .comparing(Board::getCreatedAt)
    .reversed());

// JPA에서
Sort.by(Sort.Direction.DESC,
       "createdAt");
Collections
유틸 클래스
정렬/최대최소/뒤집기/빈도수 등 컬렉션 관련 정적 메서드 모음.
Collections.sort(list);
Collections.max(list);
Collections.min(list);
Collections.reverse(list);
Collections.shuffle(list);
Collections.emptyList();
Iterator
안전한 순회/삭제
순회 중 삭제가 필요할 때 사용. for-each 중 remove() 하면 예외 발생!
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals("삭제"))
        it.remove(); // 안전!
}
컬렉션 전체 구조 — 한눈에 보기
Collection (인터페이스)
├── List — 순서 있음, 중복 허용
│ ├── ArrayList    ← 우리가 주로 사용 (인덱스 접근 빠름)
│ └── LinkedList  ← 삽입/삭제가 많을 때
├── Set   — 순서 없음, 중복 불허
│ ├── HashSet     ← 순서 상관없을 때 (가장 빠름)
│ └── LinkedHashSet ← 입력 순서 유지
└── Queue — FIFO (선입선출)
Map (별도 인터페이스)
├── HashMap    ← 키-값 쌍, 순서 없음 (가장 많이 사용)
└── LinkedHashMap ← 입력 순서 유지
언제 뭘 쓸까? — 판단 기준
상황사용할 것이유
게시글 목록, 유저 목록처럼 순서 있는 데이터List순서 보장 + 인덱스 접근 가능
API 응답 JSON (message, token, role)Map.of()키-값 쌍, 수정 불가로 안전
태그, 권한처럼 중복 없어야 할 데이터Set자동 중복 제거
목록 순회 중 삭제 필요IteratorConcurrentModificationException 방지
JPA 게시글 최신순 정렬Sort.byJPA Pageable과 연동
컬렉션 최댓값/뒤집기/섞기Collections 유틸정적 유틸 메서드 활용
예외처리 심화

커스텀 예외 클래스 + @ControllerAdvice 전역 예외처리로 모든 에러를 일관된 형식의 JSON으로 반환한다.

❌ 현재 방식의 문제
UserController.java — 현재
// 각 Controller마다 try/catch 반복
@PostMapping("/login")
public ResponseEntity<?> login(...) {
    try {
        String token = userService.login(dto);
        return ResponseEntity.ok(...);
    } catch (Exception e) {
        return ResponseEntity.badRequest()...;
    }
}
// BoardController에도 같은 패턴 반복...
✅ 개선 후
UserController.java — 개선
// try/catch 완전 제거!
@PostMapping("/login")
public ResponseEntity<?> login(...) {
    String token = userService.login(dto);
    return ResponseEntity.ok(
        Map.of("token", token));
    // 예외 → GlobalExceptionHandler 자동 처리
}
@ExceptionHandler와 @ControllerAdvice — 이론
어노테이션역할범위
@ExceptionHandler 특정 예외가 발생했을 때 처리할 메서드 지정 해당 Controller 내에서만 (단독 사용 시)
@ControllerAdvice 모든 Controller에 적용되는 공통 처리기 전체 Controller (전역)
@RestControllerAdvice @ControllerAdvice + @ResponseBody 전체 Controller, JSON 자동 반환

우리 프로젝트는 REST API(JSON 응답)이므로 @RestControllerAdvice를 사용한다. @ControllerAdvice@ResponseBody가 합쳐진 것이다.

exception/CustomException.java
@Getter
public class CustomException extends RuntimeException {
    // RuntimeException 상속 = @Transactional 자동 롤백!
    private final HttpStatus status;
    private final String message;

    // 자주 쓰는 예외 — 정적 팩토리 메서드
    public static CustomException notFound(String msg) {
        return new CustomException(HttpStatus.NOT_FOUND, msg);
    }
    public static CustomException badRequest(String msg) {
        return new CustomException(HttpStatus.BAD_REQUEST, msg);
    }
    public static CustomException unauthorized(String msg) {
        return new CustomException(HttpStatus.UNAUTHORIZED, msg);
    }
}
exception/GlobalExceptionHandler.java
@RestControllerAdvice  // 모든 Controller 예외를 여기서 처리
public class GlobalExceptionHandler {

    @ExceptionHandler(CustomException.class)
    public ResponseEntity<?> handleCustom(CustomException e) {
        return ResponseEntity.status(e.getStatus())
            .body(Map.of("status", e.getStatus().value(),
                         "message", e.getMessage()));
    }

    @ExceptionHandler(IllegalArgumentException.class)  // 기존 코드 호환성 유지
    public ResponseEntity<?> handleIllegal(IllegalArgumentException e) {
        return ResponseEntity.badRequest()
            .body(Map.of("status", 400,
                         "message", e.getMessage()));
    }

    @ExceptionHandler(Exception.class)  // 안전망
    public ResponseEntity<?> handleAll(Exception e) {
        return ResponseEntity.internalServerError()
            .body(Map.of("status", 500,
                         "message", "서버 오류가 발생했습니다."));
    }
}
Service 코드 수정 — IllegalArgumentException → CustomException
❌ 수정 전
BoardService.java (기존)
@Transactional
public void update(Long id, BoardDto dto, User user) {
    Board board = boardRepository.findById(id)
        .orElseThrow(() -> new
            IllegalArgumentException("없는 게시글"));
    if (!board.getUser().getId().equals(user.getId()))
        throw new IllegalArgumentException("권한 없음");
    board.setTitle(dto.getTitle());
}
✅ 수정 후
BoardService.java (수정)
@Transactional
public void update(Long id, BoardDto dto, User user) {
    Board board = boardRepository.findById(id)
        .orElseThrow(() ->
            CustomException.notFound("없는 게시글입니다."));
    if (!board.getUser().getId().equals(user.getId()))
        throw CustomException.forbidden("수정 권한이 없습니다.");
    board.setTitle(dto.getTitle());
}
에러 응답 형식 — 일관된 JSON
API 에러 응답 예시
// 없는 게시글 조회 시 (404)
{ "status": 404, "message": "없는 게시글입니다." }

// 잘못된 비밀번호 (400)
{ "status": 400, "message": "아이디 또는 비밀번호가 틀렸습니다." }

// 수정 권한 없음 (403)
{ "status": 403, "message": "수정 권한이 없습니다." }

// 서버 오류 (500)
{ "status": 500, "message": "서버 오류가 발생했습니다." }
📋 STEP 3 수정/생성할 파일 목록

신규 생성
exception/CustomException.java
exception/GlobalExceptionHandler.java

수정 (try/catch 제거 + CustomException 적용)
service/BoardService.java  · service/UserService.java  · service/AdminService.java
controller/UserController.java  · controller/BoardController.java  · controller/AdminController.java

@Transactional 심화

트랜잭션 전파, 롤백 조건, 더티체킹 — 우리 프로젝트 Service 코드를 기반으로 완전히 분석한다.

트랜잭션 전파 (Propagation)
REQUIRED (기본값)
기존 트랜잭션 있으면 참여, 없으면 새로 생성. 대부분의 경우 이것을 사용.
@Transactional  // = REQUIRED
public void write(BoardDto dto) {
    boardRepository.save(board);
    // 예외 → 자동 롤백!
}
readOnly = true
조회 최적화
수정 감지(더티체킹) 비활성화 → 성능 향상. 조회 메서드에 항상 붙이기.
@Transactional(readOnly = true)
public Page<BoardDto> getList(...) {
    // 조회만 → 스냅샷 안 만듦
    // 성능 향상!
}
더티체킹
save() 없이 자동 UPDATE
@Transactional 끝날 때 변경된 필드 자동 감지 → UPDATE 실행. save() 불필요.
@Transactional
public void update(Long id, BoardDto dto) {
    Board board = repo.findById(id).get();
    board.setTitle(dto.getTitle());
    // save() 없어도 자동 UPDATE!
}
롤백 조건
언제 롤백되나
RuntimeException 발생 시 자동 롤백. Checked Exception은 롤백 안됨 (직접 지정 필요).
// 자동 롤백 O
throw new RuntimeException();
throw new CustomException(...);

// 자동 롤백 X (직접 지정)
@Transactional(rollbackFor = Exception.class)
메서드readOnly이유
BoardService.write()falseINSERT → 롤백 필요
BoardService.update()falseUPDATE → 더티체킹
BoardService.delete()falseDELETE → 롤백 필요
BoardService.getList()true 권장조회만 → 성능 최적화
UserService.register()falseINSERT → 롤백 필요
UserService.login()true 권장조회만 → 성능 최적화
AdminService.getAllUsers()true 권장조회만 → 성능 최적화
AdminService.deleteUser()falseDELETE 2개 → 원자성 보장
AdminService.deleteBoard()falseDELETE → 롤백 필요
트랜잭션이란 — ACID
속성의미예시
Atomicity (원자성)전부 성공 or 전부 실패유저 삭제 시 게시글도 같이 삭제 — 하나라도 실패하면 둘 다 취소
Consistency (일관성)트랜잭션 전후 DB 상태 일관잔액이 음수가 되는 이체 불가
Isolation (격리성)동시 실행 트랜잭션 간 간섭 없음두 사람이 동시에 같은 게시글 수정 시 충돌 방지
Durability (영속성)커밋된 데이터는 영구 보존서버가 꺼져도 저장된 데이터는 유지
롤백 조건 — 언제 자동 롤백되나
예외 종류자동 롤백예시
RuntimeException (unchecked) ✅ 자동 롤백 IllegalArgumentException, NullPointerException, CustomException
Exception (checked) ❌ 롤백 안됨 IOException, SQLException
rollbackFor 직접 지정 ✅ 강제 롤백 @Transactional(rollbackFor = Exception.class)
💡 CustomException이 RuntimeException을 상속한 이유

STEP 3에서 만든 CustomException은 RuntimeException을 상속한다. 그래야 @Transactional의 자동 롤백 대상이 된다. 만약 Exception을 상속하면 예외가 발생해도 롤백되지 않아 DB가 오염될 수 있다.

트랜잭션 전파 (Propagation) — 간단 정리

한 트랜잭션 안에서 다른 @Transactional 메서드를 호출할 때 어떻게 처리할지 결정한다. 우리 프로젝트에서는 기본값인 REQUIRED만 알면 충분하다.

전파 옵션동작사용 시점
REQUIRED (기본값)기존 트랜잭션 있으면 참여, 없으면 새로 생성대부분의 경우 (우리 프로젝트 전부)
REQUIRES_NEW항상 새 트랜잭션 생성 (기존 일시 중단)로그 저장처럼 독립적으로 커밋해야 할 때
NESTED중첩 트랜잭션 (savepoint 활용)부분 롤백이 필요한 복잡한 로직
@Builder 패턴

생성자 방식의 단점을 해결하는 Builder 패턴. Entity와 DTO 생성 코드를 더 명확하고 안전하게 만든다.

😩 생성자 방식
new Board(
    null,
    "제목",
    "내용",
    user,
    0,
    LocalDateTime.now(),
    null
);
// 뭐가 뭔지 모름!
😐 Setter 방식
Board board = new Board();
board.setTitle("제목");
board.setContent("내용");
board.setUser(user);
board.setViewCount(0);
// 필수 필드 빠뜨려도
// 컴파일 에러 안남!
😊 @Builder 방식
Board.builder()
    .title("제목")
    .content("내용")
    .user(user)
    .viewCount(0)
    .build();
// 명확하고 안전!
BoardDto.java — @Builder 적용
@Getter @Builder @NoArgsConstructor @AllArgsConstructor
public class BoardDto {
    private Long id;
    private String title;
    private String content;
    private String nickname;
}

// BoardService - Entity → DTO 변환
private BoardDto toDto(Board board) {
    return BoardDto.builder()
        .id(board.getId())
        .title(board.getTitle())
        .content(board.getContent())
        .nickname(board.getUser().getNickname())
        .build();
}
@Builder 동작 원리 — Lombok이 자동 생성하는 것

클래스에 @Builder를 붙이면 Lombok이 컴파일 시점에 자동으로 Builder 클래스를 생성한다. 직접 코드를 작성할 필요가 없다.

Lombok이 자동으로 만들어주는 것 (이해용)
// Lombok이 자동으로 만들어주는 것 — 직접 작성 불필요
public static BoardDtoBuilder builder() { return new BoardDtoBuilder(); }

public static class BoardDtoBuilder {
    private Long id;
    private String title;
    private String content;
    private String nickname;

    public BoardDtoBuilder id(Long id)       { this.id = id;           return this; }
    public BoardDtoBuilder title(String t)    { this.title = t;         return this; }
    public BoardDtoBuilder content(String c)  { this.content = c;       return this; }
    public BoardDtoBuilder nickname(String n) { this.nickname = n;      return this; }
    public BoardDto        build()            { return new BoardDto(id, title, content, nickname); }
}
UserDto.java — @Builder 적용 + AdminService 사용 예시
UserDto.java + AdminService.java
@Getter @Setter
@Builder            // ← 추가!
@NoArgsConstructor
@AllArgsConstructor  // ← 추가! (@Builder 필수)
public class UserDto {
    private Long id;
    private String username;
    private String password;
    private String nickname;
    private String role;
    private String adminCode;
}

// AdminService.getAllUsers() — Builder 사용 예시
public List<UserDto> getAllUsers() {
    return userRepository.findAll().stream()
        .map(user -> UserDto.builder()
            .id(user.getId())
            .username(user.getUsername())
            .nickname(user.getNickname())
            .role(user.getRole())
            .build())    // Setter 방식보다 훨씬 명확!
        .collect(Collectors.toList());
}
⚠️ @Builder 사용 시 주의사항

@Builder만 쓰면 기본 생성자(NoArgsConstructor)가 사라진다. JPA Entity는 기본 생성자가 필수이므로 반드시 @NoArgsConstructor를 함께 써야 한다.

@NoArgsConstructor@Builder를 함께 쓸 때는 @AllArgsConstructor도 추가해야 한다. @Builder가 전체 파라미터 생성자를 필요로 하기 때문이다.

③ Entity에 @Builder를 쓸 때는 신중하게 — 모든 필드를 외부에서 세팅 가능하게 열어두면 보안 문제가 생길 수 있다. DTO에 사용하는 것이 더 안전하다.

📋 STEP 5 수정할 파일 목록

dto/BoardDto.java    → @Builder, @AllArgsConstructor 추가
dto/UserDto.java     → @Builder, @AllArgsConstructor 추가
service/BoardService.java → toDto() Builder 방식으로 변경
service/AdminService.java → getAllUsers(), getAllBoards() Builder 방식으로 변경

앞으로의 학습 로드맵

심화과정 완료 후 단기 → 중기 → 장기 목표로 단계적으로 나아간다.

단기 목표 — 심화과정
포트폴리오 완성
STEP 1: AOP 로깅 적용
STEP 2: 컬렉션 완전 정복
STEP 3: 예외처리 심화
STEP 4: @Transactional 심화
STEP 5: @Builder 패턴
중기 목표 — 실무 수준
코드 품질 향상
테스트 코드 (JUnit 5)
디자인 패턴 (싱글톤/팩토리/전략)
QueryDSL (동적 쿼리/검색)
Redis 캐싱
장기 목표 — AI 개발
AI 관련 개발
Spring Cloud / MSA
Spring AI
OpenAI API 연동
벡터 DB / RAG 구현
항목3차 완성 후심화과정 완성 후
기본 CRUD✅ 완성✅ 유지
인증/인가✅ JWT + Security✅ 유지
로깅❌ 없음✅ AOP 자동 로깅 (STEP 1)
컬렉션 이론⚠️ 쓰고 있지만 정리 안됨✅ 완전 정리 (STEP 2)
예외처리⚠️ Controller마다 try/catch✅ 전역 예외처리 (STEP 3)
트랜잭션⚠️ 기본 @Transactional만✅ readOnly + 전파 이해 (STEP 4)
DTO 생성 방식⚠️ Setter 방식 (장황)✅ @Builder 패턴 (STEP 5)
포트폴리오 완성도약 70%약 90%
3차 프로젝트 → 심화과정 → 실무 수준 완성
AOP · 컬렉션 · 예외처리 · @Transactional · @Builder — 5가지 심화 주제를 통해 Java/Spring 실력을 한 단계 올린다
3차 프로젝트📚 전체 맵심화 1 AOP