자바에서 데이터베이스에 접속할 수 있게 하는 자바 API
애플리케이션 서버와 db는 다음과 같은 과정을 거친다.
그러나 db마다 커넥션을 연결하는 법, SQL을 전달받는 방법 등이 다르다.
이것을 해결하기위해 등장한 것이 jdbc이다.
jbdc는 자바 개발자라면 필수적으로 알아두어야 하는 기술이다.
다음 3가지 기능을 표준 인터페이스로 정의하여 제공한다.
JDBC로 인해 데이터베이스를 변경하더라도 애플리케이션 서버의 사용 코드를 그대로 사용할수 있으며, db마다 다른 접속 방법을 공부할 필요가 없어졌다.
@Slf4j
public class DBConnectionUtil {
public static Connection getConnection() {
try {
//DriverManager.getConnection() <- JDBC가 제공하는 커넥션 연결
Connection connection = DriverManager.getConnection(ConnectionConst.URL, ConnectionConst.USERNAME, ConnectionConst.PASSWORD);
log.info("get Connection={}, class={}", connection, connection.getClass());
return connection;
} catch (SQLException e) {
throw new IllegalStateException(e);
}
}
}
DriverManager는 라이브러리에 등록된 DB 드라이버들을 관리하고, 커넥션을 획득하는 기능을 제공.
하는 일들
@Slf4j
public class MemberRepositoryVO {
//회원 객체 등록
public Member save(Member member) throws SQLException {
//db에 전달할 sql 정의
String sql = "insert into member(member_id, money) values(?, ?)";
Connection con = null; //db 커넥션
//?를 통한 파라미터 바인딩을 가능하게 해준다.
PreparedStatement pstmt = null;
try {
con = getConnection(); /db커넥션
pstmt = con.prepareStatement(sql); //db에 전달할 sql과 데이터들을 준비
pstmt.setString(1, member.getMemberId()); //sql의 첫번째 파라미터
pstmt.setInt(2, member.getMoney()); //sql의 두번째 파라미터
//준비된 sql을 커넥션을 통해 db에 전달
pstmt.executeUpdate(); //데이터 변경(저장하는 거니까)
return member;
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
//회원 조회
public Member findById(String memberId) throws SQLException {
//데이터 조회를 위한 sql문
String sql = "select * from member where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null; //select 쿼리를 저장하는 데이터 구조
try{
con = getConnection();
pstmt = con.prepareStatement(sql); //db에 전달할 sql과 데이터들을 준비
pstmt.setString(1, memberId); //sql의 첫번째 파라미터
rs = pstmt.executeQuery(); //조회한 결과를 ResultSet에 담아 반환
if(rs.next()){
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
}else {
throw new NoSuchElementException("memebr not found memberId=" + memberId);
}
} catch (SQLException e) {
log.info("db error", e);
throw e;
}finally {
close(con, pstmt, rs);
}
}
//회원 수정
public void update(String memberId, int money) throws SQLException {
String sql = "update member set money=? where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
int resultSize = pstmt.executeUpdate();
log.info("resultSize={}", resultSize);
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
//회원 삭제
public void delete(String memberId) throws SQLException {
String sql = "delete from member where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
pstmt.executeUpdate();
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
private void close(Connection con, Statement stmt, ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
log.info("error", e);
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
log.info("error", e);
}
}
if (con != null) {
try {
con.close();
} catch (SQLException e) {
log.info("error", e);
}
}
}
//DBConnectionUtil을 사용하여 DB 커넥션 획득
private Connection getConnection() {
return DBConnectionUtil.getConnection();
}
}
con.prepareStatement(sql)
pstmt.executeUpdate()
리소스 정리
ResultSet
커넥션을 관리하는 폴
스프링에서는 HikariCP를 기본 커넥션풀로 사용한다.
javax.sql.DataSource
public interface DataSource {
Connection getConnection() throws SQLException;
}
@Slf4j
public class ConnectionTest {
//드라이브 매니저를 통한 커넥션 연결
@Test
void driverManager() throws SQLException {
Connection con1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
Connection con2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("connection={}, class={} ", con1, con1.getClass());
log.info("connection={}, class={} ", con2, con2.getClass());
}
//데이터 소스를 사용한 커넥션 연결
@Test
void dataSourceDriverManager() throws SQLException {
//항상 새로운 커넥션을 획득
//스프링이 제공하는 코드
DataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
useDataSource(dataSource);
}
@Test
void dataSourceConnectionPool() throws SQLException, InterruptedException {
//HikariCp를 사용한 커넥션 풀링
//스프링에서 자동으로 제공하는 커넥션 풀
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
dataSource.setMaximumPoolSize(10); //커넥션 풀 사이즈 크기: 10
dataSource.setPoolName("MyPool"); //커넥션 이름: Mypool
useDataSource(dataSource);
Thread.sleep(1000);
}
private void useDataSource(DataSource dataSource) throws SQLException{
Connection con1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
Connection con2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("connection={}, class={} ", con1, con1.getClass());
log.info("connection={}, class={} ", con2, con2.getClass());
}
}
DriverManager
DataSource
@Slf4j
public class MemberRepositoryV1 {
//dataSource 의존관계 주입
private final DataSource dataSource;
public MemberRepositoryV1(DataSource dataSource) {
this.dataSource = dataSource;
}
public Member save(Member member) throws SQLException {
String sql = "insert into member(member_id, money) values(?, ?)";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, member.getMemberId());
pstmt.setInt(2, member.getMoney());
pstmt.executeUpdate();
return member;
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
...
}
하나의 거래를 안전하게 처리할 수 있도록 보장해주는 것
1)A의 계좌를 5000원 만큼 감소 시키고, 2)B의 계좌를 5000원 만큼 증가시킬때
2번 작업에서 에러가 날경우 A의 계좌만 5000원 만큼 감소하는 일이 일어날 수 있다.
1, 2 작업 모두 성공해야 저장되며 하나라도 실패할 경우 롤백시키는 기능이 바로 트랜젝션이다.
커밋(commit)
다음과 같은 항목을 보장해야 한다
1) 원자성
2) 일관성
3) 격리성
4)지속성
-> 클라이언트를 통해 SQL을 전달하면 현재 커넥션에 연결된 세션이 SQL을 실행된다.
기본 데이터 셋팅
set autocommit true;
delete from member;
insert into member(member_id, money) values ('memberA',10000);
insert into member(member_id, money) values ('memberB',10000);
계좌이체 실행
set autocommit false; //수동 커밋으로 변경
//A의 계좌를 2000만큼 감소
update member set money=10000 - 2000 where member_id = 'memberA';
//B의 계좌를 2000만큼 증가
update member set money=10000 + 2000 where member_id = 'memberB';
결과
//정상 실행시
Commit();
//오류 발생시
RollBack();
세션 1이 트랜젝션을 시작하고 데이터를 수정하는 동안 아직 커밋이 되어있지 않았는데도 세션 2에서 동시에 같은 데이터를 수정하게 되면 트랜잭션의 원자성이 깨지게 된다.
따라서 한 세션에서 데이터를 수정하는 경우 다른 세션에서 수정하지 못하게 막아야한다.
DB락을 획득한 세션만이 데이터를 수정할 수 있다.
락 타임 아웃
조회락
애플리케이션의 구조는 3가지 계층으로 나누어 진다.
프레젠테이션 계층
서비스 계층
데이터 접근 계층
따라서 서비스 계층에 트랜젝션과 비지니스 로직을 함께 작성할 경우 유지보수가 쉽지 않다.
이런 경우 트랜젝션 추상화를 사용한다.
트랜젝션 동기화 매니저 - 커넥션을 안전하게 동기화시키는 방법
탬플릿 콜백 패턴
트랜잭션 템플릿의 기본 동작은 다음과 같다.
트랜젝션 AOP
@Transactional를 사용하면 스프링이 AOP를 사용해서 트랜젝션을 편리하게 처리해준다.
트랜젝션 프록시는 트랜젝션 처리 로직을 모두 가져간 후, 트랜젝션을 시작한 후 실제 서비스를 대신 호출한다.
-> 서비스 계층에 순수한 비지니스 코드만 남길 수 있다.
필요한 클래스/메소드에 @Transactional 어노테이션을 붙여 사용한다.
//MemberServiceV3_3
@Slf4j
public class MemberServiceV3_3 {
private final MemberRepositoryV3 memberRepository;
public MemberServiceV3_3(MemberRepositoryV3 memberRepository) {
this.memberRepository = memberRepository;
}
//트랜잭션 어노테이션
@Transactional
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
//비즈니스 로직
bizLogic(fromId, toId, money);
}
private void bizLogic(String fromId, String toId, int money) throws
SQLException {
Member fromMember = memberRepository.findById(fromId);
Member toMember = memberRepository.findById(toId);
memberRepository.update(fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepository.update(toId, toMember.getMoney() + money);
}
private void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
}
선언적 트랜잭션 관리
테스트 코드
@Slf4j
@SpringBootTest //스프링 AOP를 적용하기 위해 스프링 컨테이너가 필요하다
class MemberServiceV3Test {
public static final String MEMBER_A = "memberA";
public static final String MEMBER_B = "memberB";
public static final String MEMBER_EX = "ex";
@Autowired
private MemberRepositoryV3 memberRepository;
@Autowired
private MemberServiceV3_3 memberService;
//필요한 빈을 등록할 수 있다.
@TestConfiguration
static class TEstConfig {
private final DataSource dataSource;
public TEstConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
MemberRepositoryV3 memberRepositoryV3(){
return new MemberRepositoryV3(dataSource);
}
@Bean
MemberServiceV3_3 memberServiceV3_3(){
return new MemberServiceV3_3(memberRepositoryV3());
}
}
@AfterEach
void after() throws SQLException {
memberRepository.delete(MEMBER_A);
memberRepository.delete(MEMBER_B);
memberRepository.delete(MEMBER_EX);
}
@Test
@DisplayName("정상 이체")
void accountTransfer() throws SQLException {
//given
Member memberA = new Member(MEMBER_A, 10000);
Member memberB = new Member(MEMBER_B, 10000);
memberRepository.save(memberA);
memberRepository.save(memberB);
//when
memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 2000);
//then
Member findMemberA = memberRepository.findById(memberA.getMemberId());
Member findMemberB = memberRepository.findById(memberB.getMemberId());
assertThat(findMemberA.getMoney()).isEqualTo(8000);
assertThat(findMemberB.getMoney()).isEqualTo(12000);
}
@Test
@DisplayName("이체중 예외 발생")
void accountTransferEx() throws SQLException {
//given
Member memberA = new Member(MEMBER_A, 10000);
Member memberEx = new Member(MEMBER_EX, 10000);
memberRepository.save(memberA);
memberRepository.save(memberEx);
//when
assertThatThrownBy(() ->
memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(),
2000))
.isInstanceOf(IllegalStateException.class);
//then
Member findMemberA = memberRepository.findById(memberA.getMemberId());
Member findMemberEx =
memberRepository.findById(memberEx.getMemberId());
//memberA의 돈이 롤백 되어야함
assertThat(findMemberA.getMoney()).isEqualTo(10000);
assertThat(findMemberEx.getMoney()).isEqualTo(10000);
}
}
스프링 부트가 자동으로 데이터소스와 트랜젝션 매니저를 자동으로 등록해준다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=