@Transactional
애너테이션은 메서드(혹은 클래스, 인터페이스 등)를 트랜잭션 단위로 만듭니다.
여기서 트랜잭션이란, 데이터베이스에서 수행하는 작업의 단위로, 아래와 같은 4가지 특성을 가집니다.
원자성
: 하나의 작업 단위이다. (모두 수행되던가, 전혀 수행되지 않던가)일관성
: 작업 처리 결과에 일관성이 있다. (트랜잭션 진행되는 동안에 발생하는 DB 변경은 무시한다.)독립성
: 둘 이상의 트랜잭션이 동시에 실행될 때, 다른 트랜잭션의 연산에 끼어들지 않는다.지속성
: 트랜잭션이 성공적으로 완료되면, 결과는 영구적으로 반영되어야 한다.
본 아티클에서는 간단하게 원자성을 기준으로 @Transactional
애너테이션을 사용해보겠습니다.
그리고 @Transactional
의 적용 범위(하위 메서드, 상위 메서드)에 대해, Spring 내부의 동작 원리를 통해 간단하게 알아보겠습니다.
트랜잭션이 아닌 경우
아래의 간단한 예시 코드를 먼저 이해해야 합니다.
(Java의 문법과 Spring의 라이브러리, 그리고 약간의 의사코드를 포함하여 작성되었습니다.)
// Dao를 사용하는 Service
public class Service {
privata final Dao dao;
public saveResult(int userId) {
this.dao.saveNameById("name", userId);
this.dao.saveAgeById(40, userId);
}
}
// Service가 사용하는 Dao
public class Dao {
public void saveNameById(String name, int userId) {
// DB에서 id가 userId인 user에 입력 받은 name을 저장하는 의사코드
this.database.executeQuery("UPDATE user SET name = ? WHERE id = ?", name);
}
public void saveAgeById(int age, int userId) {
// DB에서 id가 userId인 user에 입력 받은 age를 저장하는 의사코드
this.database.executeQuery("UPDATE user SET age = ? WHERE id = ?", age);
}
}
Service
의 saveResult()
는 Dao
의 saveNameById()
과 saveAgeById()
라는 2개의 메서드를 호출합니다.
그를 통해 2개의 SQL문이 실행되고, DB에는 2번의 변경이 일어납니다.
이때, 아래와 같이 age
를 저장하던 도중 예외가 발생한다고 가정하겠습니다.
public void saveAge(int age) {
throw new RuntimeException("저장 실패");
this.database.executeQuery("UPDATE user SET age = ? WHERE id = 1", age) // 입력 받은 age를 DB에 저장ㅇ하는 의사코드
}
이런 상황에서 Service
의 save()
메서드가 호출된다면,
DB
에서 해당 user
의 name
까지만 저장되고, age
는 저장되지 않을 것입니다.
예외가 발생한다면 대개의 경우,
name
만 저장되는 상황보다는 name
과 age
가 모두 저장되지 않는 상황이 바람직할 것입니다.
@Transactional
애너테이션으로 그것을 구현해보겠습니다.
트랜잭션을 사용하는 경우
public class Service {
privata final Dao dao;
@Transactional
public saveResult(int userId) {
this.dao.saveNameById("name", userId);
this.dao.saveAgeById(40, userId);
}
}
하나의 단위로 묶이기 원하는 메서드나 @Transactional
애너테이션을 붙입니다.
그럼 해당 메서드가 호출되어 수행이 완료되기 까지 발생하는 DB와의 모든 상호작용이 하나의 단위가 됩니다.
위에서 작성한 코드의 예를 들면, name
와 age
를 저장하는 작업이 하나로 묶여서,
모두 성공하면 반영(commit)되고, 하나라도 실패하거나 중간에 문제가 발생하면 회수(rollback)됩니다.
name
과 age
를 저장하는 Dao
의 메서드 각각을 트랜잭션 단위로 만들고 싶다면,
아래와 같이 사용할 수 있습니다.
public class Dao {
@Transactional
public void saveNameById(String name, int userId) {
// DB에서 id가 userId인 user에 입력 받은 name을 저장하는 의사코드
this.database.executeQuery("UPDATE user SET name = ? WHERE id = ?", name);
}
@Transactional
public void saveAgeById(int age, int userId) {
// DB에서 id가 userId인 user에 입력 받은 age를 저장하는 의사코드
this.database.executeQuery("UPDATE user SET age = ? WHERE id = ?", age);
}
}
클래스나 인터페이스의 모든 메서드에 @Transactional
을 붙여야 한다면,
아래와 같이 클래스나 인터페이스 상단에 1번 붙이는 것으로 대체할 수 있습니다.
@Transactional
public class Dao {
public void saveNameById(String name, int userId) {
// DB에서 id가 userId인 user에 입력 받은 name을 저장하는 의사코드
this.database.executeQuery("UPDATE user SET name = ? WHERE id = ?", name);
}
public void saveAgeById(int age, int userId) {
// DB에서 id가 userId인 user에 입력 받은 age를 저장하는 의사코드
this.database.executeQuery("UPDATE user SET age = ? WHERE id = ?", age);
}
}
스프링 공식 문서에서는, 인터페이스에 주석을 추가하기보다, 구체적인 클래스나 메서드에 주석을 추가할 것을 권장합니다.
예상대로 동작하지 않을 수 있기 때문인데요, 자세한 사항은 공식 문서에서 설명하고 있습니다.
@Transactional의 적용 범위
@Transactional
의 사용 시, 예상대로 동작하지 않을 수 있는 사례 한 가지를 소개하고자 합니다.
그것은 바로 내부 호출입니다.
public class Service {
privata final Dao dao;
public saveResult(int userId) {
this.saveResultByDao(userId); // Dao를 사용하는 로직을 새로운 메서드로 분리
}
@Transactional
public saveResultByDao(int userId) {
this.dao.saveNameById("name", userId);
this.dao.saveAgeById(40, userId);
}
}
(상기 코드는 상위의 Controller
가 Service
의 saveResult()
메서드를 호출한다고 가정하겠습니다.)
위의 코드는, saveResult()
메서드 내부에서 Dao
를 사용하여 DB를 수정하는 로직을 분리한 코드입니다.
name
과 age
를 저장하는 행위가 하나의 트랜잭션으로 묶일 것 같지만, 그렇지 않습니다.
내부에서 호출하는 함수의 @Transational
애너테이션이 무시되기 때문입니다.
@Transactional
은 프록시 모드에서 작동하고, 프록시를 통해 들어오는 외부 메서드 호출만 가로채기 때문입니다. 이를 정확하게 이해하려면 프록시 패턴
을 사용하는 스프링 AOP
를 이해해야 합니다. 본 아티클에서는 이정도로만 다루겠습니다.
(설정을 통해 모드를 변경하면, 내부 호출도 @Transactional
을 적용할 수 있다고 합니다. 자세한 내용은 공식 문서를 참고해주세요.)
정리하자면, @Transactional
은 프록시를 통해 들어오는 외부 메서드 호출만 가로채기 때문에 내부에서 호출되는 메서드에 추가된 @Transactional
은 무시됩니다.
잘못된 내용이나 궁금하신 점을 댓글로 남겨주시면 감사하겠습니다.
감사합니다!
'Java > Spring, Spring Boot' 카테고리의 다른 글
Request(DTO)와 Domain의 검증 책임 (0) | 2023.05.04 |
---|---|
Spring에서 HTML 결과 테스트하기 (feat. RestAssured) (0) | 2023.05.01 |
DAO에서 CRUD의 반환 타입은 어떤 것이 적절할까? (1) | 2023.04.30 |
SSR(Server-Side Rendering)에서 Controller의 구조 (#서버사이드 렌더링) (0) | 2023.04.28 |