인프런 '스프링 DB 1편' - 김영한님의 강의 내용입니다.
JDBC(Java Database Connectivity) : 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API다.
각 데이터베이스마다 애플리케이션 서버와 연결하고, SQL을 주고받는 방식이 다르기 때문에 DB 마다 적용하기가 매우 번거롭다. 그래서 JDBC라는 표준 인터페이스를 DB마다 JDBC Driver를 통해 동일한 방법으로 접근할 수 있도록 제공해준다.
JDBC를 편리하게 사용하도록 도와준다. 그러나 개발자가 직접 SQL을 작성해야한다.
SQL을 작성해야 한다는 단점이 있지만, 비교적 쉽게 개발할 수 있다.
ex) Spring JDBC Template, MyBatis
객체를 관계형 데이터베이스 테이블과 매핑해주는 기술.
개발자가 직접 SQL을 작성하지 않아도 되지만, 난이도가 높다.
ex) JPA, 하이버네이트
모든 기술의 밑바닥은 JDBC이다.
H2 DB를 활용했다.
package hello.jdbc.connection;
public abstract class ConnectionConst {
public static final String URL = "jdbc:h2:tcp://localhost/~/test";
public static final String USERNAME = "sa";
public static final String PASSWORD = "";
}
getConnection() : DB와 연걸
prepareStatemenst(sql) : DB에 쿼리를 날림
close() : DB와 통신을 TCP/IP 방식으로 통신하기 때문에 사용하지 않을 경우에는 꼭 반납을 해주어야 한다.
pstmt.executeUpdate(); : 쿼리 실행에 영향을 받은 row 수를 리턴한다.
public Member save(Member member) throws SQLException {
String sql = "insert into member(member_id, money) values (?, ?)";
Connection con = null;
PreparedStatement pstmt = null;
try {
// DB 연결
con = DBConnectionUtil.getConnection();
//sql injection 공격을 예방하기 위해 '?'를 통한 파라미터 바인딩을 가능하게 한다.
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 {
// TCP/IP 로 계속해서 통신하기 때문에 꼭 닫아줘야 한다.
close(con, pstmt, null);
}
}
private void close(Connection con, Statement stmt, ResultSet rs) {
// pstmt.close()에서 Exception 발생하면 con.close() 실행이 안되기 때문에 예외 처리를 해줘야 한다.
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
log.error("error", e);
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
log.error("error", e);
}
}
if (con != null) {
try {
con.close();
} catch (SQLException e) {
log.info("error", e);
}
}
}
저장과 비슷한 방법이지만, DB를 Update를 하는 것이 아닌 SQL만 실행하는 것 이므로 pstmt.executeQuery()를 실행함.
executeQuery()를 통해 ResultSet에 쿼리를 실행한 결과가 담긴다.
ResultSet 내부에서 cursor로 데이터를 조회하는데, 최초의 커서는 데이터를 가리키고 있지 않기 때문에 rs.next()를 최초 한번은 호출해야 데이터를 조회할 수 있다.
만약 다음 데이터가 없으면 null을 반환한다.
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);
}
}
업데이트와 삭제는 비슷하니 생략하고 테스트코드만 업로드할 것이다.
findMember.isEqualTo(member)가 True가 되는 이유는 lombok에서 제공해주는 @Data 어노테이션에서 기본적으로 Equals, HashCode를 오버라이딩 해주기 때문이다.
Assertions.assertThatThrownBy()를 통해 delete 된 값을 조회할 경우 NoSuchElementException이 터지는 것으로 테스트할 수 있다.
@Slf4j
class MemberRepositoryV0Test {
MemberRepositoryV0 repository = new MemberRepositoryV0();
@Test
void crud() throws SQLException {
//save
Member member = new Member("memberV5", 10000);
repository.save(member);
//findById
Member findMember = repository.findById(member.getMemberId());
log.info("findMember={}", findMember);
Assertions.assertThat(findMember).isEqualTo(member);
//update : money: 10000 -> 20000
repository.update(member.getMemberId(), 20000);
Member updateMember = repository.findById(member.getMemberId());
Assertions.assertThat(updateMember.getMoney()).isEqualTo(20000);
//delete
repository.delete(member.getMemberId());
Assertions.assertThatThrownBy(() -> repository.findById(member.getMemberId()))
.isInstanceOf(NoSuchElementException.class);
}
}
CRUD하면서 try catch 문에 비슷한 코드들이 계속 중복되는데 앞으로 이것들을 해결하기 위한 기술들을 배우며 공부할 예정이다.