Application 개발 시 중요 데이터는 대부분 데이터 베이스에 보관
클라이언트가 애플리케이션 서버를 통해 데이터 저장, 조회시 애플리케이션 서버는 다음 과정을 통하여 데이터 베이스를 사용한다.
커넥션 연결 (주로 TCP/IP) → Application Server는 DB가 이해할 수 있는 SQL을 DB로 전달 → DB는 전달받은 SQL 수행 후 결과 응답, 애플리케이션 서버는 응답결과 활용
각각의 데이터 베이스 마다 사용법이 다르다.
커넥션을 연결하는 방법, SQL을 전달하는 방법, 결과를 응답받는 방법까지 다 다르다. (관계형 DB는 수십개)
때문에 2가지 문제가 발생,
이런 문제를 해결하기 위한 표준. JDBC가 등장
JDBC(Java Database Connectivity)는 Java에서 DB에 접속할 수 있도록 하는 JAVA API
DataBase에서 자료를 쿼리하거나 업데이트 하는 방법을 제공한다. from wikipedia
JDBC 표준 인터페이스
java.sql.Connection
- 연결java.sql.Statement
- SQL을 담은 내용java.sql.ResultSet
- SQL 요청 응답JAVA는 표준 인터페이스를 위와 같이 정의해 두었다. 다만 인터페이스만 있다고 해서 기능이 동작하지는 않는다.
이 JDBC 인터페이스를 각각의 DB 벤더가 자신의 DB에 맞도록 구현하여 라이브러리로 제공한다. (JDBC 드라이버) ex. MySql Driver, Oracle Driver 등
이로써 두가지의 문제를 해결 할 수 있다.
한계점
JDBC는 오래 되었고(1997년 출시), 사용 방법 복잡함. 때문에 최근엔 JDBC를 직접 사용하기 보다는 JDBC를 편리하게 사용하는 다양한 기술이 존재한다. ex. SQL Mapper, ORM 기술 등
SQL Mapper, ORM 둘 다 JDBC를 사용. 따라서 JDBC가 어떻게 동작하는지 기본 원리를 알아두어야 한다.
그래야 해당 기술들을 더 깊이 이해하고, 문제가 발생했을 때 근본적인 문제를 찾아 해결할 수 있다.
JDBC가 제공하는 DriverManager
는 라이브러리에 등록된 DB 드라이버들 관리, 커넥션을 획득하는 기능을 제공
DriverManager.getConnetcion()
호출.DriverManager
는 라이브러리에 등록된 드라이버 목록 자동인식, 순서대로 다음 정보를 넘겨 맞는 드라이버에게 커넥션을 획득할 수 있는지 확인한다. (드라이버 종류 , 이름, 비밀번호 등)jdbc:h2
로 시작하면 h2 DB에 접근하기 위한 규칙. 따라서 H2 드라이버는 본인이 처리할 수 있으므로 실제 DB에 연결해서 커넥션을 획득하고 이 커넥션을 클라이언트에 반환한다. 다르다면 다음 드라이버에게 순서가 넘어간다.ConnectionConst
package hello.jdbc.connection;
public class ConnectionConst {
static final String URL = "jdbc:h2:tcp://localhost/~/test";
static final String USERNAME = "sa";
static final String PASSWORD = "";
}
DBConnectionUtil
package hello.jdbc.connection;
import lombok.extern.slf4j.Slf4j;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import static hello.jdbc.connection.ConnectionConst.*;
@Slf4j
public class DBConnectionUtil {
public static Connection getConnection() {
try {
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("get connection = {}, class={}", connection, connection.getClass());
return connection;
} catch (SQLException e) {
throw new IllegalStateException(e);
}
}
}
Member
package hello.jdbc.domain;
import lombok.Data;
@Data
public class Member {
private String memberId;
private int money;
public Member() {
}
public Member(String memberId, int money) {
this.memberId = memberId;
this.money = money;
}
}
MemberRepositoryV0 - 회원 등록
package hello.jdbc.repository;
import hello.jdbc.connection.DBConnectionUtil;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import java.sql.*;
/**
* JDBC - DriverManager 사용
*/
@Slf4j
public class MemberRepositoryV0 {
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);
//파라미터 바인딩 (?,?) 하지 않으면 sql Injection 공격 위험 ! !
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 {
//con.close(); // 외부 리소스를 쓰고 있는 것인데, 안 닫으면 계속 유지가 되면서 떠 다닌다 -> 연결이 안 끊어진다.
close(con, pstmt, null);
}
}
private void close(Connection con, Statement stmt, ResultSet rs) throws SQLException {
if (rs != null) {
try{
rs.close();
} catch (SQLException e){
log.info("error", e);
}
}
if (stmt != null){
try {
stmt.close(); // SQLException 터져도 캐치로 잡는다 con에 영향을 안준다.
} catch (SQLException e){
log.info("error", e);
}
}
if(con != null) {
try {
con.close();
} catch (SQLException e){
log.info("error", e);
}
}
}
private static Connection getConnection() {
return DBConnectionUtil.getConnection();
}
}
sql
: DB에 전달할 쿼리 정의. 데이터 등록 작업이므로 insert sql
을 준비con.prepareStatement(sql)
: DB에 전달할 SQL과 파라미터로 전달할 데이터 준비String
→ setString 사용 Int → setInt
사용pstmt.executeUpdate()
: Statement를 통해 준비된 SQL을 커넥션을 통하여 DB에 전달, excuteUpdate()
는 Int 반환, 영향 받은 DB row수가 된다.Connection
, PrepareStatement
finally
구문에 주의하여 작성MemberRepositoryV0Test
package hello.jdbc.repository;
import hello.jdbc.domain.Member;
import org.junit.jupiter.api.Test;
import java.sql.SQLException;
class MemberRepositoryV0Test {
MemberRepositoryV0 repository = new MemberRepositoryV0();
@Test
void crud() throws SQLException {
Member member = new Member("memberV0", 10000);
repository.save(member);
}
}
데이터베이스에서 select * from member
쿼리를 실행하면 데이터가 저장된 것을 확인할 수 있다.
MemberRepositoryV0 - 회원 조회 추가
...
import java.util.NoSuchElementException;
public Member findById(String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
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 member Id =" + memberId);
// error 메세지를 잘 정의해주는 것이 좋다. key 값을 넣어주어 어느 곳에서 터지는지 확인하기 위해
}
} catch(SQLException e){
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, rs);
}
}
findById() - 쿼리 실행
excuteUpdate()
를 사용하지만, 데이터를 조회할 때는 executeQuery()
를 사용한다. 결과는 ResultSet에 담아 반환한다.ResultSet
rs.next()
로 커서를 이동할 수 있다. 참고로 최초는 아무 데이터를 가르키지 않고 있어서, 최소 한번은 호출해야 데이터를 조회할 수 있다.rs.next() → true
: 데이터 있음rs.next() → false
: 데이터 없음rs.getString(”{data name}”)
, rs.getInt(”{data name}”)
을 사용한다.MemberRepositoryV0Test - 회원 조회 테스트
Member findMember = repository.findById(member.getMemberId());
log.info("findMember={}", findMember);
assertThat(findMember).isEqualTo(member);
실행결과
MemberRepositoryV0Test - findMember=Member(memberId=memberV0, money=10000)
수정,삭제는 등록과 비슷하다. 등록, 수정, 삭제처럼 데이터를 변경하는 쿼리는 excuteUpdate()
를 사용한다.
MemberRepositoryV0 - 회원 수정, 삭제
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);
}
}
MemberRepositoryV0Test - 회원 수정, 삭제
//update : money : 10000 -> 20000
repository.update(member.getMemberId(), 20000);
Member updatedMember = repository.findById(member.getMemberId());
assertThat(updatedMember.getMoney()).isEqualTo(20000);
//delete
repository.delete(member.getMemberId());
//삭제 된 것 검증은 어떻게 ?
Assertions.assertThatThrownBy(() -> repository.findById(member.getMemberId()))
.isInstanceOf(NoSuchElementException.class);