내용 : MyBatis, JPA를 사용하기 전에 JDBC에 대한 정리
우선 H2데이터베이스를 사용해서 테스트 해보자. 가볍고 편리한 DB
- 데이터베이스 파일 생성 방법
- 사용자명은
sa- JDBC URL에 다음을 입력
jdbc:h2:~/test (최초 한번) -> 이 경우연결 시험을 호출하면 오류가 발생한다.연결`을 직접 눌러주어야 한다.~/test.mv.db파일 생성- 이후부터는
jdbc:h2:tcp://localhost/~/test이렇게 접속테이블 생성해준다.(예시)
drop table member if exists cascade; create table member ( member_id varchar(10), money integer not null default 0, primary key (member_id) ); insert into member(member_id, money) values ('hi1',10000); insert into member(member_id, money) values ('hi2',20000);
애플리케이션을 개발할 때 중요한 데이터는 대부분 데이터베이스 보관한다.
1. 커넥션 연결 : 주로 TCP/IP를 사용해서 커넥션 연결
2. SQL 전달 : 애플리케이션 서버는 DB가 이해할 수 있는 SQL을 연결된 커넥션 통해 DB에 전달
3. 결과 응답 : DB는 전달된 SQL을 수행하고 그 결과를 응답한다.문제는 각각의 데이터베이스마다 커넥션을 연결하는 방법, SQL을 전달하는 방법, 그리고 결과를 응답 받는 방법이 모두 다르다는 점이다.
이런 문제를 해결하기 위해 JDBC라는 자바 표준이 등장한다.
대표적으로 다음 3가지 기능을 표준 인터페이스로 정의해서 제공한다.
- java.sql.Connection - 연결
- java.sql.Statement - SQL을 담은 내용
- java.sql.ResultSet - SQL 요청 응답
그런데 인터페이스만 있다고해서 기능이 동작하지는 않는다. 이 JDBC 인터페이스를 각각의 DB회사에서 자신의 DB에 맞도록 구현해서 라이브러리로 제공하는데, 이것을 JDBC 드라이버라 한다. 예를 들어 MySQL DB에 접근할 수 있는 것은 MySQL JDBC 드라이버라 하고, Oracle DB에 접근할 수 있는 것은 Oracle JDBC 드라이버라 한다.
이러한 JDBC를 편리하게 사용하는 다양한 기술이 존재한다. 대표적으로 SQL Mapper와 ORM 기술로 나눌 수 있다
- SQL Mapper
- 장점 : JDBC를 편리하게 사용하도록 도와준다.
- SQL 응답 결과를 객체로 편리하게 변환해준다.
- JDBC의 반복 코드를 제거해준다.
- 단점 : 개발자가 SQL을 직접 작성해야한다.
- 대표 기술 : 스프링 JdbcTemplate, MyBatis
- ORM 기술
- ORM은 객체를 관계형 데이터베이스 테이블과 매핑해주는 기술이다. 이 기술 덕분에 개발자는 반복적인 SQL을 직접 작성하지 않고, ORM 기술이 개발자 대신에 SQL을 동적으로 만들어 실행해준다. 추가로 각각의 데이터베이스마다 다른 SQL을 사용하는 문제도 중간에서 해결해준다.
- 대표 기술: JPA, 하이버네이트, 이클립스링크
JDBC DriverManager 연결 이해
- JDBC는 java.sql.Connection 표준 커넥션 인터페이스를 정의한다.
- H2 데이터베이스 드라이버는 JDBC Connection 인터페이스를 구현한 org.h2.jdbc.JdbcConnection 구현체를 제공한다
DriverManager 커넥션 요청 흐름
JDBC가 제공하는DriverManager는 라이브러리에 등록된 DB 드라이버들을 관리하고, 커넥션을 획득하는 기능을 제공한다.
- 애플리케이션 로직에서 커넥션이 필요하면
DriverManager.getConnection()을 호출한다.DriverManger는 라이브러리에 등록될 드라이버 목록을 자동으로 인식한다. 이 드라이버들에게 순서대로 정보를 넘겨서 커넥션을 획득할 수 있는지 확인한다.- 이렇게 찾은 커넥션 구현체가 클라이언트에 반환된다.
이제 본격적으로 JDBC를 사용해서 애플리케이션을 개발해보자.
여기서는 JDBC를 사용해서 회원(Member) 데이터를 데이터베이스에 관리하는 기능을 개발해보자.
drop table member if exists cascade; create table member ( member_id varchar(10), money integer not null default 0, primary key (member_id) ); ```java @Data public class Member { private String memberId; private int money; // 매개변수를 받지 않고 객체를 생성할 때 사용 // 자바에서는 클래스에 생성자를 명시적으로 작성하지 않으면 컴파일러가 자동으로 기본 생성자를 추가해줌. // 하지만 클래스에 다른 생성자가 하나라도 정의되어 있는 경우에는 컴파일러가 기본 생성자를 자동으로 추가해주지 않습니다. public Member(){ } public Member(String memberId, int money) { this.memberId = memberId; this.money = money; } }
- 회원의 ID와 해당 회원이 소지한 금액을 표현하는 단순한 클래스. 앞서 만들어둔
member테이블에 데이터를 저장하고 조회할 때 사용
MemberRepositoryV0 - 회원 등록
@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 = pstmt.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); } } 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); } } } private Connection getConnection() { return DBConnectionUtil.getConnection(); } }
커넥션 획득
- getConnection() : 이전에 만들어둔
DBConnectionUtil을 통해서 데이터베이스 커넥션을 획득한다.save() - SQL 전달
sql: 데이터베이스에 전달할 SQL을 정의한다. 여기서는 데이터를 등록해야 하므로insert sql을 준비했다.con.prepareStatement(sql): 데이터베이스에 전달할 SQL과 파라미터로 전달할 데이터들을 준비한다.리소스 정리
쿼리를 실행하고 나면 리소스를 정리해야 한다. 여기서는Connection,PreparedStatement를 사용했다. 리소스를 정리할 때는 항상 역순으로 해야한다.Connection을 먼저 획득하고Connection을 통해PreparedStatement를 만들었기 때문에 리소스를 반환할 때는 PreparedStatement 를 먼저 종료하고, 그 다음에 Connection 을 종료하면 된다. 참고로 여기서 사용하지 않은 ResultSet 은 결과를 조회할 때 사용한다.
MemberRepositoryV0 - 회원 조회 추가
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 memberId=" + memberId); } } catch (SQLException e) { log.error("db error", e); throw e; } finally { close(con, pstmt, rs); } }
- findById() - 쿼리 실행
- sql : 데이터 조회를 위한 select SQL을 준비한다.
- rs = pstmt.executeQuery() 데이터를 변경할 때는 executeUpdate() 를 사용하지만, 데이터를 조회할 때는 executeQuery() 를 사용한다. executeQuery() 는 결과를 ResultSet 에 담아서 반환한다.
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); } }
MemberRepositoryV0 - 회원 삭제 추가
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); } }