6 분 소요

1. 회원(Member) 등록

JDBC를 사용해서 회원( Member ) 데이터를 데이터베이스에 관리하는 기능을 개발해보자.

주의 🚨
H2 데이터베이스 설정 마지막에 있는 테이블과 샘플 데이터 만들기를 통해서 member 테이블을 미리 만들어두어야 한다.

등록

Member

src/main/java/hello/jdbc/domain/Member.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 - 회원 등록

src/main/java/hello/jdbc/repository/MemberRepositoryV0.java
JDBC를 사용해서 이렇게 만든 회원 객체를 데이터베이스에 저장해보자.

/**
 * 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 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.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과 파라미터로 전달할 데이터들을 준비한다.
    sql : insert into member(member_id, money) values(?, ?)"
    pstmt.setString(1, member.getMemberId()) : SQL의 첫번째 ? 에 값을 지정한다. 문자이므로 setString 을 사용한다.
    pstmt.setInt(2, member.getMoney()) : SQL의 두번째 ? 에 값을 지정한다. Int 형 숫자이므로 setInt 를 지정한다.
  • pstmt.executeUpdate() : Statement 를 통해 준비된 SQL을 커넥션을 통해 실제 데이터베이스에 전달한다.
    참고로 executeUpdate()int 를 반환하는데 영향받은 DB row 수를 반환한다. 여기서는 하나의 row를 등록했으므로 1을 반환한다.

executeUpdate()

int executeUpdate() throws SQLException;

리소스 정리

쿼리를 실행하고 나면 리소스를 정리해야 한다.
여기서는 Connection , PreparedStatement 를 사용했다. 리소스를 정리할 때는 항상 역순으로 해야한다.
Connection 을 먼저 획득하고 Connection 을 통해 PreparedStatement 를 만들었기 때문에 리소스를 반환할 때는 PreparedStatement 를 먼저 종료하고, 그 다음에 Connection 을 종료하면 된다.
참고로 여기서 사용하지 않은 ResultSet 은 결과를 조회할 때 사용한다. 조금 뒤에 조회 부분에서 알아보자.

주의 🚨
리소스 정리는 꼭! 해주어야 한다. 따라서 예외가 발생하든, 하지 않든 항상 수행되어야 하므로 finally 구문에 주의해서 작성해야한다.

테스트

MemberRepositoryV0Test - 회원 등록 테스트

src/test/java/hello/jdbc/repository/MemberRepositoryV0Test.java

class MemberRepositoryV0Test {

    MemberRepositoryV0 repository = new MemberRepositoryV0();

    @Test
    void crud() throws SQLException {

        Member member = new Member("memberV0", 10000);
        repository.save(member);
    }
}

실행 결과

데이터베이스에서 select * from member 쿼리를 실행하면 데이터가 저장된 것을 확인할 수 있다.
스크린샷 2022-07-07 오후 4 15 18

참고
이 테스트는 2번 실행하면 PK 중복 오류가 발생한다. 이 경우 delete from member 쿼리로 데이터를 삭제한 다음에 다시 실행하자.

2. 회원(Member) 조회

이번에는 JDBC를 통해 이전에 저장한 데이터를 조회하는 기능을 개발해보자.

조회

MemberRepositoryV0 - 회원 조회 추가

src/main/java/hello/jdbc/repository/MemberRepositoryV0.java

@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.info("error", e);
            throw e;

        } finally {
            close(con, pstmt, rs);
        }
    }
    ...
}

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 타입으로 반환한다.

findById() 에서는 회원 하나를 조회하는 것이 목적이다. 따라서 조회 결과가 항상 1건이므로 while 대신에 if 를 사용한다.
다음 SQL을 보면 PK인 member_id 를 항상 지정하는 것을 확인할 수 있다.
SQL: select * from member where member_id = ?

테스트

MemberRepositoryV0Test - 회원 조회 테스트 추가

src/test/java/hello/jdbc/repository/MemberRepositoryV0Test.java

@Slf4j
class MemberRepositoryV0Test {

    MemberRepositoryV0 repository = new MemberRepositoryV0();

    @Test
    void crud() throws SQLException {

         // save
        Member member = new Member("memberV2", 10000);
        repository.save(member);

        // findById
        Member findMember = repository.findById(member.getMemberId());
        log.info("findMember={}", findMember);
        assertThat(findMember).isEqualTo(member);
    }
}

실행 결과

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

3. 회원(Member) 수정

수정과 삭제는 등록과 비슷하다. 등록, 수정, 삭제처럼 데이터를 변경하는 쿼리는 executeUpdate() 를 사용하면 된다.

수정

MemberRepositoryV0 - 회원 수정 추가

src/main/java/hello/jdbc/repository/MemberRepositoryV0.java

@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(); // 쿼리를 실행하고 영향받은 row 수 반환
            log.info("resultSize={}", resultSize);

        } catch (SQLException e){
            log.error("db error", e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }
    ...
}

executeUpdate() 는 쿼리를 실행하고 영향받은 row수를 반환한다. 여기서는 하나의 데이터만 변경하기 때문에 결과로 1이 반환된다.
만약 회원이 100명이고, 모든 회원의 데이터를 한번에 수정하는 update sql 을 실행하면 결과는 100이 된다.

테스트

MemberRepositoryV0Test - 회원 수정 테스트 추가

src/test/java/hello/jdbc/repository/MemberRepositoryV0Test.java

@Slf4j
class MemberRepositoryV0Test {

    MemberRepositoryV0 repository = new MemberRepositoryV0();

    @Test
    void crud() throws SQLException {

         // save
        Member member = new Member("memberV2", 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);
    }
}

회원 데이터의 money 를 10000 20000으로 수정하고, DB에서 데이터를 다시 조회해서 20000으로 변경 되었는지 검증한다.

실행 로그

MemberRepositoryV0 - resultSize=1

pstmt.executeUpdate() 의 결과가 1인 것을 확인할 수 있다. 이것은 해당 SQL에 영향을 받은 로우 수가 1개라는 뜻이다.

4. 회원(Member) 삭제

삭제

MemberRepositoryV0 - 회원 삭제 추가

src/main/java/hello/jdbc/repository/MemberRepositoryV0.java

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

수정에서와 쿼리만 변경되고 내용은 거의 같다.

테스트

MemberRepositoryV0Test - 회원 삭제 테스트 추가

src/test/java/hello/jdbc/repository/MemberRepositoryV0Test.java

@Slf4j
class MemberRepositoryV0Test {

    MemberRepositoryV0 repository = new MemberRepositoryV0();

    @Test
    void crud() throws SQLException {

         // save
        Member member = new Member("memberV2", 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); // NoSuchElementException 예외가 발생해야 검증 성공
    }
}

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



💛 개인 공부 기록용 블로그입니다. 👻

맨 위로 이동하기