본문 바로가기

웹 프로그래밍/스프링

스프링 데이터 접근 핵심 원리

애플리케이션 서버와 DB 일반적인 사용법

1. 커넥션 연결: 주로 TCP/IP를 사용해서 커넥션을 연결

2. SQL 전달: 커넥션을 통해 DB에 SQL 전달

3. 결과 응답: DB는 SQL을 수행하고 결과를 응답, 애플리케이션 서버는 응답결과를 활용

 

과거에는 DB를 교체하면 애플리케이션 로직을 다 바꿔야했다(개발자는 모든 DB에 대한 로직을 알아야했음)

-> JDBC(Java Database Connectivity) 표준 인터페이스(자바에서 데이터 베이스에 접솔할수 있도록 하는 자바 API) 등장

-> JDBC 드라이버는 Connection(연결), Statement(SQL 전달), ResultSet(결과 응답) 을 추상화하여 DB에 맞도록 구현해서 라이브러리를 제공함 -> 즉 DB에 맞는 드라이버만 갈아끼면 된다.

 

-> 이제 애플리케이션은 JDBC 표준 인터페이스에만 의존한다. 하지만 DB마다 SQL은 다르기 때문에 변경을 해줘야함

-> JPA를 사용하여 해결 

 

커넥션 풀 이해

데이터베이스 커넥션을 획들할때

1. 애플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회

2. DB 드라이버는 DB와 TCP/IP 커넥션을 연결

3. DB 드라이버는 ID,PW와 기타 부가정보를 DB에 전달

4. DB는 ID,PW를 통해 내부 인증을 완료하고, 내부에 DB 세션을 생성

5. DB는 커넥션 생성이 완료되었다는 응답을 보냄

6. DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환

 

위 과정은 너무 시간이 많이 드는 과정임

SQL 실행 시간 + 커넥션 새로 만드는 시간 -> 응답속도 느려짐

 

커넥션을 미리 생성해두고 재사용하자(커넥션 풀)

 

애플리 케이션을 시작하는 시점에 커넥션 풀은 필요한 칸큼 커넥션을 미리 확보해서 풀에 보관(기본값 10개정도)

 

커넥션 풀에 들어있는 커넥션은 TCP/IP로 DB에 커넥션이 연결되어 있는 상태이기 때문에

언제든지 즉시 SQL을 DB에 전달할수 있음

 

애플리케이션 로직은 이제 커넥션 풀을 통해 이미 생성되어 있는 커넥션을 객체 참조로 그냥 가져다 쓰기만 하면됨

커넥션 풀에 커넥션을 요청하면 커넥션 풀은 자신이 가지고있는 커넥션 중에 하나를 반환

 

커넥션은 다 사용하고 나면 커넥션풀에 다시 반환(종료 X, 살아있는 상태로)

 

DataSource 이해

커넥션을 획득하는 방법을 추상화

DataSource는 커넥션을 획득하는 방법을 추상화하는 인터페이스임

구현체(HikariCP, DBCP2 커넥션 풀 등)

이 인터페이스의 핵심기능은 커넥션 조회 getConnection() 하나임

 

설정과 사용의 분리

설정: DataSource를 만들고 필요한 속성들을 사용해서 URL, USERNAME, PASSWORD 같은 부분을 입력하는 것을 말함

이렇게 설정과 관련된 속성들은 한곳에 모아 있는 것이 향후 변경에 더 유연하게 대처가능

사용: 설정은 신경쓰지 않고, DataSource의 getConnection()만 호출해서 사용하면 됨

 

트랜잭션의 개념 이해

굳이 데이터베이스에 저장?

트랜잭션은 하나의 거래를 안전하게 처리하도록 보장

과정중 어느하나라도 실패가 있을시 롤백(Rolback)을 해줌

모든 작업이 성해서 데이터베이스에 정상반영하는것을 커밋(Commit)이라고 함

 

트랜잭션 ACID

원자성(Atomicity): 트랜잭션 내에서 실핸한 작업들은 마치 하나의 작업인 것처럼 모두 성공 하거나 실패해야함

일관성(Consistency): 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야함

격리성(Isolation): 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리해야함(동시성 이슈)

지속성(Durability): 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 함(데이터베이스 로그)

 

격리성문제의 성능문제로 인해 트랜잭션의 격리 수준을 4단계로 나누어 정의

 

트랜잭션 격리 수준(Isolation level)

READ UNCOMMITED(커밋되지 않은 읽기)

READ COMMITED(커밋된 읽기)

REPEATABLE READ(반복 가능한 읽기)

SERIALIZABLE(직렬화 가능)

 

데이터베이스 연결구조와 DB 세션

사용자 -> 웹 애플리케이션 서버(WAS): 클라이언트  -> 데이터베이스 서버에 연결요청하고 커넥션 맺음

-> 데이터베이스 서버 내부에 세션 -> 해당 커넥션을 통한 모든 요청은 세션을 통해서 실행

세션으로 트랜잭션(커밋, 롤백) 함

사용자가 커넥션을 닫거나 DBA(DB 관리자)가 세션을 강제로 종료하면 세션은 종료

 

커넥션풀이 10개의 커넥션을 생성하면 세션도 10개 만들어짐

 

트랜잭션 개념이해

데이터 변경퀴리를 실행하고 데이터 베이스에 결과반영 커밋(Commit)을 호출

결과를 반영하고 싶지 않으면 롤백(Rollback)을 호출

 

커밋을 호출하기 전까지는 임시로 데이터를 저장함. 따라서 해당 트랜잭션을 시작한 나(세션)한테만 변경 데이터가

보이고 다른 사람(세션)에게는 변경 데이터가 보이지 않음( 이렇게 하지 않으면 데이터 정합성에 큰 오류 발생)

등록, 수정, 삭제 모두 같은 원리임 따라서 변경으로 통칭

 

자동 커밋: 각각의 쿼리 실행 직후에 자동으로 커밋을 호출(커밋이나 롤백을 호출하지 않아도 되는 편리함)

하지만 쿼리를 실행할때마다 자동으로 커밋이 되어버리기 때문에 원하는 트랙잭션 기능을 제대로 사용할수 없음

 

트랜잭션 기능을 제대로 사용하려면 수동커밋으로 사용해야함

디폴트가 자동커밋모드임 -> 트랜잭션의 시작은 수동 커밋 모드 설정(commit, rollback을 반드시 호출해줘야함)

 

DB 락 - 개념 이해

세션1이 데이터를 바꾸고 있는 와중에 세션2가 그 데이터를 변경한다면? -> 문제 발생

-> 다른 세션이 그 데이터에 접근할수 없게 락(LOCK)을 걸어 해결

 

과정

1. 세션 1은 트랜잭션 시작

2. 세션1은 해당 락이 남아있으므로 로우의 락을 먼저 획득하고 변경을 시도

3. 세션1은 락을 획득했으므로 해당 로우에 update sql을 수행

4. 세션2가 트랜잭션을 시작

5. 세션2기 해당 로우의 락이 없으므로 락이 돌아올때까지 대기함

6. 무한정 대기는 아니고 락 대기시간 만큼만 대기

7. 세션1이 커밋을 수행. 트랜잭션이 종료되었으므로 락도 반납

8. 락을 획득하기 위해 대기하던 세션2가 락을 획득

9. 세션2는 update sql을 수행

10. 세션2는 커밋을 수행하고 트랜잭션 종료하고 락도 반납

 

 DB 락 - 조회

일반적인 조회는 락을 사용하지 않음

세션1이 락을 획득하고 데이터를 변경하고 있어도 세션2는 그 데이터를 조회는 할수 있음(임시 말고 원본)

 

조회와 락

-데이터를 조회할때도 락을 획득하고 싶을때가 있음(select for update)

-이렇게 하면 세션1이 조회 시점에 락을 가져가버리기 때문에 다른 세션에서 해당 데이터를 변경할수 없음

(내가 다 볼때까지 건들지마 eg) 은행 마감처리 시간)

-트랜잭션 커밋하면 락을 반납

 

트랜잭션 - 적용

트랜잭션은 비지니스 로직이 있는 서비스 게층에서 시작해야함(비지니스 로직이 잘못되면 롤백을 해야하기 때문)

애플리케이션에서 DB 트랜잭션을 사용하렴ㄴ 트랜잭션을 사용하는 동안 같은 커넥션을 유지 해야함

 

기존 트랜잭션의 문제점

서비스 계층은 가급적 비지니스 로직만 구현하고 특정 구현 기술에 직접 의존해서는 안됨(순수 자바코드)

-> 구현 기술이 변결될때 변경의 영향 범위를 최소화할수 있음(단일책임원칙)

 

기존 트랜잭션 코드는 커넥션을 가져오고 사용하기위해 JDBC 기술에 의존했음

-> JPA로 바꾸면 다 뜯어 고쳐야함

 

비지니스 로직과 JDBC 기술이 섞여있으면 유지보수가 어려움

 

정리

1. 트랜잭션 문제

    -트랜잭션을 적용하기 위해 JDBC 구현 기술이 서비스 계층에 누수됨

    -같은 트랜잭션을 유지하기위해 커넥션을 파라미터로 넘겨야 됨(중복되는 메서드가 많아짐)

    

2. 예외 누수 문제

    -데이터 접근 계층(Repository)의 JDBC 구현 기술 예외가 서비스 계층으로 전파됨

    -예외를 잡거나 Throws해야하는데 이때 또 JDBC 기술에 의존하게됨

3. JDBC 반복 문제

    -적용 코드 반복문제(try, catch, finally)

 

 

-> 스프링은 서브스 계층을 순수하게 유지하면서 문제들에 대한 다양한 해결방법과 기술을 제공

 

트랜잭션 추상화

트랜잭션 추상화 인터페이스를 만들어서 사용함

인터페이스를 기반으로 각각의 기술에 맞는 구현체를 만들면됨 

서비스계층은 추상화된 트랜잭션 인터페이스에만 의존하면 된다. 이후에 원하는 기술에 맞게 DI로 구현체를 주입받아서 쓰면 됨

-> DI를 사용한 덕분에 OCP 원칙을 지키게 됨

 

스프링의 트랜잭션 추상화

스프링이 제공하는 트랜잭션 추상화 기술(PlatformTransactionManager)을 사용하기만 하면됨

 

트랜잭션 동기화

스프링이 제공하는 트랜잭션 매니저는 크게 2가지 역할을 함

-트랜잭션 추상화

-리소스 동기화

 

리소스 동기화

트랜잭션을 유지하려면 트랜잭션의 시작부터 끝까지 같은 데이터베이스 커넥션을 유지해야함

파라미터로 커넥션 전달시 여러문제 발생(중복 메서드, 코드 복잡성 증가)

 

스프링은 동기화를 위해 트랜잭션 동기화 매니저를 제공, 즉 트랜잭션 매니저 내부에서 트랜잭션 동기화 매니저를 사용

-> 이것은 쓰레드 로컬(ThreadLocal)을 사용해서 커넥션을 동기화 해줌

-> 멀티쓰레드 상황에 안전하게 커넥션을 동기화해서 사용 가능

 

동작 방식

1. 트랜잭션을 시작하려면 커넥션이 필요

-> 트랜잭션 매니저는 데이터소스를 통해 커넥션을 만들고(autocommit false등) 트랜잭션 시작

2. 트랜잭션 매니저는 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 보관

3. 리포지토리는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용(파라미터 필요없음)

4. 트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료, 커넥션도 닫음

 

트랜잭션 문제 해결 - 트랜잭션 매니저

전체 동착 흐름

클라이언트의 요청으로 서비스 로직 실행

1. 서비스 계층에서 transactionmanager.getTransaction()을 호출해서 트랜잭션 시작

2. 트랜잭션을 시작하려면 데이터베이스 커넥션이 필요하므로 트랜잭션 매니저는 내부에서 데이터소스를 사용해서 커넥션 생성

3. 커넥션을 수동 커밋모드로 변경해서 데이터베이스 트랙잭션을 시작

4. 커넥션을 트랜잭션 동기화 매니저에 보관

5. 트랜잭션 동기화 매니저는 쓰레드 로컬에 커넥션을 보관(안전하게 커넥션 보관가능)

6. 서비스는 비니지스로직을 실행하면서 리포지토리의 메서드들을 호출(파라미터로 커넥션 전달 X)

7. 리포지토리 메서드들은 트랜잭션이 시작된 커넥션이 필요하기때문에 DataSourceUtils.getConnection()을 사용해서

트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내 사용. 이과정을 통해 같은 커넥션을 사용하게되고 트랜잭션도 유지됨

8. 획득한 커넥션을 사용해서 SQL을 데이터베이스에 전달해서 실행

9. 비지니스 로직이 끝나고 트랜잭션을 종료(커밋 or 롤백)

10. 트랜잭션을 종료하려면 동기화된 커넥션이 필요한데 트랜잭션 동기화 매니저를 통해 획득

11. 획득한 커넥션을 통해 데이터베이스에 트랜잭션을 커밋하거나 롤백함

12 전체 리소스를 정리함

    - 트랜잭션 동기화 매니저를 정리(쓰레드 로컬 정리)

    - con.setAutoCommit(true)로 되돌림

    - con.close() 호출해서 커넥션을 종료. 커넥션 풀을 사용하는경우 con.close() 호출하면 커넥션 풀에 반환됨

 

트랜잭션 문제 해결 - 트랜잭션 템플릿

트랜잭션 로직은 같은 패턴이 반복됨(try, catch, finally)

-> 탬플릿 콜백 패턴을 활용해서 해결

 

트랜잭션 템플릿

스프링의 TransactionTemplate 템플릿 클래스로 템플릿 콜백 패턴 적용

but 아직까지는 비지니스 로직과 트랜잭션 처리 기술로직이 한 클래스에 존재함 -> 스프링 AOP를 통해 해결

 

트랜잭션 문제 해결 - 트랜잭션 AOP 이해

@Transactional을 사용하면 스피링이 AOP와 프록시를 사용해서 트랜잭션을 편리하게 처리해줌

프록시 도입후 : 원래 상태에서 프록시를 한번 거쳐서 가는 패턴 도입, 즉 프록시가 트랜잭션 처리 로직을 모두 가져감. 그리고 트랜잭션을 시작한 후에 실제 서비스를 대신 호출 -> 서비스 계층은 순수한 비지니스 로직만 남길수 있음

 

 

프록시를 사용하면 트랜잭션을 처리하는 객체와 비지니스 로직을 처리하는 서비스 객체를 명확하게 분리할수 있음

스프링이 제공하는 AOP 기능을 사용하면 프록시를 매우 편리하게 적용할수 있음

-> 개발자는 트랜잭션 처리가 필요한곳에 @Transactional 만 붙여주면됨 -> 스프링의 트랜잭션 AOP는 이 애노테이션을 인식해서 트랜잭션 프록시를 적용해줌

 

 

'웹 프로그래밍 > 스프링' 카테고리의 다른 글

스프링 예외 처리  (0) 2023.06.06
자바 예외 이해  (0) 2023.06.03
스프링 MVC(2) 검증 정리  (0) 2023.04.14
스프링 MVC(1) 원리 정리  (0) 2023.04.08
웹 애플리케이션 이해  (0) 2023.04.05