Spring DB Part I
Spring DB Part I
영한님의 스프링 DB 1편 - 데이터 접근 핵심 원리 강의를 요약한 내용입니다.
Intro
H2 데이터베이스 설정
Download
실행
실행 권한:
chmod 755 h2.sh
실행:
./h2.sh
mv.db 파일 생성:
jdbc:h2:~/test
접속:
jdbc:h2:tcp://localhost/~/test
JDBC
Java Database Connectivity
자바에서 데이터베이스에 접속하기 위해 사용되는 자바 API
Server <-> DB
Connection 연결
: 주로 TCP/IP를 사용해서 커넥션 연결SQL 전달
: 서버는 DB가 이해할 수 있는 SQL을 커넥션으로 DB에 전달Response
: DB는 전달된 SQL을 수행하고 그 결과를 응답 -> 서버는 응답 결과 활용
JDBC 표준 인터페이스
java.sql.Connection
: 연결java.sql.Statement
: SQL을 담은 내용java.sql.ResultSet
: SQL 요청 응답
JDBC 데이터 접근 기술
SQL Mapper
Spring JdbcTemplate
MyBatis
ORM
JPA
hibernate
eclipse link
데이터베이스 연결
JDBC는
java.sql.Connection
표준 커넥션 인터페이스를 정의H2 데이터베이스 드라이버는 JDBC Connection 인터페이스를 구현한
org.h2.jdbc.JdbcConnection
구현체 제공
JDBC가 제공하는
DriverManager
는 라이브러리에 등록된 DB 드라이버들을 관리하고, 커넥션을 획득하는 기능 제공
.
DriverManager 커넥션 요청 흐름
애플리케이션 로직에서 커넥션이 필요하면
DriverManager.getConnection()
호출DriverManager
는 라이브러리에 등록된 드라이버 목록을 자동으로 인식드라이버들에게 순서대로 URL, d이름, 비밀번호 등 접속이 필요한 정보를 넘겨 커넥션을 획득할 수 있는지 확인
각각의 드라이버는 URL 정보를 체크해서 본인이 처리할 수 있는 요청인지 확인
찾은 커넥션 구현체를 클라이언트에 반환
처리가 가능한 드라이버의 경우 실제 데이터베이스에 연결해서 커넥션을 획득하고 이 커넥션을 클라이언트에 반환
반면 URL이 jdbc:h2 로 시작했는데 MySQL 드라이버가 먼저 실행될 경우, 처리할 수 없다는 결과를 반환하게 되고, 다음 드라이버에게 순서가 전달
getConnection() & close()
DriverManager
DataSource
등록
조회
수정, 삭제
Connection Pool & DataSource
데이터베이스 커넥션 획득 과정
서버에서 DB 드라이버를 통해 커넥션 조회
DB 드라이버는 DB와 TCP/IP 커넥션 연결 (3 way handshake 동작 발생)
TCP/IP 커넥션이 연결되면, ID/PW와 기타 부가정보를 DB에 전달
DB는 ID/PW를 통해 내부 인증을 완료하고, 내부 DB 세션 생성
DB는 커넥션 생성이 완료되었다는 응답 전달
DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환
ConnectionPool
커넥션을 관리하는 수영장(!)
DriverManager 를 통해 데이터베이스 커넥션을 매번 새로 생성하는 과정에서 발생하는 응답 속도 저하 문제를 해결하기 위해 커넥션을 미리 생성해두고 사용
ConnectionPool 초기화
애플리케이션 시작 시점에 필요한 만큼의 커넥션을 미리 확보해서 풀에 보관
기본값은 보통 10개
ConnectionPool 연결 상태
커넥션 풀에 들어 있는 커넥션은 TCP/IP로 DB와 커넥션이 연결되어 있는 상태
언제든지 SQL을 DB에 전달 가능
ConnectionPool 사용
커넥션 풀을 통해 이미 생성되어 있는 커넥션을 객체 참조로 얻어서 사용
커넥션을 요청하면 커넥션 풀은 자신이 가지고 있는 커넥션 중 하나를 반환
커넥션 풀로부터 받은 커넥션을 사용해서 SQL을 DB에 전달하고, 그 결과를 받아서 처리
커넥션을 모두 사용하면 커넥션을 종료하지 않고 다시 사용할 수 있도록 커넥션 풀에 반환
DataSource
커넥션을 획득하는 방법을 추상화 하는 인터페이스
커넥션 풀 오픈소스
commons-dbcp2
,tomcat-jdbc pool
,HikariCP
에 직접 의존하는 것이 아니라, DataSource 인터페이스에만 의존하면 된다!
DriverManager
DriverManager
커넥션을 획득할 때 마다 URL/USERNAME/PASSWORD 를 파라미터로 계속 전달
DataSourceDriverManager
반면, 처음 객체를 생성할 때만 필요한 파리미터를 넘기고, 커넥션을 획득할 때는 단순히 dataSource.getConnection() 만 호출
설정
과사용
의 분리가 명확
Connection Pool
커넥션 풀은 별도의 쓰레드 사용해서 커넥션 풀에 커넥션을 채운다.
DriverManagerDataSource 는 항상 새로운 커넥션을 생성하는 반면, 커넥션 풀은 커넥션을 재사용
Transaction
DB에서 트랜잭션은 하나의 작업를 안전하게 처리하도록 보장
커밋(Commit)
: 모든 작업이 성공해서 DB에 정상 반영하는 것롤백(Rollback)
: 작업이 하나라도 실패해서 작업 이전으로 되돌리는 것
트랜잭션 ACID
트랜잭션은 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 보장해야 한다.
원자성(Atomicity)
: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 하거나 모두 실패해야 한다.일관성(Consistency)
: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다.데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다.
격리성(Isolation)
: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다.동시에 같은 데이터를 수정하지 못하도록 해야 한다.
트랜잭션 간에 격리성을 완벽히 보장하려면 트랜잭션을 거의 순서대로 실행해야 하므로 ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의
격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준(Isolation level)을 선택할 수 있다.
READ UNCOMMITED(커밋되지 않은 읽기)
READ COMMITTED(커밋된 읽기)
REPEATABLE READ(반복 가능한 읽기)
SERIALIZABLE(직렬화 가능)
지속성(Durability)
: 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다.중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.
트랜잭션의 사용 예시
데이터 변경 쿼리를 실행하고 데이터베이스에 결과를 반영하려면 commit 을 호출하고,
결과를 반영하고 싶지 않다면 rollback 을 호출
커밋을 호출하기 전까지는 임시로 데이터를 저장 -> 해당 트랜잭션을 시작
자동커밋과 수동커밋
자동 커밋
각각의 쿼리 실행 직후 자동으로 커밋 호출
커밋이나 롤백을 직접 호출하지 않아도 되는 편리함
하지만, 원하는 트랜잭션 기능을 제대로 사용할 수 없는 단점 존재
수동 커밋
수동 커밋 모드로 설정하는 것이 트랜잭션 시작
이후 commit, rollback 호출 필요
수동/자동 커밋 모드는 한번 설정하면 해당 세션에서 계속 유지 (중간 변경도 가능)
Lock
세션이 트랜잭션을 시작하고 데이터를 수정하는 동안 커밋 or 롤백 전까지 다른 세션에서 해당 데이터를 수정할 수 없도록 락을 제공
다른 세션은 락을 획득할 때까지 대기
락 대기 시간을 넘어가면 락 타임아웃 오류 발생(락 대기 시간을 설정 가능)
Lock Timeout 시간 설정
Lock Timeout Error
조회와 락
일반적인 조회는 락을 사용하지 않지만,
락을 획득해서 변경을 막고 싶다면,
select .. for update
구문을 사용트랜잭션 종료 시점까지 해당 데이터를 다른 곳에서 변경하지 못하도록 강제로 막아야 할 경우 사용
해당 세션이 조회 시점에 락을 가져가버리기 때문에 다른 세션에서 해당 데이터를 변경할 수 없다(트랜잭션 커밋 시 락 반납)
과거 트랜잭션 적용
트랜잭션은 서비스 계층에서부터 시작
비즈니스 로직이 잘못되면 문제가 되는 부분을 함께 롤백해주어야 한다.
트랜잭션을 시작하려면 커넥션이 필요.
set autocommit false;
같은 세션을 사용하기 위해 트랜잭션을 사용하는 동안 같은 커넥션을 유지해야 한다.
가장 단순한 방법은 커넥션을 파라미터로 전달하는 방법
과거 서버에서의 트랜젝션 적용은 서비스 계층이 매우 지저분해지고 생각보다 매우 복잡한 코드를 요구..
기존 트랜잭션의 문제점
JDBC 구현 기술이 서비스 계층에 누수되는 문제
데이터 접근 계층의 JDBC 구현 기술 예외가 서비스 계층으로 전파
try, catch, finally .. 유사한 코드의 반복
Transaction Problem
Spring Transaction Manager
트랜잭션 추상화
PlatformTransactionManager
interfaceJdbcTransactionManager
JpaTransactionManager
HibernateTransactionManager
EtcTransactionManager
리소스 동기화
트랜잭션을 유지하기 위해 트랜잭션의 시작부터 끝까지 같은 데이터베이스 커넥션을 유지해야 한다.
과거에는 파라미터로 커넥션을 전달했지만
스프링은
org.springframework.transaction.support.TransactionSynchronizationManager
를 통해ThreadLocal
로 커넥션을 동기화TransactionManager
는 내부에서TransactionSynchronizationManager
를 사용하고,TransactionManager
를 통해 커넥션을 획득ThreadLocal
을 사용해서 멀티쓰레드 상황에 안전하게 커넥션을 동기화가 가능
.
동작 방식
1.TransactionManager는 dataSource를 통해 커넥션을 만들고 트랜잭션 시작
2.TransactionManager는 트랜잭션이 시작된 커넥션을 TransactionSynchronizationManager에 보관
3.Repository는 TransactionSynchronizationManager에 보관된 커넥션을 꺼내서 사용
4.트랜잭션이 종료되면 TransactionManager는 TransactionSynchronizationManager에 보관된 커넥션을 통해 트랜잭션을 종료하고, 커넥션도 닫음
TransactionManager
트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용
DataSourceUtils.getConnection()
TransactionSynchronizationManager가 관리하는 커넥션이 있으면 해당 커넥션을 반환
커넥션이 없는 경우 새로운 커넥션을 생성해서 반환
DataSourceUtils.releaseConnection()
트랜잭션을 사용하기 위해 동기화된 커넥션은 커넥션을 닫지 않고 그대로 유지
TransactionSynchronizationManager가 관리하는 커넥션이 없는 경우 해당 커넥션을 닫음
commit(status), rollback(status) 호출 시 알아서 release 수행
Transaction Template
템플릿 콜백 패턴 적용을 위해 TransactionTemplate
템플릿 클래스 작성
Transaction의 반복되는 try, catch, finally 코드 제거
단, 서비스 로직에 트랜잭션 처리 코드가 포함되어 있는 단점이 존재
Transaction AOP
TransactionalProxy 도입을 통해 트랜잭션 처리 객체와 비즈니스 로직 처리 서비스 객체를 명확하게 분리
@Transactional
을 트랜잭션 처리가 필요한 곳에 추가해주면, 스프링의 트랜잭션 AOP가 트랜잭션이 적용된 프록시를 생성하고 자동으로 트랜잭션 처리TransactionalProxy
를 도입하면@Transactional
이 붙어 있는 메서드나 클래스에 Spring이 해당 서비스 로직을 상속받아서 자동으로 트랜잭션 코드를 생성xxxService$$EnhancerBySpringCGLIB$$..
트랜잭션 AOP 동작 흐름
Transaction이 적용된 클래스/메서드 호출
Transaction이 적용된
Spring AOP Proxy 호출
Spring Container에 등록된
Transaction Manager
획득트랜잭션 시작
. transactionManager.getTransaction()transactionManager는 내부에서 DataSource를 사용해
커넥션 생성
커넥션을
수동 커밋 모드로 변경
해서 실제 데이터베이스 트랜잭션 시작. setAutoCommit(false)커넥션을
TransactionSynchronizationManager
에 보관TransactionSynchronizationManager는
ThreadLocal
에 커넥션을 보관ThreadLocal: 멀티 쓰레드 환경에서도 안전하게 커넥션 보관
Spring AOP Proxy에서 실제 비즈니스 로직을 실행하면서 리포지토리의
메서드들을 호출
(커넥션을 파라미터로 전달할 필요가 없어짐)리포지토리는 DataSourceUtils.getConnection()을 통해
TransactionSynchronizationManager
에 보관된커넥션을 꺼내서 사용
같은 커넥션을 사용하고, 트랜잭션도 유지
획득한 커넥션을 사용해서
SQL
을 데이터베이스에전달 및 실행
비즈니스 로직이 끝나고
트랜잭션을 종료
를 위해TransactionSynchronizationManager
를 통한동기화된 커넥션을 획득
획득한 커넥션을 통해 커밋/롤백 후 트랜잭션 종료
전체
리소스
(TransactionSynchronizationManager, ThreadLocal, setAutoCommit(true), con.close()..)정리
SpringBoot 자동 리소스 등록
기존에는 데이터소스와 트랜잭션 매니저를 XML로 등록하거나 직접 스프링 빈으로 등록해야 했지만, SpringBoot를 통해 많은 부분이 자동화
자동 등록
DataSource
application.properties
에 있는 속성을 사용해서 DataSource를 생성하고 스프링 빈에 자동으로 등록직접 DataSource를 빈으로 등록하면 스프링 부트는 자동으로 등록하지 않음
TransactionManager
스프링 부트는 적절한 트랜잭션 매니저(PlatformTransactionManager)를 자동으로 스프링 빈에 등록
자동 등록 스프링 빈 이름: transactionManager
DataSource와 마찬가지로 직접 TransactionManager를 빈으로 등록하면 스프링 부트는 자동으로 등록하지 않음
자동으로 등록되는 트랜잭션 매니저는 현재 등록된 라이브러리를 보고 판단
JDBC: DataSourceTransactionManager
JPA: JpaTransactionManager
JDBC + JPA: JpaTransactionManager
SpringBoot가 application.properties에 지정된 속성을 참고해서 데이터소스와 트랜잭션 매니저를 자동으로 생성
생성자를 통해 SpringBoot가 만들어준 데이터소스 빈을 주입 가능
직접 등록
Java Excaption
Object
: 모든 객체의 최상위 부모Throwable
: 최상위 예외상위 예외를 잡으면 그 하위 예외(Error..)까지 함께 잡으므로, Throwable 예외는 잡지 말고, Exception부터 잡자.
Error
: 애플리케이션에서 복구 불가능한 시스템 예외 (메모리 부족이나 심각한 시스템 오류)unchecked exception
Exception
: 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외Exception과 그 하위 예외는 모두 컴파일러가 체크하는 checked exception
컴파일러가 체크해 주기 때문에 잡거나 던지거나 하나를 필수로 선택
단, RuntimeException은 예외
RuntimeException
: 컴파일러가 체크하지 않는 unchecked exceptionRuntimeException과 그 자식 예외는 모두 unchecked exception
예외의 기본 규칙
예외는 잡아서 처리하거나 던져야 함.
예외를 잡거나 던질 때 지정한 예외뿐만 아니라 자식 예외들도 함께 처리
.
예외 잡기
try-catch
Repository 예외 발생 -> Service로 예외 throws -> Service에서 예외 처리 -> 이후 정상 흐름으로 동작
예외 던지기
throws Exception
Repository 예외 발생 -> Service로 예외 throws -> Controller로 예외 throws
예외를 처리하지 못하고 계속 던지면 main() 쓰레드의 경우 예외 로그를 출력하면서 시스템이 종료되고, 웹 애플리케이션의 경우 WAS가 해당 예외를 받아서 처리하는데, 주로 사용자에게 지정한 오류 페이지를 전달
Checked Exception
컴파일러가 예외를 체크해주면, 잡아서 처리하거나, 밖으로 던지도록 선언
예외를 잡아서 처리할 수 없을 경우에는 예외를 throws로 던져줘야 함.
장점: 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아주는 안전 장치
단점: 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하는 번거로움
크게 신경쓰고 싶지 않은 예외까지 모두 챙겨야 하고, 의존관계에 따른 단점도 존재
활용
\1. 기본적으로 Unchecked(Runtime) Exception를 사용하자.
Checked Exception은 Service, Controller에서 처리할 수 없는 예외를 throws 선언으로 계속 던지다보면,
복구 불가능한 예외
,의존 관계 문제
발생복구 불가능한 예외
: 로그를 남기고 ServletFilter, SpringInterceptor, Spring ControllerAdvice를 통해 일관성있게 공통으로 처리하자. (실무의 대부분의 예외들은 복구 불가능한 시스템 예외)의존 관계 문제
: 처리할 수도 없는 SQLException에 의존하여 기술이 변경되면 의존 코드를 전부 고쳐주어야 하는 문제 발생(OCP, DI 위반). -> Exception을 던져서 문제를 해결할 수 있을 것 같지만, 모든 예외를 다 단지기 떄문에 체크 예외를 체크할 수 있는 기능이 무효화
\2. 체크 예외는 비즈니스 로직상 의도적으로 던지는 예외를 잡아서 반드시 처리해야 하는 경우에만 사용하자.
계좌 이체 실패 예외
결제시 포인트 부족 예외
로그인 ID, PW 불일치 예외
Unchecked Exception
컴파일러가 체크하지 않는 예외
체크 예외와 언체크 예외는 기본적으로 동일하지만,
Checked Exception: 예외를 잡아서 처리하지 않으면 항상 throws 선언 필요
Unchecked Exception: 예외를 잡아서 처리하지 않아도 throws 생략 가능
예외를 처리할 수 없을 때 예외를 밖으로 던지는데, throws를 필수로 선언해야 하는가 생략할 수 있는가의 차이가 큼
장점: 신경쓰고 싶지 않은 언체크 예외는 무시하고 throws 선언 생략 가능
단점: 컴파일러가 예외 누락을 잡아주지 않으므로, 실수로 예외를 누락할 수 있음
활용
CheckedException이 발생하면 RuntimeException으로 전환해서 예외를 던지자.
시스템에서 발생한 예외는 대부분 복구 불가능 예외이므로, Runtime Exception을 사용하면 서비스나 컨트롤러가 복구 불가능한 예외를 신경쓰지 않아도 되고 공통으로 처리할 수 있다.
해당 객체가 처리할 수 없는 예외는 무시하면 되므로, 예외를 강제로 의존하지 않아도 된다.
RuntimeException은 놓칠 수 있기 때문에 문서화가 중요
JPA EntityManager
JdbcTemplate
Stack Trace
예외를 전환할 때는 반드시 기존 예외를 포함해야 하자
그렇지 않으면.. 스택 트레이스를 확인할 때 상단에서 발생한 예외를 확인할 수 없는 심각한 문제 발셍
로그를 출력할 때 마지막 파라미터에 예외를 넣어주면 로그에 스택 트레이스 출력 가능
Spring Exception
서비스 계층은 가급적 특정 구현 기술에 의존하지 않고, 순수하게 유지하는 것이 좋다.
예외에 대한 의존(예외 누수)을 해결하기 위해 런타임 예외와 인터페이스를 적용해 보자.
접근 예외 생성
Service Layer에서 특정 기술에 의존적인 예외(ex. SQLException)를 잡아서 처리하고 싶을 경우, RuntimeException 예외를 속상받은 커스텀 예외를 Repository Layer에서 변환해서 처리할 수 있음
단, SQL ErrorCode는 데이터베이스 마다 다르므로 데이터베이스에 종속적
스프링의 예외 추상화
스프링은 데이터 접근 계층에 대한 일관된 예외 추상화를 제공
스프링이 제공하는 데이터 접근 계층의 모든 예외는 런타임 예외
DataAccessException
NonTransient
Exception일시적이지 않은 예외, 같은 SQL을 그대로 반복 실행하면 실패
ex. SQL 문법 오류, 데이터베이스 제약조건 위배 등
Transient
Exception일시적인 예외, 하위 예외는 동일한 SQL을 다시 시도했을 때 성공할 가능성 존재
ex. 쿼리 타임아웃, 락 관련 오류 등
각 예외는 특정 기술에 종속되지 않게 설계
특정 기술을 사용하면서 발생하는 예외를 스프링이 제공하는 예외로 변환하는 역할 수행
예외 변환기를 통해서 SQLException의 ErrorCode에 맞는 적절한 스프링 데이터 접근 예외로 변환
Service/Controller Layer에서 예외 처리가 필요하면 특정 기술에 종속적인 SQLException 대신 스프링이 제공하는 데이터 접근 예외를 사용
Repository
Service
스프링이 제공하는 SQL 예외 변환기
SQL ErrorCode
SQL ErrorCode를 sql-error-codes.xml 파일에 대입해서 어떤 데이터 접근 예외로 전환해야 할지 탐색
템플릿 콜백 패턴
JDBC의 반복 문제를 해결
JdbcTemplate
커넥션 조회, 커넥션 동기화
PeparedStatement 생성 및 파라미터 바인딩
쿼리 실행
결과 바인딩
예외 발생시 스프링 예외 변환기 실행
리소스 종료
트랜잭션을 위한 커넥션 동기화, 스프링 예외 변환기도 자동 실행
Last updated