| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- redis
- 지연 로딩
- @Transactional
- MSA
- @ComponentScan
- AWS
- dockerhub
- securitycontextholderfilter
- Dead Letter Queue
- Web
- 컨테이너
- Spring Data JPA
- DLQ
- DI
- JPA
- Spring Container
- Routing Key
- JPQL
- mybatis
- docker
- JWT
- JdbcTemplate
- kafka
- 스프링 부트
- Spring
- 페이징
- 쿠버네티스
- docker compose
- CORS
- 서블릿 컨테이너
- Today
- Total
look-forest
요약 본문
DataSource
커넥션을 얻는 방법은 JDBC DriverManager를 직접 사용하거나, 커넥션 풀을 사용하는 등 다양한 방법이 존재한다. DataSource는 커넥션을 획득하는 방법을 추상화하는 인터페이스이다. 핵심 기능은 커넥션 조회 하나이다.

Transaction
데이터베이스 서버에 연결을 요청하고 커넥션을 맺게 되면 데이터베이스 서버는 내부에 세션이라는 것을 만든다.
세션은 트랜잭션을 시작하고, 커밋 또는 롤백을 통해 트랜잭션을 종료한다. 이후 새로운 트랜잭션을 다시 시작할 수 있다.

- 커밋을 호출하기 전까지는 임시로 데이터를 저장하는 것이다. 따라서 해당 트랜잭션을 시작한 세션(사용자)에게만 변경 데이터가 보이고 다른 세션(사용자)에게는 변경 데이터가 보이지 않는다.
- 수동 커밋 모드로 설정하는 것을 트랜잭션을 시작한다고 표현할 수 있다.
- 수동 커밋 설정을 하면 이후에 꼭 commit , rollback 을 호출해야 한다. (호출 안하면 타임아웃 발생하여 rollback)
- 데이터를 변경하려면 락을 획득해야 하고, 트랜잭션을 끝내면 락을 반납한다.
- 데이터를 조회할 때도 락을 획득하고 싶을 때, select for update 구문을 사용하면 된다.
(정산 등 트랜잭션 종료 시점까지 해당 데이터를 다른 곳에서 변경하지 못하도록 강제로 막아야 할 때) - 트랜잭션은 비즈니스 로직이 있는 서비스 계층에서 시작해야 한다.
비즈니스 로직이 잘못되면 해당 비즈니스 로직으로 인해 문제가 되는 부분을 함께 롤백해야 하기 때문.

같은 커넥션을 유지하는 가장 단순한, 고대의 방법은 커넥션을 파라미터로 전달하는 것이다.

스프링과 문제 해결 - 트랜잭션
순수 JDBC로 구현 시 문제점
- 트랜잭션을 사용하기 위해서 javax.sql.DataSource, java.sql.Connection, java.sql.SQLException 같은 JDBC 기술에 의존해야 한다. 향후 JDBC에서 JPA 같은 다른 기술로 바꾸어 사용하게 되면 서비스 코드도 모두 함께 변경해야 한다.
- 핵심 비즈니스 로직과 JDBC 기술이 섞여 있어서 역할 분리가 되지 않는다.
- 똑같은 기능도 트랜잭션용 기능과 트랜잭션을 유지하지 않아도 되는 기능으로 분리해야 한다.(커넥션 전달 O/X)
- 트랜잭션 적용 코드를 보면 반복이 많다. try , catch , finally ...
트랜잭션 추상화
트랜잭션 기능을 추상화하면, 서비스는 특정 트랜잭션 기술에 직접 의존하는 것이 아니므로 코드를 전혀 변경하지 않고, 트랜잭션 기술을 마음껏 변경할 수 있다.
스프링이 제공하는 트랜잭션 추상화 기술을 사용하면 된다.

package org.springframework.transaction;
public interface PlatformTransactionManager extends TransactionManager {
//이름이 getTransaction() 인 이유는 기존에 이미 진행중인 트랜잭션이 있는 경우 해당 트랜잭션에 참여할 수 있기 때문
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
트랜잭션 동기화
스프링이 제공하는 트랜잭션 매니저는 크게 2가지 역할을 한다.
- 트랜잭션 추상화
- 리소스 동기화
트랜잭션을 유지하려면 트랜잭션의 시작부터 끝까지 같은 데이터베이스 커넥션을 유지해아한다.
스프링은 트랜잭션 동기화 매니저를 제공한다. 이것은 쓰레드 로컬( ThreadLocal )을 사용해서 커넥션을 동기화해준다.
트랜잭션 매니저는 내부에서 이 트랜잭션 동기화 매니저를 사용한다. 트랜잭션 동기화 매니저는 쓰레드 로컬을 사용하기 때문에 멀티쓰레드 상황에 안전하게 커넥션을 동기화 할 수 있다. 따라서 커넥션이 필요하면 트랜잭션 동기화 매니저를 통해 커넥션을 획득하면 된다.

동작 방식
- 트랜잭션 매니저는 데이터소스를 통해 커넥션을 만들고 트랜잭션을 시작한다. (DataSourceUtils)
- 트랜잭션 매니저는 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 보관한다. (쓰레드로컬)
- 리포지토리는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다.
- 트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료하고,
커넥션도 닫는다.
참고
쓰레드 로컬을 사용하면 각각의 쓰레드마다 별도의 저장소가 부여된다. 따라서 해당 쓰레드만 해당 데이터에 접근 가능.
트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
- DataSourceUtils.getConnection()
- 트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션을 반환한다.
- 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 새로운 커넥션을 생성해서 반환한다.
- DataSourceUtils.releaseConnection()
- 트랜잭션을 사용하기 위해 동기화된 커넥션은 커넥션을 닫지 않고 그대로 유지해준다.
- 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 해당 커넥션을 닫는다
동작 흐름

트랜잭션 매니저 1 - 트랜잭션 시작

- 서비스 계층에서 transactionManager.getTransaction() 을 호출해서 트랜잭션을 시작한다.
- 트랜잭션을 시작하려면 먼저 커넥션이 필요하다. 트랜잭션 매니저는 내부에서 데이터소스를 사용해서 커넥션을 생성
- 커넥션을 수동 커밋 모드로 변경해서 실제 데이터베이스 트랜잭션을 시작한다.
- 커넥션을 트랜잭션 동기화 매니저에 보관한다.
- 트랜잭션 동기화 매니저는 쓰레드 로컬에 커넥션을 보관한다. (멀티 쓰레드 환경에 안전하게 커넥션을 보관)
트랜잭션 매니저 2 - 로직 실행

- 서비스는 비즈니스 로직을 실행하면서 리포지토리의 메서드들을 호출한다.
- 리포지토리 메서드들은 트랜잭션이 시작된 커넥션이 필요하다. 리포지토리는 DataSourceUtils.getConnection() 을 사용해서 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다. (같은 커넥션을 사용하고, 트랜잭션도 유지)
- 획득한 커넥션을 사용해서 SQL을 데이터베이스에 전달해서 실행한다.
트랜잭션 매니저 3 - 트랜잭션 종료

- 비즈니스 로직이 끝나고 트랜잭션을 종료한다. 트랜잭션은 커밋하거나 롤백하면 종료된다.
- 트랜잭션을 종료하려면 동기화된 커넥션이 필요하다. 트랜잭션 동기화 매니저를 통해 동기화된 커넥션을 획득한다.
- 획득한 커넥션을 통해 데이터베이스에 트랜잭션을 커밋하거나 롤백한다.
- 전체 리소스를 정리한다.
- 트랜잭션 동기화 매니저를 정리한다. 쓰레드 로컬은 사용후 꼭 정리해야 한다.
- con.setAutoCommit(true) 로 되돌린다. 커넥션 풀을 고려해야 한다.
- con.close() 를 호출해셔 커넥션을 종료한다. 커넥션 풀을 사용하는 경우 con.close() 를 호출하면 커넥션 풀에 반환
트랜잭션 문제 해결 - 트랜잭션 템플릿
트랜잭션을 사용하는 로직을 살펴보면 패턴이 반복되는 것을 확인할 수 있다.
이런 형태는 각각의 서비스에서 반복된다. 달라지는 부분은 비즈니스 로직 뿐이다.
이럴 때 템플릿 콜백 패턴을 활용하면 이런 반복 문제를 깔끔하게 해결할 수 있다. 스프링은 TransactionTemplate 이라는 템플릿 클래스를 제공한다.

트랜잭션 템플릿의 기본 동작
- 비즈니스 로직이 정상 수행되면 커밋.
- 언체크 예외가 발생하면 롤백. 그 외의 경우 커밋. (체크 예외의 경우에는 커밋. 관리가 되므로..?)
한계
서비스 로직인데 비즈니스 로직 뿐만 아니라 트랜잭션을 처리하는 기술 로직이 함께 포함되어 있다.
이렇게 비즈니스 로직과 트랜잭션을 처리하는 기술 로직이 한 곳에 있으면 두 관심사를 하나의 클래스에서 처리하게 된다.
이럴 때 스프링 AOP를 통해 프록시를 도입하면 문제를 깔끔하게 해결할 수 있다.
스프링이 제공하는 트랜잭션 AOP
스프링은 트랜잭션 AOP를 처리하기 위한 모든 기능을 제공한다. 스프링 부트를 사용하면 트랜잭션 AOP를 처리하기 위해 필요한 스프링 빈들도 자동으로 등록해준다. 개발자는 트랜잭션 처리가 필요한 곳에 @Transactional 애노테이션만 붙여주면 된다. 스프링의 트랜잭션 AOP는 이 애노테이션을 인식해서 트랜잭션 프록시를 적용해준다.


스프링 부트의 자동 리소스 등록
스프링 부트는 데이터소스와 트랜잭션 매니저를 스프링 빈에 자동으로 등록한다.
- 현재 등록된 라이브러리를 보고 적절한 트랜잭션 매니저 생성
- application.properties 에 있는 속성을 사용해서 DataSource( HikariDataSource )를 생성
자바 예외 기본 원칙
- 기본적으로 언체크(런타임) 예외를 사용하자.
런타임 예외를 사용하면 서비스나 컨트롤러가 복구 불가 능한 예외를 신경쓰지 않아도 된다.
예외를 전환할 때는 꼭! 기존 예외를 포함해야 한다. 그렇지 않으면 스택트레이스를 확인할 때 진짜 원인을 알 수 없다 - 체크 예외는 비즈니스 로직상 의도적으로 던지는 예외에만 사용하자.
해당 예외를 잡아서 반드시 처리해야 하는 문제일 때만 체크 예외를 사용해야 한다 ( 결제시 포인트 부족 예외 등)
스프링과 문제 해결 - 예외 처리, 반복
만약 특정 상황에는 예외를 잡아서 복구하고 싶으면 예외를 어떻게 구분해서 처리할 수 있을까?
SQLException 에는 데이터베이스가 제공하는 errorCode 라는 것이 들어있다.

그런데 SQL ErrorCode는 각각의 데이터베이스 마다 다르므로 DBMS가 변경될 때 마다 ErrorCode도 모두 변경해야 한다.
스프링 예외 추상화
스프링은 앞서 설명한 문제들을 해결하기 위해 데이터 접근과 관련된 예외를 추상화해서 제공한다.

스프링이 제공하는 예외 변환기
스프링은 데이터베이스에서 발생하는 오류 코드를 스프링이 정의한 예외로 자동 변환해주는 변환기를 제공한다.

SQLErrorCodeSQLExceptionTranslator 사용 예

적용 예 - 레포지토리 계층에 직접 적용하면 된다.

JDBC 반복 문제 해결 - JdbcTemplate
스프링은 JDBC의 반복 문제를 해결하기 위해 JdbcTemplate 이라는 템플릿을 제공한다.

JdbcTemplate 은 JDBC로 개발할 때 발생하는 반복을 대부분 해결해준다. 그 뿐만 아니라 지금까지 학습했던,
트랜잭션을 위한 커넥션 동기화는 물론이고, 예외 발생시 스프링 예외 변환기도 자동으로 실행해준다.
(그래서 BadSqlGrammerException이 던져졌었구나..)
'Spring > Spring 데이터 접근 - 핵심 원리' 카테고리의 다른 글
| 숲 보기 (1) | 2024.11.29 |
|---|---|
| 스프링과 문제 해결 - 예외 처리, 반복 (0) | 2024.08.16 |
| 자바 예외 이해 (0) | 2024.08.15 |
| 스프링과 문제 해결 - 트랜잭션 (0) | 2024.08.13 |
| 트랜잭션 이해 (0) | 2024.08.12 |