[SpringDB1] JDBC 개발
등록
여기서는 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)
);
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 sqlcon.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구문에 작성해야 한다. 만약 이 부분을 놓치게 되면 커넥션이 끊어지지 않고, 계속 유지되는 문제가 발생할 수 있다. 이런 것을 리소스 누수라고 하고, 결과적으로 커넥션 부족으로 장애가 발생할 수 있다.
[참고]
PreparedStatement는Statement의 자식 타입이며?를 통한 파라미터 바인딩을 가능하게 해준다.
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명 조회인 경우

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의@Data가toString()을 적절히 오버라이딩해서 보여주기 때문이다. isEqualTo():findMemer.equals(member)를 비교- 결과가 참인 이유는 lombok의
@Data는 해당 객체의 모든 필드를 사용하도록equals()를 오버라이딩 하기 때문이다.
- 결과가 참인 이유는 lombok의
delete
회원을 삭제한 다음 findById()를 통해서 조회하는데, 회원이 없기 때문에 NoSuchElementException이 발생한다.
assertThatThrownBy는 해당 예외가 발생해야 검증에 성공한다.
댓글남기기