1. JPA의 예외 처리 (Exception Handling)
JPA를 쓰다 보면 서비스 레이어에서 예외가 발생했을 때 데이터가 롤백되는 현상을 보셨을 겁니다. 스프링의 트랜잭션 예외 처리에는 아주 중요한 기본 규칙이 있습니다.
- 언체크 예외 (
RuntimeException,Error): * 하위 비즈니스 로직이나 JPA에서 예외가 터지면, 스프링 프록시는 이를 “복구 불가능한 치명적인 에러”로 판단합니다. 따라서 앞서 배웠던 흐름대로 즉시connection.rollback()을 때려 완전히 데이터베이스를 이전 상태로 되돌립니다. - 체크 예외 (
Exception,ClassNotFoundException등): - 스프링은 체크 예외가 터지면 “비즈니스적으로 발생할 수 있는 정상적인 예외(예: 잔액 부족 등)”라고 판단하여 롤백하지 않고
connection.commit()을 수행해 버립니다. (이걸 막으려면@Transactional(rollbackFor = Exception.class)처럼 옵션을 줘야 합니다.)
우리가 예전에 마스터했던 @RestControllerAdvice 구구차가 대기하고 있다가, 프록시가 안전하게 롤백을 완료한 뒤 던져준 예외 패킷을 가로채서 클라이언트에게 예쁜 JSON 형태로 에러를 내려주는 구조로 이어집니다.
2. N+1 문제 (성능의 절대 악)
JPA를 처음 쓰는 개발자가 운영 환경에서 서버를 터트리는 주범 1위가 바로 이 N+1 문제입니다.
무슨 문제인가?
“데이터 1건(1)을 조회하려 했는데, 연관된 데이터 때문에 추가 쿼리가 N번 더 나가는 현상”
예를 들어, ‘하나의 팀(Team)’이 ‘여러 개의 코드(Code)’를 가지는 1:N 관계(예: 우리의 애랑해 조직과 소속 프로젝트들)라고 가정해 봅시다.
// 모든 팀(10개)을 조회합니다. (쿼리 1번 발동)
List<Team> teams = teamRepository.findAll();
for (Team team : teams) {
// 각 팀의 프로젝트 이름을 출력하려고 꺼내는 순간!
// JPA가 "어? 프로젝트 데이터가 1차 캐시에 없네?" 하고 각 팀마다 추가 쿼리를 날립니다. (쿼리 N번 발동)
System.out.println(team.getProjects().size());
}
- 결과: 나는 분명
findAll()쿼리 1번만 날렸는데, 팀이 10개라면 추가로SELECT * FROM project WHERE team_id = ?라는 쿼리가 10번(N) 더 실행되어 총 11번의 쿼리가 폭발합니다. 만약 팀이 1만 개라면 서버는 바로 뻗어버리겠죠. - 원인: JPA의 연관관계 지연 로딩(Lazy Loading) 속성 때문에 발생합니다. 처음에 팀 객체만 가져오고 프로젝트는 가짜 객체(Proxy)로 채워놨다가, 실시간으로 꺼내 쓸 때마다 DB를 계속 찌르기 때문입니다.
- 해결책: 해결은 의외로 간단합니다. JPA에게 “야, 나중에 따로 찌르지 말고 처음부터 조인(JOIN)해서 한 트럭에 다 실어와!”라고 명령하는
Fetch Join기술을 레포지토리에 적용하면 쿼리 단 1번으로 깔끔하게 해결됩니다.
3. 트랜잭션 전파 (Transaction Propagation)
비즈니스가 복잡해지면 트랜잭션이 이미 켜져 있는 상태에서, 다른 트랜잭션 메서드를 내부적으로 또 호출하는 상황이 무조건 발생합니다. 이때 “두 개의 트랜잭션을 하나로 묶을 것인가? 아니면 각각 독립적으로 실행할 것인가?”를 결정하는 규칙이 전파(Propagation)입니다.
가장 대표적인 2가지 옵션만 알면 끝납니다.
① REQUIRED (기본값)
- 의미: 기존에 켜져 있는 트랜잭션이 있으면 거기에 숟가락을 얹어서(참여) 같이 간다는 뜻입니다. 없다면 새로 만듭니다.
- 동작: 외부 메서드와 내부 메서드가 하나의 커다란 트랜잭션 울타리로 묶입니다. 따라서 내부 메서드에서 에러가 터져서 롤백되면, 외부 메서드까지 싹 다 같이 물리적으로 롤백됩니다.
② REQUIRES_NEW
- 의미: 이미 켜져 있는 트랜잭션이 있든 없든 상관없이, 기존 트랜잭션을 잠시 대기(Suspend)시키고 완전히 새로운 독립된 트랜잭션을 만들겠다는 뜻입니다.
- 동작: 두 메서드는 서로 남남이 됩니다. 예를 들어 ‘회원가입’ 도중 ‘로그 저장’ 기능이 실패하더라도, 로그 저장만 롤백되고 회원가입은 정상적으로 성공시키고 싶을 때 로그 저장 메서드 위에
@Transactional(propagation = Propagation.REQUIRES_NEW)를 붙여서 격리시킵니다.
정리
- 예외 처리: 프록시 객체가
RuntimeException계열이 터지면 DB를 즉시 롤백하고, 체크 예외는 커밋하는 인프라 규칙. - N+1 문제: 지연 로딩 때문에 루프를 돌 때마다 추가 쿼리가 N번 폭발하는 성능 저하 현상. (해결:
Fetch Join) - 트랜잭션 전파: 트랜잭션 안에서 또 다른 트랜잭션이 호출될 때 하나의 울타리로 묶을지(
REQUIRED), 독립시킬지(REQUIRES_NEW) 결정하는 정책.