-
인프런 김영한 님의 스프링 DB 1 강의를 듣고 정리한 글 (
아직 듣지 않고 결제한 강의 들으면서 다시 정리하기)❓JDBC
애플리케이션을 개발 시 데이터는 대부분 데이터베이스에 저장한다.
애플리케이션 서버와 DB - 일반적인 사용법
1. 커넥션 연결
- 주로 TCP/IP를 사용해서 커넥션을 연결한다.
2. SQL 전달
- 애플리케이션 서버는 DB가 이해할 수 있는 SQL을 연결된 커넥션을 통해 DB에 전달한다.
3. 결과 응답
- DB는 전달된 SQL을 수행하고 그 결과를 응답한다. 애플리케이션 서버는 응답 결과를 활용한다.
⚠️ 문제 : 데이터베이스마다 커넥션 연결 방법, SQL 전달 방법, 결과 응답받는 방법이 모두 다르다.
⇒ 다른 종류의 DB로 변경시 애플리케이션 서버에 개발된 DB 사용 코드도 함께 변경
⇒ 개발자가 각각 DB마다 커넥션 연결, SQL 전달, 결과를 응답받는 방법을 새로 학습해야 함.
이를 해결하기 위해 JDBC 자바 표준 등장
JDBC 표준 인터페이스 ⭐
💡 JDBC(Java Database Connectivity)
- 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API다.
- JDBC는 데이터베이스에서 자료를 쿼리하거나 업데이트하는 방법을 제공한다.JDBC는 대표적으로 3가지 기능을 표준 인터페이스로 정의해 제공한다.
- java.sql.Connection - 연결
- java.sql.Statement - SQL을 담은 내용
- java.sql.ResultSet - SQL 요청 응답
📖 JDBC 드라이버
- JDBC 표준 인터페이스를 각각의 DB 벤더에서 자신의 DB에 맞도록 구현해 라이브러리로 제공한다.
- ex) MySQL DB에 접근할 수 있는 MySQL JDBC 드라이버
- ex) Oracle DB에 접근할 수 있는 Oracle JDBC 드라이버
문제 해결
두 가지 문제 해결
- 데이터베이스를 다른 종류의 데이터베이스로 변경할 때 데이터베이스 사용 코드를 변경해야 하는 문제
- 애플리케이션 로직은 JDBC 표준 인터페이스에 의존
- 따라서 DB에 맞는 JDBC 구현 라이브러리만 변경하면 된다.
- 다른 종류의 데이터베이스로 변경을 해도 사용코드를 그대로 유지할 수 있다.
- 개발자가 각각의 데이터베이스 커넥션 연결, SQL 전달, 결과를 응답받는 방법을 새로 학습해야 하는 문제
- 개발자는 JDBC 표준 인터페이스 사용법만 학습하면 된다.
⚠️ 표준화 한계
- 각각의 데이터베이스마다 SQL, 데이터타입 등의 일부 사용법이 다르다.
- 따라서 데이터베이스를 변경하면 JDBC 코드는 변경하지 않아도 되지만 SQL은 해당 데이터베이스에 맞도록 변경해야 한다.
- JPA를 사용하면 각각의 데이터베이스마다 다른 SQL을 정의해야 하는 문제도 해결할 수 있다.
📌 SQL Mapper / ORM
JDBC를 편리하게 사용하는 다양한 기술
- SQL Mapper
- ORM
- 등등
JDBC를 직접 사용하는 방법
- 애플리케이션 로직에서 SQL을 직접 JDBC로 전달
- 매우 복잡
SQL Mapper
장점
- SQL 응답 결과를 객체로 편리하게 변환
- JDBC 반복 코드 제거
단점
- 개발자가 SQL를 직접 작성해야 한다.
대표 기술
- Jdbc Template
- MyBatis
ORM
- ORM은 객체를 관계형 데이터베이스 테이블과 매핑해주는 기술이다.
- 개발자는 직접 SQL을 작성하지 않는다.
- ORM이 개발자 대신 SQL을 동적으로 만들어 실행해준다.
- 각각의 데이터베이스마다 다른 SQL을 사용하는 문제도 중간에서 해결해 준다.
대표 기술
- JPA
- 하이버네이트
- 이클립스링크
📖 JPA
- 자바 진영의 ORM 표준 인터페이스
- 이것을 구현한 것이 하이버네이트, 이클립스 링크 등의 구현체이다.
SQL Mapper vs ORM
SQL Mapper ORM SQL 직접 작성 직접 작성 ❌ 난이도 SQL만 작성하면 쉽다. 쉽지 않다. ❗이 모든 기술들도 내부에서는 모두 JDBC를 사용한다.
- JDBC가 어떤 식으로 동작하는지 기본 원리를 알아두어야 한다.
〽️ 데이터베이스 연결
데이터베이스에 연결하기 위한 필요 기본 정보
public class ConnectionConst { public static final String URL = "jdbc:h2:tcp://localhost/~/jdbc-test"; public static final String USERNAME = "sa"; public static final String PASSWORD = ""; }
JDBC를 사용해 실제 데이터베이스에 연결하는 코드
@Slf4j public class DBConnectionUtil { public static Connection getConnection() { try { Connection connection = DriverManager.getConnection( ConnectionConst.URL, ConnectionConst.USERNAME, ConnectionConst.PASSWORD); log.info("==== getConnection = {}, class = {}", connection, connection.getClass()); return connection; } catch (SQLException e) { throw new IllegalStateException(); } } }
- DriverManager.getConnection
- 데이터베이스 드라이버를 찾아 해당 드라이버가 제공하는 커넥션을 반환
🔍 H2 드라이버를 사용하는 경우
여기서 인터페이스 Connection의 구현체인 org.h2.jdbc.JdbcConnection 을 반환
- H2 데이터베이스 드라이버가 제공하는 H2 전용 커넥션
H2 드라이버의 Connection 코드(java.sql.Connection 인터페이스 구현)
public class JdbcConnection extends TraceObject implements Connection, JdbcConnectionBackwardsCompat, CastDataProvider { private static final String NUM_SERVERS = "numServers"; private static final String PREFIX_SERVER = "server"; private static boolean keepOpenStackTrace; private final String url; private final String user; ... }
❗ 데이터베이스가 변경되어도 DriverManager.getConnection() 코드는 변함이 없다.
❓어떻게 DB에 따른 드라이버를 찾는 것일까
JDBC 커넥션의 인터페이스 / 구현체
- JDBC는 java.sql.Connection 표준 커넥션 인터페이스 정의
- H2, MySQL 데이터베이스 드라이버는 JDBC Connection 인터페이스를 구현한 구현체 제공
DriverManager 커넥션 요청 흐름
📖 DriverManager
- JDBC가 제공
- 라이브러리에 등록된 DB 드라이버를 관리, 커넥션을 획득하는 기능 제공
📖 DriverManager.getConnection()- 애플리케이션 로직에서 커넥션이 필요하면 DriverManager.getConnection() 호출
- 라이브러리에 등록된 드라이버 목록을 자동으로 인식하고 순서대로 정보를 넘기면서 커넥션을 획득할 수 있는지 확인한다.
- 정보 : URL, 이름, 비밀번호 등 접속에 필요한 정보
- 각각의 드라이버는 URL 정보를 체크해 처리할 수 있는 요청인지 확인
- 처리할 수 없다면 다음 드라이버에게 순서를 넘긴다.
- 이렇게 찾은 커넥션 구현체를 반환
⌨️ CRUD
JDBC를 사용해 회원(Member) 데이터를 데이터베이스에 관리하는 기능 개발
Member
@Getter @Setter public class Member { private String memberId; private int money; public Member() {} public Member(String memberId, int money) { this.memberId = memberId; this.money = money; } @Override public String toString() { final StringBuilder sb = new StringBuilder("Member{"); sb.append("memberId='").append(memberId).append('\''); sb.append(", money=").append(money); sb.append('}'); return sb.toString(); } }
등록
MemberRepository
/** * 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 를 통해 database에 쿼리를 날리낟. PreparedStatement pstmt = null; try { con = getConnection(); // 커넥션이 prepareStatement 를 만들고 sql 을 넘긴다. pstmt = con.prepareStatement(sql); // SQL에 대한 파라미터 바인딩 // 여기서 파라미터는 memberId, money 두 가지이다. 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를 해주어야 함 // 외부 리소스를 사용하는것이기 때문에 닫아주지 않으면 계속 유지가 될 수 있다. close(con, pstmt, null); } } // con, stmt, rs 를 close를 하는 과정 // 각각에 try-catch를 사용하지 않으면 하나에서 exception 발생시 다른 것들을 close // 못해주는 상황이 발생할 수 있기 때문 private void close(Connection con, Statement stmt, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException e) { log.info("resultSet close error", e); } } if (stmt != null) { try { stmt.close(); } catch (SQLException e) { log.info("prepareStatement close error", e); } } if (con != null) { try { con.close(); } catch (SQLException e) { log.info("connection close error", e); } } } // connection을 가져오는 코드 private Connection getConnection() { return DBConnectionUtil.getConnection(); } }
- sql - 데이터베이스에 전달할 SQL 정의
- con.prepareStatement(sql) - 데이터베이스에 전달할 SQL과 파라미터로 전달할 데이터들 준비
- pstmt.executeUpdate() : statement를 통해 준비된 SQL을 커넥션을 통해 실제 DB에 전달
📖 리소스 정리(close)
- 쿼리를 실행하고 나면 리소스를 정리해주어야 한다.
- 리소스 정리 시 항상 역순으로 해야 한다.
- 예외가 발생하든 하지 않든 항상 수행되어야 한다 ⇒ finally 구문에서 작성 ❗
조회
MemberRepository
public Member findById(String memberId) throws SQLException { String sql = "select * from member where member_id = ?"; Connection con = null; PreparedStatement pstmt = null; // select 쿼리의 결과를 담고 있다. ResultSet rs = null; try { con = getConnection(); pstmt = con.prepareStatement(sql); pstmt.setString(1, memberId); // pstmt.executeUpdate() : 데이터 변경시 사용 // pstmt.executeQuery() : 데이터 조회시 사용, ResultSet 반환 rs = pstmt.executeQuery(); // rs.next()로 한 번은 호출을 해주어야 실제 데이터에 접근할 수 있다. // 한 번 호출 => 데이터가 있는지 없는지 확인 // 첫 번째 데이터가 있다면 true 반환, 없다면 false 반환 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) { log.error("db error", e); throw e; } finally { close(con, pstmt, rs); } }
📖 Result Set
- select 쿼리의 결과가 순서대로 들어간다.
- ex) “SELECT member_id, money” → ‘member_id’, ‘money’라는 이름으로 데이터 저장
- ResultSet 내부에 있는 커서(cursor)를 이동해 다음 데이터를 조회할 수 있다.
- rs.next() : 커서가 다음으로 이동
- 최초의 커서는 데이터를 가리키고 있지 않아 rs.next() 를 최초 한 번은 호출해야 한다.
수정 / 삭제
MemberRepository
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); // 쿼리 실행 후 영향받은 row 수 반환 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); } }
참고자료
https://www.inflearn.com/course/스프링-db-1
'🌱 spring' 카테고리의 다른 글
ThreadLocal (0) 2023.01.18 🌊 Connection Pool (0) 2023.01.13 Spring Interceptor에서 Request 데이터 처리 (1) 2022.12.06 애플리케이션 컨텍스트와 빈팩토리 (0) 2022.11.16 JPA - @ElementCollection (0) 2022.11.02