2022. 8. 3. 15:59ㆍLanguage`/Spring
자바에서 DB에 접근하기 위해서는 "JDBC API"를 활용해서 DB에 접근할 수 있다
간단하게 DB에 접근해서 데이터를 받아오는 과정을 설명하면 다음과 같다
1. DB Connection 얻기
2. DB에 보낼 쿼리를 생성하고 Statement or PreparedStatement를 통한 쿼리 flush
3. DB에 쿼리를 보내고 그에 따른 결과 데이터들을 ResultSet을 통해서 받기
4. ResultSet에 담긴 쿼리 결과 데이터들을 원하는 형태로 parsing해서 사용
여기서 또 DB에 접근하는 방식은 3가지로 나눌 수 있다
1. 순수 JDBC
순수한 Native Query를 생성해서 DB로 날리고 그에 대한 결과를 parsing해서 사용하는 방식이다
- 순수 JDBC API 사용
1. SQL 생성
2. flush & ResultSet으로 결과 받아오기
3. SQL 결과 → 객체로 Parsing
2. SQL Mapper
SQL Mapper도 순수 Native Query를 생성해서 날리는 것 까지는 JDBC API를 직접 사용하는 것과 동일하다.
하지만 SQL Mapper의 가장 큰 장점은 "SQL ↔ 객체"의 Mapping을 알아서 해준다는 것이다
3. ORM
ORM 기술중 대표적인 것은 JPA(Hibernate)이고, ORM 기술의 가장 큰 특징은 SQL Query문 자체도 개발자가 생성해낼 필요가 없다는 것이다
트랜잭션 추상화
JDBC 기술에서 트랜잭션을 사용하는 방식과 ORM(JPA)에서 트랜잭션을 사용하는 방식은 약간 다르다
JDBC
void jdbcTransaction() throws SQLException {
Connection con = getConnection();
try {
con.setAutoCommit(false); // 트랜잭션 시작
// 비즈니스 로직 ~~~~
con.commit(); // 트랜잭션 커밋
} catch (Exception e) {
con.rollback(); // 트랜잭션 롤백
} finally {
releaseConnection(con); // 커넥션 반납
}
}
private static Connection getConnection(){
try {
return DriverManager.getConnection(URL, USERNAME, PASSWORD);
} catch (SQLException e) {
throw new IllegalStateException();
}
}
private static void releaseConnection(Connection con){
if(con != null){
try {
con.close();
} catch (SQLException e) {
throw new IllegalStateException();
}
}
}
JDBC에서 트랜잭션을 시작하는 방법은 "Connection의 setAutoCommit을 false로 설정"하면 트랜잭션이 시작된다
ORM(JPA)
@PersistenceContext
private final EntityManager em;
void jpaTransaction() {
EntityTransaction tx = em.getTransaction();
try {
tx.begin(); // 트랜잭션 시작
// 비즈니스 로직 ~~~~
tx.commit(); // 트랜잭션 커밋
} catch (Exception e) {
tx.rollback(); // 트랜잭션 롤백
} finally {
em.close(); // 엔티티 매니저 반납
}
}
JPA에서 트랜잭션을 시작하는 방법은 "EntityManager로부터 Transaction을 얻어오고 tx.begin()을 통해서 트랜잭션을 시작한다"
만약에 개발팀에서 원래는 JDBC 기술에 의존적으로 개발을 하다가 어느 순간 회의를 통해서 ORM 기술로 DB 접근을 하겠다고 결심을 했다면 트랜잭션 뿐만 아니라 JDBC 기술에 의존적으로 개발한 모든 코드를 수정해야 하는 문제가 발생하게 된다
>> 트랜잭션 추상화를 통해서 이러한 문제를 해결해보자
트랜잭션 매니저 (PlatformTransactionManager)
PlatformTransactionManager 인터페이스에는 총 3가지의 메소드가 있고 오른쪽은 트랜잭션 매니저 인터페이스를 각 DB 접근 기술에 맞게 구체화한 클래스들이다
- 스프링 부트에서는 라이브러리를 통해서 어떤 DB 접근 기술(JDBC, JPA, ..)을 사용하는지 자동 인식해서 적절한 트랜잭션 매니저를 스프링 빈으로 등록해준다
- JPA = DatasourceTransactionManager(JpaTransactionManager)
- JDBC = DatasourceTransactionManager(JdbcTransactionManager)
- TransactionStatus getTransaction()
getTransaction을 통해서 "트랜잭션을 시작"할 수 있다
메소드명이 "getTransaction : 트랜잭션 얻기"인 이유는 기존에 진행중인 트랜잭션이 이미 있다면 해당 트랜잭션에 참여할 수 있기 때문이다
>> 이는 트랜잭션 전파 설정이 PROPAGATION으로 되어있을 경우 유효한 개념이다
- void commit() & void rollback()
commit은 트랜잭션을 "커밋"하고, rollback은 트랜잭션을 "롤백"한다
트랜잭션 동기화 매니저
Spring이 제공하는 트랜잭션 매니저는 크게 2가지 역할을 수행한다
(1) 트랜잭션 추상화
JDBC 기술의 트랜잭션과 JPA(ORM) 기술의 트랜잭션은 서로 사용하는 방식이 다르다
이렇게 DB 접근 기술에 따라 서로 다른 트랜잭션 사용 방식을 "TransactionManager"를 통해서 추상화함으로써 공통된 방식으로 사용할 수 있게 되었다
(2) 리소스 동기화
기본적으로 트랜잭션을 유지하기 위해서는 "하나의 트랜잭션은 동일한 커넥션을 사용해야 한다"
DB Connection은 말그대로 ThreadLocal상에서 이루어지는 DB에 대한 모든 접근과 관련이 있다. 따라서 하나의 수행 단위인 트랜잭션 내부에서는 동일한 커넥션을 사용해야만 한다
동일한 커넥션을 유지하기 위해서 사용되는 여러가지 방법이 있지만 만약 파라미터로 계속해서 커넥션을 넘김으로써 커넥션 동기화를 해주면 코드도 더러워질뿐만 아니라 [커넥션 넘기는 메소드 & 커넥션 넘기지 않는 메소드]또한 구분해줘야 하는 불편함이 생기게 된다
>> 리소스 동기화를 위해서 Spring은 "트랜잭션 동기화 매니저"라는 기능을 제공한다
1. 트랜잭션을 시작하기 위해서는 DB Connection이 필요하다. 따라서 트랜잭션 매니저는 Datasource를 통해서 Connection을 만들고 생성한 Connection은 "트랜잭션 동기화 매니저"에 저장한다
2. 트랜잭션 시작을 위해서 "setAutoCommit(false)"로 설정한다
// 비즈니스 로직 호출 & 수행 //
3. 비즈니스 로직 내부적으로 Repository에 접근해야 하면 "트랜잭션 동기화 매니저"에서 Connection을 획득해서 사용한다
4. Repository에 접근하는 로직이 모두 끝나면 Connection을 다시 트랜잭션 동기화 매니저에 반납한다
트랜잭션 동기화 매니저는 특정 트랜잭션에 대한 Connection은 트랜잭션이 완전히 종료가 되지 않는 이상 계속 유지한다
5. 모든 트랜잭션 과정이 끝나게 되면 해당 트랜잭션과 관련있는 Connection은 트랜잭션 동기화 매니저가 전부 제거해준다
트랜잭션 동기화 매니저라는 "커넥션 보관소" 덕분에 하나의 트랜잭션에서 유지되는 커넥션을 손쉽게 관리할 수 있는 것이다
ThreadLocal을 사용하면 각 Thread마다 별도의 저장소가 존재하고, 해당 저장소에는 접근 권한이 있는 하나의 Thread만 접근할 수 있다
@Transactional
위에서 트랜잭션을 시작하는 위치는 "Service Layer"이다.
Service Layer에 트랜잭션 관련한 코드가 섞이게 된다면 "순수한 핵심 비즈니스 로직"만 관리하기 어려운 문제가 존재한다
따라서 "Proxy"개념을 트랜잭션에 도입해서 [트랜잭션 처리 & 서비스 Layer]를 명확하게 구분해주는 것이 올바른 아키텍처 설계 방식이라고 할 수 있다
Spring에서는 @Transactional을 붙여주면 트랜잭션 AOP가 @Transaction을 인식해서 트랜잭션 프록시를 적용시켜준다
선언적 트랜잭션 vs 프로그래밍 트랜잭션
선언적 트랜잭션이란 @Transaction 애노테이션을 활용해서 매우 편리하게 트랜잭션을 적용하는 관리 방식이다.
반면, 프로그래밍 트랜잭션이란 직접 트랜잭션 매니저/트랜잭션 템플릿 등을 사용해서 트랜잭션 관련 코드를 직접 작성하는 관리 방식이다
일반적으로 @Transaction을 붙여주면 "트랜잭션 AOP Proxy"가 앞단에서 트랜잭션과 관련된 모든 로직을 처리해주기 때문에 선언적 트랜잭션에 의한 관리가 더 효율적이고 간단하다