Spring DB : JDBC 개발 + 등록 + 조회 + 수정 + 삭제

    이 글은 인프런 김영한님의 강의를 복습하며 작성한 글입니다.

     

    JDBC를 이용한 CRUD Repository 제작 

    이번 포스팅에서는 JDBC를 이용한 CRUD Repository를 제작하려고 한다. 앞선 포스팅에서 만들었던 "DBConnectionUtill"이라는 클래스를 이용해 Connection을 얻어와서 CRUD를 하는 Repository를 제작하고, 실제 테스트까지 처리를 한다. 

     

    첫번째, DB Table을 만든다. 

    create table member(
    
    member_id varchar(10),
    money integer not null default 0,
    primary key(member_id)
    
    )

    DB에 다음 테이블을 만들어준다. JPA와 다르게 DriverManager는 Table을 자동으로 DDL 해주지 않기 때문이다. 

     

    두번째, Repository 클래스를 만든다.

    @Slf4j
    public class MemberRepositoryV0 {
    
        public Member save(Member member) throws SQLException {
            String sql = "insert into member(member_id, money) values(?,?)";
    
            Connection conn = null;
            PreparedStatement pstmt = null;
    
    
            try {
                conn = getConnection();
                pstmt = conn.prepareStatement(sql);
    
                pstmt.setString(1, member.getMemberId());
                pstmt.setInt(2, member.getMoney());
    
                pstmt.executeUpdate();
                return member;
            } catch (Exception e) {
                log.error("error",e);
                throw e;
            }finally{
                close(conn, pstmt, null);
            }
        }
    }
    • Repository의 Save 메서드를 구현했다. 
      • 파라미터 바인딩을 사용하기 위해 PreparedStatement를 사용했다. 
      • PrepareStatement에 와일드 카드를 사용해주고, 추후 setString + setInt 등을 이용해 파라미터 바인딩을 할 수 있다. 
      • PrepareStatement에는 SQL이 들어가고, 이 SQL을 실행시키면 실제로 DB에 쿼리가 나가게 된다. 
    • 사용한 자원(Connection, PreparedStatement ,ResultSet)은 반드시 회수해야한다. 
      • 예외가 발생하든 / 하지 않든 반드시 해야함. 따라서 Finally 구문에 주의해서 작성해야한다.
      • 이 부분을 처리하지 않을 경우, 커넥션이 끊어지지 않고 계속 유지되는 문제가 발생한다. 즉, 커넥션이 마른다.

     

    세번째, Repository 클래스의 getConnection() / Close() 메서드 만들기

    getConnection() 메서드

    private Connection getConnection() {
        return DBConnectionUtil.getConnection();
    }
    • getConnection()은 앞서 만들었던 DBConnectionUtil.getConnection()을 바로 호출한다.
    • DBConnectionUtil은 DrvierManager에게 설정 정보를 넘겨주고 Connection을 받아온다. 

     

    Close() 메서드

    private void close(Connection connection, PreparedStatement pstmt, ResultSet rs) {
    
        if (rs != null) {
            try {
                rs.close();
            } catch (Exception e) {
                log.info("error",e);
            }
        }
    
        if (pstmt != null) {
            try {
                pstmt.close();
            } catch (Exception e) {
                log.info("error",e);
            }
        }
    
        if (connection != null) {
            try {
                connection.close();
            } catch (Exception e) {
                log.info("error",e);
            }
        }
    }
    • 자원은 반드시 사용한 것의 반대 순서로 Release를 해야한다.
      • 얻을 때는 Connection > PreparedStatement > ResultSet 순으로 얻었다.
      • 해제할 때는 ResultSet > PreparedStatement > Connection 순으로 해제한다. 
    • 사용한 자원은 반드시 해제해야한다. 그런데 각 자원을 해제할 때 마다 Exception이 발생할 수 있기 때문에 Try ~ Catch로 처리한다. 

     

    테스트 코드 작성

    class MemberRepositoryV0Test {
    
        private MemberRepositoryV0 repository = new MemberRepositoryV0();
    
        @Test
        void crud() throws SQLException {
            Member member = new Member("member1", 10000);
            repository.save(member);
        }
    }
    • 다음 테스트 코드를 작성할 수 있다. 
    • 아직까지 정상적으로 들어갔는지 조회를 할 수 없는 상황이기 때문에 JUnit을 이용한 테스트 검사는 할 수 없다. 

     

     

    PreparedStatement는 왜 사용하는가? 

    SQL Injection 공격을 당하지 않기 위해서는 PreparedStatement를 사용한다.  

    // Preparedstatment
    sql = insert into member(member_id, money) values (?, ?)
    
    // Statement
    memberId = "select * from ..."
    sql = insert into member(member_id, money) values ("+ memberId", "+ money")
    • 위와 같은 구문이 있을 경우, Statement를 사용하면 values의 memberId 자리에 앞에 있는 memberId에 저장된 select 쿼리가 직접 들어오게 된다고 한다. 즉, 원하지 않는 SQL Injection이 들어와서 의도치 않게 DB에 있는 모든 정보가 탈취될 수 있다.
    • 반면 PrepareStatment에서 ?, ?는 SQL 구문이 아닌 단순한 파라미터로 인지되기 때문에 SQL Injection이 되지 않는다. 

     

     

    Member 조회 기능 추가

    Member 조회 코드

    public Member findById(String memberId) throws SQLException {
        String sql = "select * from member where member_id = ?";
    
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
    
        try {
            conn = getConnection();
            pstmt = conn.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("error", e);
            throw e;
        }finally {
            close(conn, pstmt, rs);
        }
    }
    • DriverManager로 얻은 Connection으로 다음과 같이 코드를 작성할 수 있다. 
    • 결과는 ResultSet으로 돌려받는다. ResultSet은 DataTable 같은 구조를 가지고 있으며, 내부적으로 Cursor가 가리키는 Column 값을 불러올 수 있다.
    • 초기값은 0이기 때문에 최초로 데이터를 가리키는 곳으로 이동이 필요하다. 따라서 rs.next()를 통해서 다음 커서를 설정한다. 

     

    Member 조회 테스트 코드

    @Test
    void crud() throws SQLException {
        String memberId = "member5";
    
        Member member = new Member(memberId, 10000);
        repository.save(member);
    
        //findById
        Member findMember = repository.findById(member.getMemberId());
        log.info("findMember = {}", findMember);
    
        // 주소비교 + 해쉬비교
        log.info("member == findMember : {}", findMember == member);
        log.info("member equals findMember : {}", member.equals(findMember));
        assertThat(findMember).isEqualTo(member);
    • Member를 저장한 후, Repository에서 Member를 찾아와 같은 객체인지 확인한다.
    • 이 때, Member와 findMember는 Hash 상으로는 같은 객체이지만 주소 상으로는 다른 객체여야 한다. 따라서 equals만 True를 만족한다. 

     

     

    ResultSet 자료구조

    • ResultSet은 "select member_id, money"로 지정을 하면, 'member_id → money' 컬럼 순서대로 결과가 들어간다. 
    • ResultSet은 내부적으로 Cursor를 사용한다. Cursor를 이동해서 다음 테이터를 조회한다.
    • rs.next()
      • 내부의 커서를 움직이는 메서드다
      • boolean 타입을 반환한다.
        • True : 현재 값 존재함.
        • False : 현재 값 존재하지 않음.
      • Cursor는 최초에는 데이터를 가리키지 않는다. 사용하기 위해 한번 커서를 옮겨줘야함. 
    • rs.getString() / rs.getInt() → rs.getString("member_id")
      • 특정 Column으로 조회해서 현재 Cursor가 가리키는 값을 불러온다. 

     

    Member 수정 기능 개발

    Member 수정 코드 

    public void update(String memberId, int money) throws SQLException {
        String sql = "update member set money = ? where member_id = ?";
    
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
    
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
    
            pstmt.setInt(1, money);
            pstmt.setString(2, memberId);
    
            int resultSize = pstmt.executeUpdate();
            log.info("resultSize = {}", resultSize);
    
        } catch (SQLException e) {
            log.error("error", e);
            throw e;
        }finally {
            close(conn, pstmt, rs);
        }
    }
    • memberId를 통해 Member를 찾아, 그 Member의 Money Column을 업데이트 한다. 
    • execute()는 Return 값으로 영향받은 Row의 값을 돌려준다. 

     

    Member 수정 테스트 코드 

    @Test
    void crud() throws SQLException {
        String memberId = "member5";
    
        Member member = new Member(memberId, 10000);
        repository.save(member);
    
        repository.update(memberId, 20000);
        Member updateMember = repository.findById(memberId);
        assertThat(updateMember.getMoney()).isEqualTo(20000);
     }

    Member를 수정하는 테스트 코드를 작성한다. 돈을 10,000 → 20,000으로 변경하는 코드다.

     

    Member 삭제

    Member 삭제 기능 구현

    public void delete(String memberId) throws SQLException {
        String sql = "delete from member where member_id=?";
    
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
    
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
    
            pstmt.setString(1, memberId);
    
            int resultSize = pstmt.executeUpdate();
            log.info("resultSize = {}", resultSize);
    
        } catch (SQLException e) {
            log.error("error", e);
            throw e;
        }finally {
            close(conn, pstmt, rs);
        }
    
    }
    • 특정 MemberId를 가지는 Member를 삭제하는 기능을 구현했다.
    • 이를 위해 Delete 쿼리를 사용했다. 

     

    Member 삭제 테스트 코드 

    @Test
    void crud() throws SQLException {
        String memberId = "member5";
    
    	// delete
        repository.delete(member.getMemberId());
        assertThatThrownBy(() -> repository.findById(memberId)).isInstanceOf(NoSuchElementException.class);
    }
    • delete를 이용해서 Member를 삭제하는 코드를 작성했다.
    • 검증은 Member가 지워지고, 그걸 조회했을 때 NoSuchElementException이 발생하는 것을 이용했다. 

     

     

     

     

    정리

    • JDBC 인터페이스를 구현한 DriverManager를 이용해 Connection을 얻어와 Save 기능을 구현했다. 
    • 자원은 Connection > PreparedStatement > ResultSet으로 얻게 된다. 리소스는 반드시 해제해야하는데, 이 때 얻은 순선의 반대로 해제해야한다.
    • 리소스를 해제 하기 위해서는 Try / Catch 문으로 처리해야한다. 왜냐하면 하나라도 자원 해제를 하던 과정에 실패하게 되면, 나머지 자원이 해제가 되지 않는다.
    • Connection이 해제되지 않는 경우, Connection이 계속 유지된다. 이 경우, DB 세션이 유지되기 때문에 계속 이런 상태가 지속될 경우 DB 커넥션이 마를 수 있다. 

    'Spring > Spring DB' 카테고리의 다른 글

    Spring DB : DataSource 이해  (0) 2022.04.28
    Spring DB : 커넥션 풀  (0) 2022.04.28
    Spring DB : 데이터베이스 연결  (0) 2022.04.28
    Spring DB : JDBC와 ORM의 필요성  (0) 2022.04.28
    스프링 DB : 프로젝트 생성  (0) 2022.04.28

    댓글

    Designed by JB FACTORY