4 분 소요


등록

여기서는 JDBC를 사용해서 Member 데이터를 데이터베이스에 관리하는 기능을 개발했다.

H2 데이터베이스에 member 테이블을 미리 만들어두었다.

schema.sql

drop table member if exists cascade;
create table member (
    member_id varchar(10),
    money integer not null default 0,
    primary key (member_id)
);

DB연결은 전 포스팅을 참고

Member

@Data
public class Member {

    private String memberId;
    private int money;

    public Member() {

    }

    public Member(String memberId, int money) {
        this.memberId = memberId;
        this.money = money;
    }
}

회원 등록 구현

@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 = 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) {
                lon.info("error", e);
            }
        }

        if(stmt != null) {
            try {
                stmt.close();
            } catch (SQLException e) {
                lon.info("error", e);
            }
        }

        if(con != null) {
            try {
                con.close();
            } catch (SQLException e) {
                lon.info("error", e);
            }
        }
    }

    private Connection getConnection() {
        return DBConnectionUtil.getConnection();
    }
}

커넥션 획득

  • getConnection(): 이전에 만들어둔 DBConnectionUtil을 통해서 데이터베이스 커넥션 획득.

save() - SQL 전달

  • sql: 데이터베이스에 전달할 SQL을 정의한다. 여기서는 데이터를 등록해야 하므로 insert sql
  • con.prepareStatement(sql): 데이터베이스에 전달할 SQL과 파라미터로 전달할 데이터들을 준비
    • sql: insert into member(member_id, money) values(?, ?)
    • pstmt.setString(1, member.getMemberId()) : SQL의 첫번째 ?에 값을 지정
  • pstmt.executeUpdate(): Statement를 통해 준비된 SQL을 커넥션을 통해 실제 데이터베이스에 전달.
    • executeUpdate()는 영향받은 DB row 수를 int타입으로 반환한다.

리소스 정리
쿼리를 실행하고 나면 리소스를 정리해야 하는데, 정리는 항상 역순으로 해야한다. Connection - PreparedStatement순으로 만들었기 때문에, 리소스를 반환할 때는 PreparedStatement를 먼저 종료하고, 그 다음에 Connection을 종료하면 된다.

[주의!]
리소스 정리는 꼭 해주어야 하기 때문에, 예외가 발생하든 하지 않든 항상 수행되어야 하므로 finally구문에 작성해야 한다. 만약 이 부분을 놓치게 되면 커넥션이 끊어지지 않고, 계속 유지되는 문제가 발생할 수 있다. 이런 것을 리소스 누수라고 하고, 결과적으로 커넥션 부족으로 장애가 발생할 수 있다.

[참고]
PreparedStatementStatement의 자식 타입이며 ?를 통한 파라미터 바인딩을 가능하게 해준다.
SQL Injection 공격을 예방하려면 PreparedStatement를 통한 파라미터 바인딩 방식을 사용해야 한다.


조회

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() - 쿼리 실행

  • rs = pstmt.executeQuery(): 데이터를 변경할 때는 executeUpdate()를 사용하지만, 데이터를 조회할 때는 executeQuery()를 사용한다.
  • executeQuery()는 결과를 ResultSet에 담아서 반환한다.

ResultSet

  • ResultSet 내부에 있는 cursor를 이동해서 다음 데이터를 조회할 수 있다.
  • rs.next(): 호출하면 커서가 다음으로 이동한다. 참고로 최초의 커서는 데이터를 가리키고 있지 않기 때문에, rs.next()를 최초 한번은 호출해야 데이터를 조회할 수 있다.
    • rs.next()의 결과가 true면 커서의 이동 결과 데이터가 있다는 뜻이다.
    • rs.next()의 결과가 false면 더 이상 커서가 가리키는 데이터가 없다는 뜻이다.
  • rs.getString("member_id"): 현재 커서가 가리키고 있는 위치의 member_id 데이터를 String 타입으로 반환한다.

결과 예시

ResultSet의 결과 예시가 회원 2명 조회인 경우
12

findById()에서는 회원 하나를 조회하는 것이 목적이기 때문에 조회 결과가 항상 1건이므로 while 대신에 if를 사용했다.


수정

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);
    }
}

executeUpdate()는 쿼리를 실행하고 영향받은 row수를 반환하고, 여기서는 하나의 데이터만 변경하기 때문에 결과로 1이 반환된다.


삭제

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);
    }
}

수정 쿼리와 비교해보면 쿼리만 변경되고 내용은 거의 같다.

테스트 코드

MemberRepositoryV0(+)

class MemberRepositoryVoTest {

    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 updateMember = repository.findById(member.getMemberId());
        assertThat(updateMember.getMoney()).isEqualTo(20000);

        // delete
        repository.delete(member.getMemberId());
        assertThatThrownBy(() -> repository.findById(member.getMemberId()))
                .isInstanceOf(NoSuchElementException.class);
    }
}

findById 실행 결과

MemberRepositoryV0Test - findMember=Member(memberId=memberV0, money=10000)
  • 실행 결과에 member객체의 참조 값이 아닌 실제 데이터가 보이는 이유는 lombok의 @DatatoString()을 적절히 오버라이딩해서 보여주기 때문이다.
  • isEqualTo(): findMemer.equals(member)를 비교
    • 결과가 참인 이유는 lombok의 @Data는 해당 객체의 모든 필드를 사용하도록 equals()를 오버라이딩 하기 때문이다.

delete
회원을 삭제한 다음 findById()를 통해서 조회하는데, 회원이 없기 때문에 NoSuchElementException이 발생한다.
assertThatThrownBy는 해당 예외가 발생해야 검증에 성공한다.


<출처 : 인프런 - 스프링 DB 1편 : 데이터 접근 핵심 원리(김영한)>

댓글남기기