인프런 김영한 - 스프링 DB 1 강의를 듣고 정리한 글 입니다.
jdbc 의 등장배경과 사용방법, jdbc 연결방법에 대해서 다룹니다.
목차
- jdbc 란
- jdbc의 역사?
- JDBC 와 최신 데이터 접근 기술
- 데이터베이스 JDBC 커넥션
- JDBC DriverManager 이해
- JDBC 를 사용한 간단한 CRUD
1. jdbc 란
JDBC(Java Database Connectivity) 는 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API 입니다.
2. jdbc 등장이유
어플리케이션을 개발할 때 중요한 데이터는 대부분 데이터베이스에 보관합니다.
클라이언트가 서버(어플리케이션)에 요청을 보내면, 데이터를 저장하거나 조회하는 것을 아래처럼 데이터베이스를 사용하여 처리합니다.
- 커넥션 연결: 주로 TCP/IP를 사용해서 커넥션을 연결한다.
- SQL 전달: 애플리케이션 서버는 DB가 이해할 수 있는 SQL을 연결된 커넥션을 통해 DB에 전달한다.
- 결과 응답: DB는 전달된 SQL을 수행하고 그 결과를 응답한다. 애플리케이션 서버는 응답 결과를 활용한다.
물론 이 모든건 옛날 이야기입니다..ㅎ
이러한 방법의 문제점은, 각각의 데이터베이스(mysql, redis, mongoDb 등등) 사용방법이 다르고, 접근 방법이 달라 데이터베이스를 변경할 때마다 데이터베이스 접근 코드도 함께 변경되어야 한다는 점입니다.
➡️ jdbc가 나온 이유 : 이러한 문제의 해결을 위해 JDBC 라는 자바에서 데이터베이스 접근 표준 인터페이스가 나왔습니다. 🔥
대표적으로 다음 3가지 기능을 표준 인터페이스로 정의해서 제공합니다.
- java.sql.Connection - 연결
- java.sql.Statement - SQL을 담은 내용
- java.sql.ResultSet - SQL 요청 응답
DB 드라이버란
JDBC 는 표준 "인터페이스" 이기 때문에, JDBC 만으로는 아무것도 할 수 없습니다.
구현체가 필요하고, 각각 DB 회사에서 JDBC 인터페이스의 구현체로 만들어 라이브러리로 제공하는게 JDBC 라이브러리 입니다.
- 예를 들어서 MySQL DB에 접근할 수 있는 것은 MySQL JDBC 드라이버라 하고,
- Oracle DB에 접근할 수 있는 것은 Oracle JDBC 드라이버라 합니다
JDBC 장점 및 이점
jdbc 가 등장하여서 2가지 문제가 해결되었습니다.
1. 데이터 베이스 변경시, 불필요한 어플리케이션의 코드도 변경되어야 했던 문제
- 데이터베이스 연결 로직을 jdbc 인터페이스에 의존하게 됨으로써, 다른 데이터베이스 종료로 변경하여도, jdbc 구현 라이브러리만 변경해주면 됩니다.
- 어플리케이션의 비지니스 로직은 영향을 받지 않습니다.
2. 데이터베이스마다 달랐던 커넥션 연결, sql 전달, 응답 방법을 jdbc 표준 라이브러리가 동일하게 제공하기 때문에, 따로 학습하지 않아도 됩니다.
JDBC 한계
📌 jdbc 표준 인터페이스 등장으로 일반적인 부분은 공통화 하였지만, 각각 데이터베이스마다 제공하는 기능과 특별한 양식들은 다르기 때문에 결국, 한계가 있었다고 합니다.
- ex) 페이징 SQL - 각 db 마다 사용방법이 다름
👏🏻 즉, 데이터베이스 연결 코드는 JDBC 를 사용함으로 써, 변경하지 않아도 되었지만 SQL 문은 해당 데이터베이스에 맞게 수정해주어야 하는 한계점이 존재합니다.
-> 현재 이 부분은 "JPA" 로 상당부분을 해결합니다!
3. JDBC 와 최신 데이터 접근 기술
JDBC의 한계를 극복하기 위해서 대표적으로 SQL Mapper 혹은 ORM기술이 등장했습니다. (mybatis vs jpa??)
SQL Mapper
- 장점: JDBC를 편리하게 사용하도록 도와준다.
- JDBC의 반복 코드를 제거해준다.
- SQL 응답 결과를 객체로 편리하게 변환해준다.
- 단점: 개발자가 SQL을 직접 작성해야한다.
(어디까지나 ORM 에 비해서)
대표 기술: 스프링 JdbcTemplate, MyBatis
ORM 기술
- ORM은 객체를 관계형 데이터베이스 테이블과 매핑해주는 기술이다.
- 이 기술 덕분에 개발자는 반복적인 SQL을 직접 작성하지 않고, ORM 기술이 개발자 대신에 SQL을 동적으로 만들어 실행해준다.
- 추가로 각각의 데이터베이스마다 다른 SQL을 사용하는 문제도 중간에서 해결해준다.
- 대표 기술: JPA, 하이버네이트, 이클립스링크
JPA는 자바 진영의 ORM 표준 인터페이스이고, 이것을 구현한 것으로 하이버네이트와 이클립스 링크 등의 구현 기술이 있다.
🔥 하지만, 2가지 기술 모두 base low level 에서는 JDBC를 사용합니다!
4. 데이터베이스 JDBC 커넥션
jdbc 커넥션을 얻어서, h2 데이터베이스에 연결해보겠습니다.
실행전, h2 디비를 백그라운드로 실행시켜야 코드가 동작합니다.
ConnectionConst.class
- h2에 연결하기위한 상수 정보만을 담은 추상클래스를 만들어줍니다.
- 아래의 변수들을 이용해 정보를 호출할건데, 지금부터 아래의 코드 모두 ConnectionConst 를 전역패키지로 import 해 변수만을 불러서 이용할 예정입니다.
- 추상클래스는 상수와 추상메소드만을 가질수 있고 인스턴스화 하지 않고 상수에 접근할 수 있습니다.
//상수 데이터만 담은 class - 객체 생성하지 않기위해 추상클래스로
public abstract class ConnectionConst {
public static final String URL = "jdbc:h2:tcp://localhost/~/test";
public static final String USERNAME = "sa";
public static final String PASSWORD = "";
}
DBConnectionUtil.class
- JDBC를 이용해서, 실제 h2 에 연결할 커넥션을 얻어오는 코드 입니다.
- JAVA 에서 데이터베이스에 연결하려면 JDBC가 제공하는 DriverManager.getConnection(..) 메소드를 사용해서 얻어와야합니다.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
//DB 연결
@Slf4j
public class DBConnectionUtil {
public static Connection geConnection(){
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);
}
}
}
🙌🏻 그럼 위에서 말한 것 처럼, DB 가 달라지는걸 어디서 어떻게 신경쓰길래, 사용자는 JDBC 만 사용하면 디비 변경 체크를 하지 않아도 될까요?
- 여기서 사용된 DriverManager 가 External Libraries 에 있는 현재 다운되어진 라이브러리 중 디비 라이브러리를 찾아옵니다.
- 그래서 DriverManager.getConnection() 은 데이터베이스 드라이브러를 찾아 해당 드라이버가 제공하는 커넥션을 반환해주는 것 입니다.
5. JDBC DriverManager 이해
- JDBC DriverManager란, JDBC 드라이버 세트를 관리하기 위한 기본 서비스입니다.
- DriverManager 초기화는 느리게 수행되며 스레드 컨텍스트 클래스 로더를 사용하여 서비스 공급자를 찾습니다.
- getConnection 메소드가 호출되면 DriverManager 는 초기화 시 로드된 드라이버와 현재 애플리케이션과 동일한 클래스 로더를 사용하여 명시적으로 로드된 드라이버 중에서 적절한 드라이버를 찾으려고 시도합니다.
📌 쉽게 정리하자면, JDBC 인터페이스에서 커넥션을 얻기위한 메소드이고, 이 커넥션은 각각 DB 의 구현체를 의미합니다.
📌 JDBC가 제공하는 DriverManager 는 라이브러리에 등록된 DB 드라이버들을 관리하고, 커넥션을 획득하는 기능을 제공합니다.
getConnection 과정
(다수의 디비 라이브러리에서 맞는 디비 구현체를 가져오는 과정!!)
- 애플리케이션 로직에서 커넥션이 필요하면 DriverManager.getConnection() 을 호출한다.
- DriverManager 는 라이브러리에 등록된 드라이버 목록을 자동으로 인식한다. 이 드라이버들에게
- 순서대로 다음 정보를 넘겨서 커넥션을 획득할 수 있는지 확인한다.
- URL: 예) jdbc:h2:tcp://localhost/~/test
- 이름, 비밀번호 등 접속에 필요한 추가 정보
- 여기서 각각의 드라이버는 URL 정보를 체크해서 본인이 처리할 수 있는 요청인지 확인한다.
- 예를 들어서 URL이 jdbc:h2 로 시작하면 이것은 h2 데이터베이스에 접근하기 위한 규칙이다.
- 따라서 H2 드라이버는 본인이 처리할 수 있으므로 실제 데이터베이스에 연결해서 커넥션을 획득하고 이 커넥션을 클라이언트에 반환한다.
- 반면에 URL이 jdbc:h2 로 시작했는데 MySQL 드라이버가 먼저 실행되면 이 경우 본인이 처리할 수 없다는 결과를 반환하게 되고, 다음 드라이버에게 순서가 넘어간다.
- 이렇게 찾은 커넥션 구현체가 클라이언트에 반환된다.
6. JDBC 를 사용한 간단한 CRUD
JDBC를 사용하여 DriverManager로 직접 커넥셕을 얻어와 디비에 연결해 쿼리를 날려보는 로직입니다.
단순한
Member.class
@Data
public class Member {
private String memberId;
private int money;
}
MemberRepositoryV0.class
📌 커넥션을 얻어 쿼리를 날리는 Repository입니다. 매우 길어 과정을 요약하자면
- 커넥션을 얻는다.
- 커넥셩을 닫을 객체를 null 로 초기화하는 이유는, SQLException 은 Checked Exception 이기 때문에, try catch 로 처리해주기 위함이다.
- PreparedStatement 객체를 이용해 쿼리를 디비에 날린다.
- 모든 처리가 완료되었다면, 연결된 커넥션을 순차적으로 역순으로 끊어주어야한다.
- ResultSet 은 디비에서 불러온 결과를 저장해서 cursor 를 이용해 값을 조회하는 역할을 한다. (조회 저장소다)
@Slf4j
public class MemberRepositoryV0 {
public Member save(Member member) throws SQLException {
String sql = "insert into member(member_id, money) values (?, ?)";
Connection connection = null;
PreparedStatement preparedStatement = null; // 이걸로 쿼리를 날림?
try {
connection = getConnection();
preparedStatement = connection.prepareStatement(sql);
//파라미터 바인딩
preparedStatement.setString(1, member.getMemberId());
preparedStatement.setInt(2, member.getMoney());
preparedStatement.executeUpdate();
return member;
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
//db 연결을 끊어줘야함
preparedStatement.close();
//여기서 익셉션이 터지면 connection 이 안끊어짐 그래서 다 묵어야함
connection.close();
//요래 해야함
close(connection, preparedStatement, null);
}
}
public Member findById(String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
connection = getConnection();
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, memberId);
resultSet = preparedStatement.executeQuery();
if (resultSet.next()) {
Member member = new Member();
member.setMemberId(resultSet.getString("member_id"));
member.setMoney(resultSet.getInt("money"));
return member;
}else{
throw new NoSuchElementException("member not found memberId : " + memberId);
}
} catch (SQLException e) {
log.error("error : ", e);
throw e;
}finally {
close(connection,preparedStatement,resultSet);
}
}
public void update(String memberId, int money) throws SQLException {
String sql = "update member set money=? where member_id=?";
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
connection = getConnection();
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setInt(1, money);
preparedStatement.setString(2, memberId);
int resultSize = preparedStatement.executeUpdate();
log.info("resultSize = {}", resultSize);
} catch (SQLException e) {
log.error("error : ", e);
throw e;
}finally {
close(connection,preparedStatement,null);
}
}
public void delete(String memberId) throws SQLException {
String sql = "delete from member where member_id=?";
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
connection = getConnection();
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, memberId);
int resultSize = preparedStatement.executeUpdate();
log.info("resultSize = {}", resultSize);
} catch (SQLException e) {
log.error("error : ", e);
throw e;
}finally {
close(connection,preparedStatement,null);
}
}
private void close(Connection con, Statement stmt, ResultSet resultSet) {
//이렇게 해야 Exception 이 터져도 catch 로 잡기 때문에 각각의 close 가 다른 close 에 영향을 주지 않음
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
log.error("error", e);
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
log.error("error", e);
}
}
if (con != null) {
try {
con.close();
} catch (SQLException e) {
log.error("error", e);
}
}
}
private static Connection getConnection() {
return DBConnectionUtil.geConnection();
}
}
Connection 얻어온 Repository TEST 코드
MemberRepositoryV0Test.class
@Slf4j
class MemberRepositoryV0Test {
MemberRepositoryV0 repositoryV0 = new MemberRepositoryV0();
@Test
void crud() throws SQLException {
//save
Member memberV0 = new Member("memberV0", 100000);
repositoryV0.save(memberV0);
//findById
Member findMember = repositoryV0.findById(memberV0.getMemberId());
log.info("findMember = {}", findMember);
//2개는 다른 인스턴스지만, isEqualTo 가 equals() 를 호출해 값만 비교 하기 때문에 true
//@Data 롬복은, 모든 상태 값을 비교할수 있는 equals 와 고유한 hashCode 를 자동으로 만들어준다.
assertThat(findMember).isEqualTo(memberV0);
//update
int updateMoney = 200000;
repositoryV0.update(memberV0.getMemberId(), updateMoney);
Member updateMember = repositoryV0.findById(memberV0.getMemberId());
assertThat(updateMember.getMoney()).isEqualTo(updateMoney);
//delete
repositoryV0.delete(memberV0.getMemberId());
assertThatThrownBy(() -> repositoryV0.findById(memberV0.getMemberId()))
.isInstanceOf(NoSuchElementException.class);
}
}
여기까지 자바 표준 데이터베이스 인터페이스인 JDBC 를 사용하여 DB 에 직접 연결하여 crud 를 해본 포스팅이였습니다.
jsp & servlet 을 학교에서 배울때가 생각났던 시간이였고,,
그만큼 이 강의를 들으면서 학교에서는 얼마나 옛날 기술을 알려줬던건가.. 하는 생각이 들어씀다
아무튼 유익해!
참고
'Spring > Spring 김영한' 카테고리의 다른 글
[Spring DB 1편] (3) 트랜잭션의 이해 (0) | 2022.10.28 |
---|---|
[Spring DB 1편] (2) 커넥션풀과 데이터소스 이해 (0) | 2022.09.30 |
[Spring 기본편] (2) Spring 컨테이너의 필요성 - 스프링 컨테이너란, Bean의 관리 (0) | 2022.05.14 |
[Spring 기본편] (1) 스프링이란 / 스프링과 객체 지향에 대하여 (0) | 2022.05.13 |