[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 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
구문에 작성해야 한다. 만약 이 부분을 놓치게 되면 커넥션이 끊어지지 않고, 계속 유지되는 문제가 발생할 수 있다. 이런 것을 리소스 누수라고 하고, 결과적으로 커넥션 부족으로 장애가 발생할 수 있다.
[참고]
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
는 해당 예외가 발생해야 검증에 성공한다.
댓글남기기