[Spring] Transactional 어노테이션 왜씀

말하는 감자·2025년 5월 8일

내일배움캠프

목록 보기
55/73

Transactional

세션을 들어도 좀 아리쏭하다
옵션값도 많고 트랜잭션 << 이게 DB에서 특성 달달외우기 형식으로만 알다보니깐
여기서도 그런느낌인줄알았지... 대충맞긴한거같은데

중간에 뇌가빠져서 다시 정리해야할 것 같다.

Spring에서 트랜잭션을 관리하려면 @Transactional 어노테이션을 쓰면 된다.
이 어노테이션은 메서드에 붙이면 트랜잭션 처리를 해주는데,
메서드 실행 중 예외가 터지면 지금까지 수행된 쿼리를 전부 롤백시키고,
예외 없이 끝나면 커밋된다.

알고보니 이건 그냥 단순 어노테이션이 아니라 AOP 기반임.
@Transactional이 붙으면 프록시 객체가 생성돼서 진짜 메서드 호출 전에 트랜잭션을 열고,
예외 발생 여부에 따라 커밋/롤백을 처리함.



💡 예외 처리 기준

기본적으로는 RuntimeException이 발생했을 때만 롤백함.
CheckedException은 기본적으로 롤백 안 함.
(예: IOException, SQLException 같은 거)
→ 왜냐면 이건 우리가 예외처리 코드를 작성할 수 있으니까, 프레임워크가 대신 롤백 안 해주는 거임.
필요하면 rollbackFor 옵션을 써서 CheckedException도 롤백하도록 지정할 수 있음.




관점 지향 프로그래밍과 프록시 패턴

Spring Boot에서는 생각보다 많은 부분에 AOP(관점 지향 프로그래밍)이 적용돼 있다.
그중 대표적인 게 바로 @Transactional

✅ 1. @Transactional이 없을 때

→ 우리가 평소 생각하는 메서드 호출 순서 그대로 동작함.
→ 각 레이어는 그대로 진입하고, 특별한 중간 처리는 없음.

✅ 2. @Transactional이 붙으면

→ Service를 호출할 때, 진짜 Service 객체가 아니라 'Proxy'가 먼저 호출됨
→ 이 프록시 객체가 트랜잭션을 시작하고,
→ 진짜 서비스 메서드를 실행한 다음, 예외 발생 여부에 따라 커밋 또는 롤백을 수행함

정리하자면

@Transactional은 마치 경호원이 대신 먼저 문을 열고 들어가서 상황을 보고 조치하는 것처럼,
Proxy가 대신 호출해서 트랜잭션을 통제하고, 메서드 호출은 그 뒤에 이뤄지는 구조임.

이걸보고나면 "같은 클래스 내에서 @Transactional 메서드를 서로 호출하면 왜 트랜잭션이 안 먹히는가?" 같은 문제도 자연스럽게 이해하게 됨.
(→ 그건 프록시를 우회해버리기 때문)




@Transactional 속성

✅ propagation (트랜잭션 전파 방식)

가장 헷갈리는 부분인데, 핵심만 정리하면 이렇다:


✅ isolation (고립 수준)

DB의 트랜잭션 격리 수준 설정.
SELECT 중에 다른 트랜잭션이 데이터를 바꾸면 보이는지 여부를 조절함.


✅ readOnly(boolean)

  • @Transactional(readOnly = true) → 성능 최적화용
  • 단순 조회라면 insert/update/flush 같은 작업 안 하니까, JPA가 영속성 컨텍스트 줄임
  • 성능 체감은 미미하지만 리더블하고 의미 전달용으로도 좋음

✅ timeout

  • 쿼리가 너무 오래 걸릴 경우 강제 종료시키는 옵션
  • 예: @Transactional(timeout = 5) → 5초 넘으면 롤백
  • 실무에서는 큰 트랜잭션 처리 시나 배치성 로직에서 종종 씀

✅ rollbackFor / noRollbackFor

  • 기본은 RuntimeException만 롤백되는데,
    특정 CheckedException도 롤백하려면 rollbackFor 써야 함
  • 반대로 noRollbackFor로 특정 예외는 롤백 안 되게 만들 수도 있음

즉, @Transactional은 단순 어노테이션이 아니라
AOP + 프록시 + 트랜잭션 상태 감지가 복합적으로 작동하는 구조였고,
옵션들만 잘 이해하면 굉장히 유연하게 트랜잭션 제어가 가능함.

ㅊㅊ
https://velog.io/@kyu0/Spring-Boot-Transactional%EA%B3%BC-%EC%86%8D%EC%84%B1%EB%93%A4-%ED%94%84%EB%A1%9D%EC%8B%9C-%ED%8C%A8%ED%84%B4
https://velog.io/@kshired/Transactional에서-Runtime-Exception-조심하세요
https://hyejikim.tistory.com/87
https://stackoverflow.com/questions/1099025/spring-transactional-what-happens-in-background








OSIV 패턴

요청이 들어오면 DB 세션(영속성 컨텍스트)을 열어두고, 응답이 나갈 때까지 유지하는 방식

Session per Request

session per request란 영속성 세션과 요청 라이프사이클을 같이 묶기 위한 트랜잭션 패턴이다. 스프링은 이 패턴의 자체적으로 구현했는데, 이것이 바로 OpenSessionInViewInterceptor이다. 이 패턴을 통해 lazy 연관관계를 사용하는 것을 용이하게해 개발 생산성을 높여준다.

OSIV를 사용한다면 요청의 시작과 끝동안 영속성 컨텍스트가 유지되기 때문에, Service레이어의 @Transactional을 벗어나 컨트롤러 또는 뷰에서도 lazy 로딩이 가능하다

OSIV를 사용할까 말까?

간단한 서비스라면 OSIV의 생산성 증대가 도움이 될 수도 있다

하지만 조금이라도 서비스가 복잡해지면 OSIV로 인한 문제들이 발생할 수 있다.
특히 외부 호출이 잦거나 트랜잭션 밖에서 로직이 많은 경우 OSIV를 비활성화하는 것이 매우 추천된다

트랜잭션 범위 명확히 관리하고 싶을때도 끄고 Service 내에서만 영속성 사용 권장됨.

즉,

OSIV는 생산성은 높여주지만, 트랜잭션 범위가 흐려지고 성능 문제 생길 수 있음 → 실무에선 끄는 게 기본값

ㅊㅊ
https://velog.io/@sangmin7648/OSIV란
https://devoong2.tistory.com/entry/OSIVOpen-Session-In-View-%EB%8F%99%EC%9E%91%EC%9B%90%EB%A6%AC-%EB%B0%8F-%EC%A3%BC%EC%9D%98%EC%82%AC%ED%95%AD#google_vignette








실습 과제

이건 레벨1이라 10초만에끝냈음

이게문제임 이거때문에 til쓴다

  • manager 등록 시 로그를 남긴다.
  • manager 등록이 실패하더라도 로그(log 테이블)는 무조건 DB에 저장되어야 한다.
  • 이를 위해 로그 저장은 독립적인 트랜잭션에서 실행되어야 한다.

그럼 Transactional을쓰되, saveManager랑 독립적으로 작동하는 Transactional 한 로그저장 메소드가 필요함.. 그것이 바로바로

@Transactional(REQUIRES_NEW)

이거다.

REQUIRES_NEW는 "이건 꼭 따로 커밋돼야 돼!" 같은 중요한 보조작업에 쓰는 옵션이고,
실수하면 메인 트랜잭션까지 망칠 수 있으니 꼭 분리된 서비스에서 호출해야 함.

REQUIRES_NEW는 무슨 역할을 하는 거냐?

메인로직이 영향받지 않으면서 다른 보조역할을 실행시키려고 할 떄 적합하다
이번경우처럼 로그남기려고할 때 많이 사용하는 옵션이라고 한다.

질문답변
ManagerService.saveManager(user)에서 예외 나도 saveLog() 실행되나?❌ 예외 발생 시 실행되지 않느다
REQUIRES_NEW는 어떤 효과가 있나?✅ 별도 트랜잭션으로 커밋/롤백 분리됨
saveLog() 실패해도 메인 로직 살리려면?✅ try-catch로 예외를 잡아야 함

그래서 ManagerService.saveManager(user)을 큰 try-catch문으로 잡아놓고
예외가 발생하는 지점에서 바로 throw하는게 아니라 catch문을 거치되, catch문에서는
로그로 매니저 저장실패 << 를 넣을거고,

정상적으로 저장 다 하고나서 save될때는 저장성공을 로그로 남길생각이다.

@Getter
@Entity
@NoArgsConstructor
@Table(name = "log")
public class Log extends Timestamped {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY, optional = true)
    @JoinColumn(name = "user_id", nullable = true)
    private User user;

    @ManyToOne(fetch = FetchType.LAZY, optional = true)
    @JoinColumn(name = "todo_id", nullable = true)
    private Todo todo;

    private LogStatus status;

    public Log(User user, Todo todo, LogStatus status) {
        this.user = user;
        this.todo = todo;
        this.status = status;
    }
}

이거 로그 엔티티인데 투머친가 아직좀 곰니됨
오류날때는 유저나 todo나 null상태가 될수있기때문에 null옵션 true로 바꿔줌

@Service
@RequiredArgsConstructor
public class LogService {

    private final LogRepository logRepository;

    @Transactional(propagation = REQUIRES_NEW)
    public void saveLog(User user, Todo todo, LogStatus status){
        Log log = new Log(user, todo, status);
        logRepository.save(log);
    }
}

로그 서비스에서는 저장에 필요한 정보들 받아오고,,,,
참고로 상태는 enum 클래스로 관리했음

saveManager는 이렇게 바꿧음

profile
대충 데굴데굴 굴러가는 개발?자

0개의 댓글