김영한 님의 스프링 DB 1편 - 데이터 접근 핵심 원리 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard
어플리케이션 서버가 DB를 사용하는 과정
커넥션 연결 : 주로 TCP/IP를 사용해서 커넥션 연결을 수행
SQL 전달 : 어플리케이션 서버는 DB가 이해할 수 있는 SQL을 연결된 커넥션을 통해 DB에 전달
결과 응답 : DB는 전달된 SQL을 수행하고 그 결과를 응답, 어플리케이션 서버는 응답 결과를 활용
BUT> DB마다 커넥션을 연결하는 방법, SQL을 전달하는 방법, 결과를 응답 받는 방법이 모두 다르다는 문제점이 존재
DB를 변경하면 어플리케이션 서버에 작성된 DB 사용 코드도 함께 변경해야한다
개발자가 각 DB마다 커넥션 연결, SQL 전달, 결과 응답 받는 방법을 학습해야한다
➡️이런 문제를 해결하기 위해 JDBC 등장
JDBC( Java Database Connectivity )는 자바에서 DB에 접속할 수 있도록 하는 자바 API
JDBC는 DB에서 자료를 쿼리하거나 업데이트하는 방법을 제공
JDBC 표준 인터페이스
java.sql.Connection
- 연결
java.sql.Statement
- SQL을 담은 내용
java.sql.ResultSet
- SQL 요청 응답
JDBC 인터페이스를 각각의 DB회사에서 자신의 DB에 맞도록 구현해서 라이브러리로 제공하는 것이 JDBC 드라이버
즉, JDBC 드라이버는 JDBC 표준 인터페이스를 구현해서 만든 드라이버
어떤 DB의 드라이버를 사용하는지에 관계 없이 개발자는 어플리케이션 로직을 작성할 때 JDBC 표준 인터페이스에만 맞춰서 개발하면 된다
DB가 바뀌어도 어플리케이션 로직은 JDBC 표준 인터페이스를 그대로 사용하고 드라이버
( 구현체 )만 변경하면 된다
DB를 변경했을 때 어플리케이션 서버의 DB 사용 코드를 함께 변경해야하는 문제
어플리케이션 로직이 이제 JDBC 표준 인터페이스에만 의존하기 때문에 DB를 변경하고 싶으면 JDBC 구현 라이브러리만 변경하면 된다
어플리케이션 서버의 사용 코드는 그대로 유지된다
각 DB의 커넥션 연결, SQL 전달, 결과를 응답 받는 방법을 학습해야하는 문제
각 DB마다 SQL, 데이터 타입 등의 일부 사용법이 다르고, 실무에서 기본으로 사용하는 페이징 SQL도 각 DB마다 사용법이 다르다
DB를 변경해도 JDBC 코드는 변경하지 않아도 되지만 SQL은 변화한 DB에 맞게 수정해야한다
JPA(Java Persistence API)를 사용하면 이렇게 각각의 데이터베이스마다 다른 SQL을 정의해야 하는 문제도 많은 부분 해결할 수 있다
JDBC를 직접 사용하는 방식
JDBC는 오래된 기술이기도 하고 사용하는 방법이 복잡하기 때문에 직접 JDBC를 사용하기 보다는 JDBC를 편리하게 사용할 수 있는 다양한 기술들을 활용
대표적으로 SQL Mapper, ORM 기술로 나눌 수 있다
SQL Mapper는 SQL 응답 결과를 객체로 편리하게 변환해준다
JDBC의 반복 코드를 제거해준다
but> 개발자가 직접 SQL을 작성해야한다
대표적으로 스프링 JDBC Template, MyBatis가 있다
ORM은 객체를 관계형 데이터베이스 테이블과 매핑해주는 기술
객체를 전달하면 구현체가 객체의 매핑 정보를 보고 쿼리를 직접 만들어낸다
즉, 반복적인 SQL을 직접 작성하지 않는다
ORM 기술이 개발자 대신 SQL을 동적으로 만들어 실행해준다
각각의 데이터베이스마다 다른 SQL을 사용하는 문제도 중간에서 해결해준다
대표적으로 JPA, 하이버네이트, 이클립스링크가 있다
JPA는 자바 진영의 ORM 표준 인터페이스이고, 이것을 구현한 것으로 하이버네이트와 이클립스링크 등의 구현 기술이 있다
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);
}
}
DriverManager.getConnection() : 라이브러리에 있는 DB 드라이버를 찾아 해당 드라이버가 제공하는 커넥션( Connection )을 반환해준다
위의 코드는 H2 데이터베이스 드라이버가 작동해서 실제 DB와 커넥션을 맺고 결과를 반환해준다
Connection은 인터페이스이고 getConnection()을 통해 가져온 것은 구현체( Connection 구현체 )
로그는 아래처럼 찍히는데 가져오는 구현체가 JdbcConnection
라는 의미
class=class org.h2.jdbc.JdbcConnection
JdbcConnection
는 H2 데이터베이스 드라이버가 제공하는 H2 전용 커넥션이고 JDBC 표준 커넥션 인터페이스인 java.sql.Connection 인터페이스를 구현하고 있다
JDBC는 java.sql.Connection
표준 커넥션 인터페이스를 정의한다
H2 데이터베이스 드라이버는 JDBC Connectin 인터페이스를 구현한 org.h2.jdbc.JdbcConnection
을 제공
JdbcConnection이 있어야 H2 데이터베이스와 통신이 가능하다
DriverManager
는 라이브러리에 등록된 DB 드라이버들을 관리하고, 커넥션을 획득하는 기능을 제공어플리케이션 로직에서 커넥션이 필요하면 DriverManager.getConnection()
을 호출
DriverManager는 라이브러리에 등록된 드라이버 목록을 자동으로 인식하고 이 드라이버들에게 순서대로 정보를 넘겨서 커넥션을 획득할 수 있는지 확인
각각의 드라이버는 URL 정보를 체크해서 본인이 처리할 수 있는 요청인지 확인해서 처리할 수 있는 경우 실제 데이터베이스에 연결해서 커넥션을 획득하고 이 커넥션을 클라이언트에 반환, 본인이 처리할 수 없으면 처리할 수 없다는 결과를 반환하고 다음 드라이버에게 순서가 넘어간다
이렇게 찾아진 커넥션 구현체가 클라이언트에게 반환된다
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);
pstmt.setString(1, member.getMemberId());
pstmt.setInt(2, member.getMoney());
pstmt.executeUpdate();
return member;
} catch (SQLException e) {
log.info("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
}
getConnection()
: 이전에 만든 DBConnectionUtil을 통해 데이터베이스 커넥션을 획득con.prepareStatement(sql)
: 데이터베이스에 전달할 SQL과 파라미터로 전달할 데이터들을 준비
?
자리에 파라미터를 넣기 위해 setXXX()
메서드를 이용하고, 인덱스는 1부터 시작한다pstmt.executeUpdate()
PreparedStatement를 통해 준비된 SQL을 커넥션을 통해 데이터베이스에 전달하면 실제로 쿼리가 수행된다
위 메서드는 int를 반환하는데 영향 받은 DB row의 수를 의미
Connection과 PreparedStatement를 획득한 역순으로 닫아 주어야한다
실제 TCP/IP를 커넥션을 이용해 외부 리소스를 사용하는 것이기 때문에 닫지 않으면 커넥션이 끊어지지 않고 계속 유지되기 때문
try 쪽에서 예외가 발생해도 리소스 정리를 실행하기 위해 finally에 작성
finally에서 리소스를 정리해야하기 때문에 미리 Connection con = null;
처럼 선언을 해두어야 한다
finally에서 바로 Connection, PreparedStatement 를 닫지 않은 이유
finally에서 pstmt를 닫는 중에 예외가 발생하면 Connection을 닫을 수 없다
예외가 발생해도 다른 리소스를 정상적으로 닫을 수 있도록 각 close() 역시 try, catch로 묶어서 닫아야한다
Connection, PreparedStatement, ResultSet을 각각 try, catch로 묶어서 닫기 위한 메서드가 close()
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("memberId"));
member.setMoney(rs.getInt("money"));
return member;
} else{
throw new NoSuchElementException("member not found memberId = " + memberId);
}
}
...
}
catch, finally는 이전과 동일해서 생략
rs = pstmt.executeQuery()
데이터를 변경할 때는 executeUpdate() 를 사용하지만, 데이터를 조회할 때는 executeQuery() 를 사용
executeQuery() 는 결과를 ResultSet 에 담아서 반환
ResultSet은 select 쿼리의 결과가 순서대로 들어간다
ResultSet 내부에 있는 커서( cursor )를 이동해서 다음 데이터를 조회할 수 있다
rs.next()
커서가 다음으로 이동
커서가 이동했을 때 데이터가 있으면 true, 없으면 false를 반환
최초의 커서는 데이터를 가리키고 있지 않기 때문에 rs.next()
를 최초 한번은 호출해야 데이터를 조회할 수 있다
getXXX()
: 커서가 가리키고 있는 위치의 데이터를 XXX 타입으로 반환
repository.delete(member.getMemberId());
assertThatThrownBy(() -> repository.findById(member.getMemberId()))
.isInstanceOf(NoSuchElementException.class);
삭제를 진행한 후 제대로 삭제되었는지 확인하는 테스트
조회 메서드에서 조회되지 않았을 때 NoSuchElementException
예외를 던지는 것을 활용
assertThatThrownBy
를 통해 삭제된 멤버를 조회했을 때 NoSuchElementException
이 발생하면 정상적으로 지워진 것으로 판단