서버-DB 동작 원리, JDBC, 트랜잭션, Spring 편의성에 관한 이야기
애플리케이션 서버와 DB 연결 방법
- 커넥션 연결
- SQL 전달
- 결과 응답
하지만 각각의 DB벤더(회사)마다 연결하는 방법, SQL 전달 방법, 결과 응답받는 방법이 모두 다르다.
그래서 이것을 표준화한 JDBC가 등장
JDBC를 사용한 DB연결 방법
JDBC DriverManager 사용
데이터베이스에 연결하려면 JDBC가 제공하는 DriverManager.getConnection(..)를 사용하면 된다.
DriverManager는 라이브러리에 등록된 드라이버 목록을 자동으로 인식한다. 이 드라이버들에게 순서대로 다음 정보를 넘겨서 커넥션을 획득할 수 있는지 확인한다.
- URL, 이름, 비밀번호 등 접속에 필요한 정보를 드라이버 매니저에 넘김
- 각각의 드라이버는 URL 정보를 체크해서 본인이 처리할 수 있는 요청인지 확인한다.
- 요청할 수 있는 드라이버를 찾으면 클라이언트에게 커넥션 구현체를 반환
커넥션 이해
- 드라이버매니저에게 커넥션 조회
- TCP/IP 커넥션 연결
- ID/PW/부가정보 전달
- DB에서 이것을 인증 및 DB 세션 생성
- 커넥션 생성후 드라이버매니저에게 반환
- 드라이버매니저가 요청한 로직에게 반환
커넥션 하나를 만드는 것은 위의 과정처럼 매우 복잡하고 자원이 소모되는 일.
이것을 해결하기위해 커넥션 풀 등장
커넥션 풀 이해
애플리케이션을 시작하는 시점에 커넥션 풀은 필요한 만큼 커넥션을 미리 확보해서 풀에 보관한다.
애플리케이션 로직에서 이제는 직접 DB 드라이버를 통해서 새로운 커넥션을 획득하는 것이 아니다.
이제는 커넥션 풀을 통해 이미 생성되어 있는 커넥션을 객체 참조로 그냥 가져다 쓰기만 하면 된다.
커넥션 풀에 커넥션을 요청하면 커넥션 풀은 자신이 가지고 있는 커넥션 중에 하나를 반환한다.
DataSource 이해
위에서 말했듯이 커넥션을 얻는 방법은 다양하다. (드라이버 매니저에서 직접 생성, 커넥션풀에서 꺼내서 사용)
만약 드라이버 매니저로 직접 생성하는 방법을 사용하다가 커넥션풀로 교체하려면 어떻게 해야 할까? 또는 기존 커넥션풀을 다른 커넥션풀로 변경하고 싶으면 어떻게 해야 할까?
애플리케이션 코드를 모두 변경해야 한다.
이를 해결하기 위해
커넥션을 획득하는 방법을 추상화했다. 이것이 DataSource!
트랜잭션
트랜잭션 ACID
원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 보장해야 한다.
원자성: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야 한다.
일관성: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 예를 들어 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다.
격리성: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어 동시에 같은 데이터를 수정하지 못하도록 해야 한다. 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준 (Isolation level)을 선택할 수 있다.
지속성: 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.
트랜잭션을 시작하려면 커넥션이 필요하다.
애플리케이션에서 DB 트랜잭션을 사용하려면 트랜잭션을 사용하는 동안 같은 커넥션을 유지해야 한다. 그래야 같은 세션을 사용할 수 있다.
가장 간단한 방법은 레포지토리에서 커넥션을 서비스단의 파라미터로 넘겨주어서 같은 커넥션이 사용되도록 유지한다.
하지만 위 방법은 서비스단의 코드가 너무 더러워진다. (순수한 서비스 계층 설계가 깨져버림)
스프링을 사용해서 위 문제를 해결할 수 있다.
트랜잭션 추상화
JDBC, JPA의 트랜잭션 사용방법은 다르다. → 기술을 변경하기 위해서 코드를 다 고쳐야 한다.
그래서 트랜잭션을 추상화한 게 TxManager이다.
트랜잭션 동기화
트랜잭션을 유지하려면 트랜잭션의 시작부터 끝까지 같은 데이터베이스 커넥션을 유지해야 한다.
결국 같은 커넥션을 동기화(맞추어 사용) 하기 위해서 이전에는 파라미터로 커넥션을 전달하는 방법을 사용했다.
스프링은 트랜잭션 동기화 매니저를 제공한다. 이것은 쓰레드 로컬( ThreadLocal )을 사용해서 커넥션을 동기화해준다. 트랜잭션 매니저는 내부에서 이 트랜잭션 동기화 매니저를 사용한다.
트랜잭션 동기화 매니저는 쓰레드 로컬을 사용하기 때문에 멀티쓰레드 상황에 안전하게 커넥션을 동기화할 수 있다. 따라서 커넥션이 필요하면 트랜잭션 동기화 매니저를 통해 커넥션을 획득하면 된다. 따라서 이전처럼 파라미터로 커넥션을 전달하지 않아도 된다.
동작 방식을 간단하게 설명하면 다음과 같다.
- 트랜잭션을 시작하려면 커넥션이 필요하다. 트랜잭션 매니저는 데이터소스를 통해 커넥션을 만들고 트랜잭션을 시작한다. (트랜잭션매니저 생성자에 데이터소스를 넘겨준다.)
- 트랜잭션 매니저는 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 보관한다.
- 리포지토리는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다. 따라서 파라미터로 커넥션을 전달하지 않아도 된다.
- 트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료하고, 커넥션도 닫는다.
트랜잭션 템플릿
트랜잭션을 사용하는 로직은 같은 패턴이 반복된다.
반복 패턴: 트랜잭션을 시작하고, 비즈니스 로직을 실행하고, 성공하면 커밋하고, 예외가 발생해서 실패하면 롤백한다.
트랜잭션 템플릿
템플릿 콜백 패턴을 적용하려면 템플릿을 제공하는 클래스를 작성해야 하는데, 스프링은 TransactionTemplate라는 템플릿 클래스를 제공한다.
- 트랜잭션 템플릿 덕분에, 트랜잭션을 사용할 때 반복하는 코드를 제거할 수 있었다. (커밋, 롤백 코드 제거) 하지만 서비스 로직인데 비즈니스 로직 뿐만 아니라 트랜잭션을 처리하는 기술 로직이 함께 포함되어 있다.
- 비즈니스 로직과 트랜잭션을 처리하는 기술 로직이 한 곳에 있으면 두 관심사를 하나의 클래스에서 처리하게 된다. 결과적으로 코드를 유지보수하기 어려워진다.
- 서비스 로직은 가급적 핵심 비즈니스 로직만 있어야 한다. 하지만 트랜잭션 기술을 사용하려면 어쩔 수 없이 트랜잭션 코드가 나와야 한다. 어떻게 하면 이 문제를 해결할 수 있을까?
트랜잭션 AOP
트랜잭션 프록시가 트랜잭션 처리 로직을 모두 가져간다. 그리고 트랜잭션을 시작한 후에 실제 서비스를 대신 호출한다. 트랜잭션 프록시 덕분에 서비스 계층에는 순수한 비즈니즈 로직만 남길 수 있다.
개발자는 트랜잭션 처리가 필요한 곳에 @Transactional 애노테이션만 붙여주면 된다. 스프링의 트랜잭션 AOP는 이 애노테이션을 인식해서 트랜잭션 프록시를 적용해 준다.
스프링 부트의 자동 리소스 등록
스프링 부트가 등장하기 이전에는 데이터소스와 트랜잭션 매니저를 개발자가 직접 스프링 빈으로 등록해서 사용했다. 그런데 스프링 부트로 개발을 시작한 개발자라면 데이터소스나 트랜잭션 매니저를 직접 등록한 적이 없을 것이다.
스프링 부트는 application.properties에 있는 속성을 사용해서 DataSource를 생성한다. 그리고 스프링 빈에 등록한다.
application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
- 스프링 부트가 기본으로 생성하는 데이터소스는 커넥션풀을 제공하는 HikariDataSource이다. 커넥션풀과 관련된 설정도 application.properties를 통해서 지정할 수 있다.
- spring.datasource.url 속성이 없으면 내장 데이터베이스(메모리 DB)를 생성하려고 시도한다.
트랜잭션 매니저 - 자동 등록
- 스프링 부트는 적절한 트랜잭션 매니저( PlatformTransactionManager )를 자동으로 스프링 빈에 등록한다.
- 자동으로 등록되는 스프링 빈 이름: transactionManager
- 참고로 개발자가 직접 트랜잭션 매니저를 빈으로 등록하면 스프링 부트는 트랜잭션 매니저를 자동으로 등록하지 않는다.
어떤 트랜잭션 매니저를 선택할지는 현재 등록된 라이브러리를 보고 판단
스프링의 예외 추상화
각각의 DB마다 SQL ErrorCode는 다르다.
SQLExceptionTranslator
스프링은 데이터베이스에서 발생하는 오류 코드를 스프링이 정의한 예외로 자동으로 변환해 주는 변환기를 제공한다.
JdbcTemplate
JdbcTemplate은 JDBC로 개발할 때 발생하는 반복을 대부분 해결해 준다. 그뿐만 아니라 지금까지 학습했던, 트랜잭션을 위한 커넥션 동기화는 물론이고, 예외 발생 시 스프링 예외 변환기도 자동으로 실행해 준다.