JDBC 의 기본 구성
Jdbc 표준 인터페이스
`java.sql.Connection` - 연결
`java.sql.Statement` - SQL 쿼리를 담는 객체
`java.sql.ResultSet` - SQL 실행 결과를 담는 객체
데이터베이스와 자바 애플리케이션을 연결하기 위한 표준으로, 위의 3가지를 정의하여 제공한다.
JDBC 드라이버
JDBC 표준 인터페이스를 각자의 DB 에 맞도록 구현하여 제공하는 라이브러리를 JDBC 드라이버라고 한다.
(예를 들어, MySQL DB 라면 MySQL JDBC 드라이버 / Oracle DB 라면 Oracle JDBC 드라이버)
DriverManager
라이브러리에 등록된 DB 드라이버들의 목록을 관리하고, 커넥션을 획득하는 기능을 제공한다.
하지만 매번 새로운 커넥션을 생성하기 때문에 성능에 비효율적이다.
이러한 성능 문제를 해결하기 위해, 커넥션 풀을 사용하는 방식이 도입되었다.
커넥션 풀
데이터베이스 커넥션 획득 순서
- 애플리케이션 로직이 DB 드라이버를 통해 커넥션을 조회
1. DB 드라이버가 DB 와 TCP/IP 커넥션 연결
2. ID, PW 등 부가정보를 DB 에 전달
3. DB 는 내부 인증을 완료하고 내부에 DB 세션을 생성 후 응답 보냄
4. DB 드라이버는 커넥션 객체를 생성하여 클라이언트에 반환
이처럼 커넥션을 획득하는 데 리소스를 매번 사용하기 때문에 응답 속도에 영향을 준다.
따라서 커넥션을 미리 생성해두고, 사용하는 방식이 커넥션 풀이다.
커넥션 풀
- 커넥션 풀에 있는 커넥션은 TCP/IP 로 DB 와 커넥션이 연결되어 있는 상태로, 즉시 SQL 을 DB 에 전달할 수 있다.
- 매번 DB 드라이버를 통해 새로운 커넥션을 획득하는 것이 아닌, 생성된 커넥션을 객체 참조로 바로 사용한다.
- 커넥션을 모두 사용하면, 해당 커넥션을 종료하지 않고 그대로 커넥션 풀에 반환한다.
- `hikariCP` 를 대부분 사용한다.
DataSource
커넥션을 획득하는 방법을 추상화 하는 인터페이스이다.
- DBCP2 커넥션 풀 / HIKARICP 커넥션 풀 : 커넥션 풀 O
- DriverManagerDataSource : 커넥션 풀 x (매번 새로운 커넥션을 생성하여 사용)
(DriverManager 는 DataSource 를 구현하지 않기 때문에, DataSource 를 구현한 DriverManagerDataSource 클래스이다. )
어느 방법을 사용해도 애플리케이션 로직이 DataSource 인터페이스에만 의존하여 사용할 수 있다.
DriverManeger / DriverManagerDataSource / HikariDataSource 차이
// DriverManager - DataSource 구현체 X, 커넥션 풀 X
DriverManager.getConnection(URL, USERNAME, PASSWORD)
// DriverManagerDataSource - DataSource 구현체 O, 커넥션 풀 X
DataSource ds = new DriverManagerDataSource(URL, USER, PASSWORD);
Connection conn1 = ds.getConnection(); // 새로운 커넥션
Connection conn2 = ds.getConnection(); // 또 새로운 커넥션
// HikariDataSource - DataSource 구현체 O, 커넥션 풀 O
HikariDataSource hikari = new HikariDataSource();
hikari.setJdbcUrl(URL);
hikari.setUsername(USER);
hikari.setPassword(PASSWORD);
Connection conn1 = hikari.getConnection(); // 풀에서 하나 꺼냄
Connection conn2 = hikari.getConnection(); // 같은 풀에서 또 하나 꺼냄 (재사용 가능)
`DriverManager`
: 항상 URL, ID, PW 같은 부가정보로 커넥션을 생성
: DataSource 구현체 X, 커넥션 풀 X
`DriverManagerDataSource`
: 처음 객체 생성시에만 URL, ID, PW 같은 부가정보를 넘겨주고, 획득할 때는 새로운 커넥션을 호출
: DataSource 구현체 O, 커넥션 풀 X
(이때, getConnection( ) 으로 새로운 커넥션을 항상 사용하기 때문에 커넥션 풀을 사용하는 것은 아니다.)
`HikariDataSource`
: 커넥션을 생성 항상 생성하지 않고, 같은 커넥션 풀에서 재사용
: DataSource 구현체 O, 커넥션 풀 O
JDBC를 편리하게 사용하는 기술들
데이터베이스마다 SQL, 데이터타입 등 일부 사용법이 다르므로 해당 기술과 함께 편리하게 사용한다.
- SQL Mapper
- ORM 기술
SQL Mapper
- SQL 응답 결과를 객체로 편리하게 반환해 준다.
- JDBC 반복 코드를 제거해 준다.
- SQL 문을 직접 작성해야 하지만, 객체 매핑을 도와주는 유틸리티가 있다.
- 스프링 Jdbc Template, MyBatis
ORM 기술
- 객체를 관계형 데이터베이스 테이블과 매핑해주는 기술
- SQL 문을 직접 작성하지 않고, 동적으로 만들어 실행해 준다.
- 자바 컬렉션에 저장하고 조회하듯이 사용하면 ORM 기술이 데이터베이스에 객체를 저장하고 조회해 준다.
- JPA, 하이버네이트, 이클립스 링크, Querydsl, 스프링 데이터 JPA
( JPA 는 자바의 ORM 표준 인터페이스, 이를 구현한 것이 하이버네이트 / 이클립스 링크 등)
( 스프링 데이터 JPA, Querydsl 은 JPA 를 편리하게 사용할 수 있도록 도와주는 프로젝트)
JDBC 트랜잭션
여러 개의 작업을 하나의 작업처럼 동작해야 할 때 트랜잭션을 사용한다.
트랜잭션은 데이터베이스에 정상 반영되는 `Commit` 과 이전으로 되돌리는 `Rollback` 이 있다.
ACID
`원자성` : 마치 하나의 작업처럼 모두 성공 or 모두 실패
`일관성` : 일관성 있는 데이터베이스 상태 유지 (무결성 제약 조건 항상 만족)
`격리성` : 트랜잭션들이 서로 영향을 미치지 않도록 격리
`지속성` : 트랜잭션이 성공하면 결과가 항상 기록
트랜잭션 격리 수준
`READ UNCOMMITED` : 커밋되지 않은 읽기
`READ COMMITTED` : 커밋된 읽기 (일반적으로 사용)
`REPEATABLE READ` : 반복 가능한 읽기
`SERIALIZABLE` : 직렬화 가능
세션
- 각각의 커넥션마다 데이터베이스 서버 내부에 세션이 생기고, 해당 커넥션을 통한 요청은 세션을 통해 실행한다.
- 세션은 트랜잭션을 시작하고, 커밋 or 롤백을 통해 트랜잭션을 종료한다.
- 커넥션을 닫거나, 세션을 강제로 종료하면 세션은 종료된다.
기본 원리
1. 커밋을 호출하기 전까지는 임시로 데이터를 저장하며, 해당 트랜잭션을 시작한 세션에게만 임시 데이터가 보인다.
2. 커밋을 호출하면, 임시 데이터가 실제 데이터베이스에 반영된다.
3. 롤백을 호출하면, 임시 데이터가 적용되지 않고 트랜잭션을 시작하기 직전의 상태로 복구된다.
자동 커밋, 수동 커밋
자동 커밋은 기본으로 설정되어 있고, 각각의 쿼리 실행 직후 자동으로 커밋을 호출한다.
따라서 트랜잭션을 사용하려면 `수동 커밋` 으로 설정하고, commit, rollback 을 꼭 호출해야 한다.
여기서 수동 커밋으로 설정하는 것을 `트랜잭션의 시작` 이라고 한다.
락
데이터의 정합성을 보장하기 위한 동시성 제어 수단이다.
1. 락 획득 세션
: 트랜잭션이 특정 로우를 변경하려고 하면, 해당 로우에 락을 걸어야 한다.
: 다른 세션이 동시에 해당 로우에 접근하지 못하도록 막는 역할이다.
2. 다른 세션
: 락이 걸린 상태에서 다른 세션이 같은 데이터에 접근하면
: 락이 해제될 때까지 대기 or 락 대기 시간이 넘어가면 락 타임아웃 오류 발생
3. 락 반납
: 트랜잭션이 종료되면 해당 락도 함께 자동으로 해제
일반적인 조회는 락이 필요하지 않고, 변경 시에 락이 필요하다.
하지만 조회 시에도 락을 획득해야 할 때, `select for update` 구문을 사용하면 된다.
예시 코드
public Member findById(Connection con, String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
rs = pstmt.executeQuery();
if (rs.next()) {
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
} else {
throw new NoSuchElementException("member not found memberId=" + memberId);
}
} catch (SQLException e) {
throw e;
} finally {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(pstmt);
}
}
public void update(Connection con, String memberId, int money) throws SQLException {
String sql = "update member set money=? where member_id=?";
PreparedStatement pstmt = null;
try {
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
pstmt.executeUpdate();
} catch (SQLException e) {
throw e;
} finally {
JdbcUtils.closeStatement(pstmt);
}
}
public class MemberService {
private final DataSource dataSource;
private final MemberRepository memberRepository;
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
Connection con = dataSource.getConnection();
try {
con.setAutoCommit(false);
bizLogic(con, fromId, toId, money);
con.commit();
} catch (Exception e) {
con.rollback();
throw new IllegalStateException(e);
} finally {
release(con);
}
}
private void bizLogic(Connection con, String fromId, String toId, int money) throws SQLException {
Member fromMember = memberRepository.findById(con, fromId);
Member toMember = memberRepository.findById(con, toId);
memberRepository.update(con, fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepository.update(con, toId, toMember.getMoney() + money);
}
private void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
private void release(Connection con) {
if (con != null) {
try {
con.setAutoCommit(true); //커넥션 풀 고려
con.close();
} catch (Exception e) {
}
}
}
}
트랜잭션을 시작하려면 커넥션이 필요하다.
그리고 그 커넥션을 유지해야 하기 때문에, 파라미터로 넘어온 커넥션을 사용해야 하고, 리포지토리에서 커넥션을 닫으면 안 된다.
문제점
- 트랜잭션을 적용하기 위해, 서비스 계층에 특정 기술이 의존되어 있다.
- 커넥션을 유지하기 위해 파라미터로 넘기는 방법은, 여러 문제를 야기한다.
(ex. 같은 기능이라도 트랜잭션용 기능, 트랜잭션 유지하지 않는 기능으로 분리 등)
- 트랜잭션 적용하는 코드가 반복된다. (try - catch - finally)
이러한 문제점들을 스프링은 트랜잭션 추상화, AOP 등을 통해 해결한다.
출처 | 스프링 DB 1(김영한) - 인프런
출처 | 스프링 DB 2(김영한) - 인프런
'💠프로그래밍 언어 > Java' 카테고리의 다른 글
[Spring] 에러 코드로 예외 처리하기, 스프링 예외 추상화 사용하기 (0) | 2025.04.23 |
---|---|
[Spring] 스프링 트랜잭션의 추상화, 템플릿, AOP 그리고 자동 등록까지 (0) | 2025.04.23 |
[Spring] 서블릿 파일 업로드와 스프링 파일 업로드 (0) | 2025.04.06 |
[Spring] 컨버터와 포맷터 (컨버전 서비스, 스프링 기본 제공 포맷터) (0) | 2025.04.06 |
[Spring] API 에러 처리하는 방법, ExceptionResolver 와 @ControllerAdvice (0) | 2025.04.06 |