이전 포스팅에서는 순수 JDBC 를 사용해 사용자가 직접 트랜잭션을 제어하고 관리하는 방법에 대해서 알아보았다.
근본적이고 "유일한" 방식은 틀림없지만, 매번 연결할 때마다 선언하고 관리하는건 비효율적이다.
때문에 스프링에서는 @Transactional 어노테이션을 제공해 코드의 중복을 없애고 편리하게 관리할 수 있도록 지원한다.
하지만 동작원리는 똑같다.
- 코드예시
@Transactional
public UUID saveUser(User user) {
log.debug("User(service) : 새로운 유저를 저장합니다. {}", user);
return userRepository.save(user);
}
다음과 같은 새로운 유저를 저장하는 예시 코드가 있다.
기존 스프링에서는 Spring Configuration 에 @EnableTransactionManagement 어노테이션을 붙여야하지만, 스프링부트에서는 자동으로 붙여준다.
@Transactional 어노테이션을 붙이면 해당 public 메소드에 내부적으로 데이터베이스 트랜잭션 처리를 해준다.
따라서 기존 코드는 아래와 같은 코드로 바뀐다.
public UUID saveUser(User user) {
Connection connection = dataSource.getConnection();
try (connection) {
log.debug("User(service) : 새로운 유저를 저장합니다. {}", user);
// userRepository.save(user); 와 같은 로그인 로직
connection.commit();
} catch (SQLException e) {
connection.rollback();
}
}
@Transactional 어노테이션을 추가했을 뿐인데, JDBC에 연결할 때 필요한 코드들을 자동으로 삽입해줍니다.
commit 과 rollback 코드도 알아서 추가해주기 때문에 데이터 일관성과 무결성을 보존해준다.
그럼 어떻게 스프링에서는 자동으로 코드를 삽입해줄까 ?
- AOP 와 Proxy
@Transactional 어노테이션은 기본적으로 Spring AOP 이다.
AOP란 관점 지향 프로그래밍의 약자로, 흩어진 관심사들을 하나로 모듈화 시킨 것을 의미한다.
때문에 반복적으로 사용되는 코드를 하나의 모듈로 통일시켜 코드의 반복을 없애고, 수정하기 편하게 바꾼 것을 의미한다.
Spring AOP 는 기본적으로 Proxy 방식으로 동작한다.
Proxy 방식이란 객체를 사용하고자 할 때, 실질적인 객체를 직접 참조하는 것이 아닌 해당 객체를 대행하는 객체를 통해 접근하는 방식을 의미한다. 즉 복사본이라 생각하면 쉽게 와닿을 거라 생각한다.
직접 객체를 호출하게 되면, 원하는 코드 로직에서 해당 객체를 호출해야 하는데, 이 때 부가적으로 호출하는 로직이 포함될 가능성이 높기 때문에 또 다른 코드의 중복이 발생할 확률이 높고 이는 유지보수성이 크게 떨어진다. 때문에 Spring 에서는 Target 이 되는 객체 or 객체의 상위 인터페이스를 상속하는 Proxy 클래스를 생성하고, Proxy 클래스에서 관련된 처리를 진행한다.
Spring 에서 보편적으로 Proxy 객체를 자동으로 생성하는 2가지 방법이 있다.
- JDK Proxy and CGLib Proxy
두 방식 모두 설정을 통해 Spring 이 처리해주는 Proxy 객체이다.
두 방식의 큰 차이점은 Target 의 어느 부분을 상속 받아 구현하느냐이다.
JDK Proxy 방식은 Target 객체의 상위 인터페이스를 상속받아 프록시 객체를 생성한다. 해당 인터페이스를 구현한 클래스가 아니면 사용할 수 없으며, 후에 다른 인터페이스에 의존하게 되도 사용할 수 없다.
CGLib Proxy 방식은 Target 클래스를 직접 상속받아 프록시 객체를 생성한다. 인터페이스를 상속하지 않기 때문에 좀 더 편리하다. 편리하고 성능상에서 조금 더 좋다고 알려졌기 때문에 보편적으로 많이 쓰는 방식이다.
우리가 @Transactional 어노테이션을 사용하면 해당 객체를 초기화 할 뿐 아니라, Proxy 객체 또한 초기화 한다. 때문에 CGLib 라이브러리의 도움을 받아 만든 proxy 객체를 사용하게 되면 실제 객체의 userService 에 코드를 추가한 거와 같은 동작이 실행되게 된다.
Proxy 객체는 database 의 connection 과 transaction 을 연결하고 끊으며, 실제 로직은 상속받은 객체에게 위임하며 로직이 실행된다. 또한 Proxy 객체는 트랜잭션 상태 관리에 대한 결정을 Transaction Manger 에게 위임한다.
Transaction Manger 는 트랜잭션을 알리는 doBegin 메소드와 해당 트랜잭션을 반영하는 doCommit 과 같은 메소드를 포함하고 있으며, 해당 메소드를 통해 트랜잭션을 관리한다.
@Transactional 어노테이션에는 propagation 옵션을 통해 isolation level 을 정해줄 수 있다.
이는 데이터베이스의 격리 시간을 설정하는 옵션으로 복잡한 내용이다.
@Transactional(propagation = Propagation.REQUIRED) // default 로 트랜잭션의 시작을 알린다.
@Transactional(propagation = Propagation.SUPPORTS) // 트랜잭션을 신경쓰지 않는다.
@Transactional(propagation = Propagation.MANDATORY) // 트랜잭션을 따로 열지는 않지만, 아무도 열지 않는다면 에러 처리
@Transactional(propagation = Propagation.NESTED) // 저장점(SavePoint) 만 잡는다.
해당 예시 외에도 다양한 옵션이 있고, 해당 옵션이 필요할 때 추가로 깊게 공부하는 걸 추천한다.
@Transactional 어노테이션을 단지 사용하면 편하게 데이터베이스에 변경사항을 저장하고 관리할 수 있지만, 근본적인 동작원리와 순수 JDBC를 통해 트랜잭션을 관리하는게 매우 중요하다고 생각한다.
인턴십을 통해 직접 JDBC를 사용해보고 트랜잭션을 실제로 관리해본 경험을 해서 참 다행이고 운이 좋다고 생각한다.
'개인 공부 > Spring' 카테고리의 다른 글
[Spring] HashMap 으로 Cache 구현하기 (0) | 2022.11.14 |
---|---|
[Spring] Redis vs EHcache vs HashMap (0) | 2022.11.09 |
[Spring] JDBC 에서 Transaction 관리하는 법 (0) | 2022.11.08 |
[Spring] #9 JPA 엔티티 매핑 (0) | 2022.04.18 |
[Spring] #8 JPA 동작 원리 (0) | 2022.04.16 |