-
[김영한 스프링] 05. JDBC 이해 - JDBC 개발 조회, 수정, 삭제Spring/스프링 DB 1편 - 데이터 접근 핵심 원리 2023. 10. 18. 00:27
JDBC 개발 - 조회
MemberRepositoryV0 - 회원 조회 추가
package hello.jdbc.repository; import hello.jdbc.connection.DBConnectionUtil; import hello.jdbc.domain.Member; import lombok.extern.slf4j.Slf4j; import java.sql.*; import java.util.NoSuchElementException; /** * JDBC - DriverManager 사용 */ @Slf4j public class 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; } } 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.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.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 Connection getConnection() { return DBConnectionUtil.getConnection(); } }
findById() - 쿼리 실행
- sql : 데이터 조회를 위한 select SQL을 준비한다.
- rs = pstmt.executeQuery() 데이터를 변경할 때는 executeUpdate()를 사용하지만, 데이터를 조회할 때는 executeQuery()를 사용한다. executeQuery()는 결과를 ResultSet에 담아서 반환한다.
executeQuery()
ResultSet executeQuery() throws SQLException;
ResultSet
- ResultSet은 다음과 같이 생긴 데이터 구조이다. 보통 select 쿼리의 결과가 순서대로 들어간다.
- 예를 들어서 select member_id, money라고 지정하면 member_id, money라는 이름으로 데이터가 저장된다.
- 참고로 select *을 사용하면 테이블의 모든 컬럼을 다 지정한다.
- ResultSet 내부에 있는 커서(cursor)를 이동해서 다음 데이터를 조회할 수 있다.
- rs.next() : 이것을 호출하면 커서가 다음으로 이동한다. 참고로 최초의 커서는 데이터를 가리키고 있지 않기 때문에 rs.next()를 최초 한번은 호출해야 데이터를 조회할 수 있다.
- rs.next()의 결과가 true면 커서의 이동 결과 데이터가 있다는 뜻이다.
- rs.next()의 결과가 false면 더이상 커서가 가리키는 데이터가 없다는 뜻이다.
- rs.getString("member_id") : 현재 커서가 가리키고 있는 위치의 member_id 데이터를 String 타입으로 반환한다.
- rs.getInt("money") : 현재 커서가 가리키고 있는 위치의 money 데이터를 int 타입으로 반환한다.
ResultSet 결과 예시
참고로 이 ResultSet의 결과 예시는 회원이 2명 조회되는 경우이다.
- 1-1에서 rs.next()를 호출한다.
- 1-2의 결과로 cursor가 다음으로 이동한다. 이 경우 cursor가 가리키는 데이터가 있으므로 true를 반환한다.
- 2-1에서 rs.next()를 호출한다.
- 2-2의 결과로 cursor가 다음으로 이동한다. 이 경우 cursor가 가리키는 데이터가 있으므로 true를 반환한다.
- 3-1에서 rs.next()를 호출한다.
- 3-2의 결과로 cursor가 다음으로 이동한다. 이 경우 cursor가 가리키는 데이터가 없으므로 false를 반환한다.
findById()에서는 회원 하나를 조회하는 것이 목적이다. 따라서 조회 결과가 항상 1건이므로 while 대신에 if를 사용한다. 다음 SQL을 보면 PK인 member_id를 항상 지정하는 것을 확인할 수 있다.
SQL: select * from member where member_id = ?
MemberRepositoryV0Test - 회원 조회 추가
package hello.jdbc.repository; import hello.jdbc.domain.Member; import lombok.extern.slf4j.Slf4j; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import java.sql.SQLException; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*; @Slf4j class MemberRepositoryV0Test { MemberRepositoryV0 repository = new MemberRepositoryV0(); @Test void crud() throws SQLException { // save Member member = new Member("memberV0", 10000); repository.save(member); // findById Member findMember = repository.findById(member.getMemberId()); log.info("findMember={}", findMember); assertThat(findMember).isEqualTo(member); } }
- 참고로 실행 결과에 member 객체의 참조 값이 아니라 실제 데이터가 보이는 이유는 롬복의 @Data가 toString()을 적절히 오버라이딩 해서 보여주기 때문이다.
- isEqualTo() : findMember.equals(member)를 비교한다. 결과가 참인 이유는 롬복의 @Data는 해당 객체의 모든 필드를 사용하도록 equals()를 오버라이딩 하기 때문이다.
참고
이 테스트는 2번 실행하면 PK 중복 오류가 발생한다. 이 경우 delete from member 쿼리로 데이터를 삭제한 다음에 다시 실행하자.JDBC 개발 - 수정, 삭제
MemberRepositoryV0 - 회원 수정 추가
package hello.jdbc.repository; import hello.jdbc.connection.DBConnectionUtil; import hello.jdbc.domain.Member; import lombok.extern.slf4j.Slf4j; import java.sql.*; import java.util.NoSuchElementException; /** * JDBC - DriverManager 사용 */ @Slf4j public class 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 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; } } 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.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.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 Connection getConnection() { return DBConnectionUtil.getConnection(); } }
executeUpdate()는 쿼리를 실행하고 영향받은 row 수를 반환한다. 여기서는 하나의 데이터만 변경하기 때문에 결과로 1이 반환된다. 만약 회원이 100명이고, 모든 회원의 데이터를 한번에 수정하는 update sql을 실행하면 결과는 100이 된다.
MemberRepositoryV0Test - 회원 수정 추가
package hello.jdbc.repository; import hello.jdbc.domain.Member; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import java.sql.SQLException; import static org.assertj.core.api.Assertions.*; @Slf4j class MemberRepositoryV0Test { MemberRepositoryV0 repository = new MemberRepositoryV0(); @Test void crud() throws SQLException { // save Member member = new Member("memberV0", 10000); repository.save(member); // findById Member findMember = repository.findById(member.getMemberId()); log.info("findMember={}", findMember); assertThat(findMember).isEqualTo(member); // update : money 10000 -> 20000 repository.update(member.getMemberId(), 20000); Member updatedMember = repository.findById(member.getMemberId()); assertThat(updatedMember.getMoney()).isEqualTo(20000); } }
회원 데이터의 money를 10000 -> 20000으로 수정하고, DB에서 데이터를 다시 조회해서 20000으로 변경되었는지 검증한다.
로그
MemberRepositoryV0 - resultSize=1
pstmt.executeUpdate()의 결과가 1인 것을 확인할 수 있다. 이것은 해당 SQL에 영향을 받은 로우 수가 1개라는 뜻이다.
결과
MemberRepositoryV0 - 회원 삭제 추가
package hello.jdbc.repository; import hello.jdbc.connection.DBConnectionUtil; import hello.jdbc.domain.Member; import lombok.extern.slf4j.Slf4j; import java.sql.*; import java.util.NoSuchElementException; /** * JDBC - DriverManager 사용 */ @Slf4j public class 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); } } 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 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; } } 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.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.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 Connection getConnection() { return DBConnectionUtil.getConnection(); } }
MemberRepositoryV0Test - 회원 삭제 추가
package hello.jdbc.repository; import hello.jdbc.domain.Member; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import java.sql.SQLException; import static org.assertj.core.api.Assertions.*; @Slf4j class MemberRepositoryV0Test { MemberRepositoryV0 repository = new MemberRepositoryV0(); @Test void crud() throws SQLException { // save Member member = new Member("memberV0", 10000); repository.save(member); // findById Member findMember = repository.findById(member.getMemberId()); log.info("findMember={}", findMember); assertThat(findMember).isEqualTo(member); // 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()); } }
로그
회원을 삭제한 다음 findById()를 통해서 조회한다. 회원이 없기 때문에 NoSuchElementException이 발생한다. assertThatThrownBy는 해당 예외가 발생해야 검증에 성공한다.
참고
마지막에 회원을 삭제하기 때문에 테스트가 정상 수행되면, 이제부터는 같은 테스트를 반복해서 실행할 수 있다. 물론 테스트 중간에 오류가 발생해서 삭제 로직을 수행할 수 없다면 테스트를 반복해서 실행할 수 없다.
트랜잭션을 활용하면 이 문제를 깔끔하게 해결할 수 있는데, 자세한 내용은 뒤에서 설명한다.출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1
'Spring > 스프링 DB 1편 - 데이터 접근 핵심 원리' 카테고리의 다른 글
[김영한 스프링] 07. 커넥션풀과 데이터소스 이해 - DataSource 예제 (0) 2023.10.18 [김영한 스프링] 06. 커넥션풀과 데이터소스 이해 - 커넥션 풀, DataSource 이해 (0) 2023.10.18 [김영한 스프링] 04. JDBC 이해 - JDBC 개발 등록 (1) 2023.10.17 [김영한 스프링] 03. JDBC 이해 - 데이터베이스 연결 (1) 2023.10.17 [김영한 스프링] 02. JDBC 이해 - JDBC 이해 & 최신 데이터 접근 기술 (1) 2023.10.17