Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 자바의 정석 기초편 ch13
- 자바의 정석 기초편 ch1
- @Aspect
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch7
- 스프링 mvc2 - 타임리프
- jpa - 객체지향 쿼리 언어
- 스프링 db2 - 데이터 접근 기술
- 스프링 db1 - 스프링과 문제 해결
- 스프링 입문(무료)
- 스프링 mvc2 - 검증
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch5
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch11
- 2024 정보처리기사 수제비 실기
- 게시글 목록 api
- 타임리프 - 기본기능
- 스프링 mvc1 - 서블릿
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch4
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch14
- 2024 정보처리기사 시나공 필기
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch8
- 스프링 mvc1 - 스프링 mvc
Archives
- Today
- Total
나구리의 개발공부기록
JDBC 이해 - JDBC 개발(등록 / 조회 / 수정 / 삭제) 본문
인프런 - 스프링 완전정복 코스 로드맵/스프링 DB 1편 - 데이터 접근 핵심 원리
JDBC 이해 - JDBC 개발(등록 / 조회 / 수정 / 삭제)
소소한나구리 2024. 9. 11. 15:07 출처 : 인프런 - 스프링 DB 1편 데이터 접근 핵심 원리 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard
1. JDBC 개발 - 등록
- H2 데이터 베이스를 실행 해놓고 member 테이블을 미리 만들어두고 시작해야함(이전 강의 참고)
1) Member - domain
package hello.jdbc.domain;
@Data
public class Member {
private String memberId;
private int money;
// 기본 생성자
public Member() {}
// 매개변수가 있는 생성자
public Member(String memberId, int money) {
this.memberId = memberId;
this.money = money;
}
}
2) MemberRepositoryV0
(1) 커넥션 획득
- getConnection() : 이전에 만들어둔 DBConnectionUtil을 통해서 데이터 베이스 커넥션을 획들
(2) save() - SQL 전달
- sql : 데이터 베이스에 전달할 SQL을 작성
- con.prepareStatement(sql) : 데이터베이스에 전달할 SQL과 파라미터로 전달할 데이터들을 준비함
- pstmt.setString(1, member.getMemberId()) : SQL의 첫번째 ?에 값을 지정, 문자이므로 setString을 사용
- pstmt.executeUpdate() : Statement를 통해 준비된 SQL을 커넥션을 통해 실제 데이터베이스에 전달함, 리턴 값으로 int를 반환하는데 영향받은 DB의 row수를 반환함 -> 여기 예제에서는 하나의row를 등록했으므로 1이 반환됨
** 리소스 정리
- 쿼리를 실행하고 나면 리소스를 꼭 정리해야하며, 리소스를 사용한 역순으로 정리해야 함
- 해당 예제에서는 Connection을 먼저 획득하고 PreparedStatement를 만들었기 때문에 반환할 때는 역순으로PreparedStatement를 먼저 종료하고 그 다음에 Connection을 종료,
- 여기선 사용하지 않았지만 ResultSet은 결과를 조회할 때 사용함
- 예외가 발생하든 발생하지 않은 항상 수행되어야 하므로 finally 구문에 주의해서 작성해야 하며 해당 부분을 놓치게 되면 커넥션이 끊어지지 않고 계속 유지되는 문제가 발생하는데 이런 것을 리소스 누수라고함 -> 결과적으로 커넥션 부족으로 장애가 발생할 수 있음
- PreparedStatement -> Statement의 자식타입, ?(물음표)를 통한 파라미터 바인딩을 가능하게 해주고 SQL Injection 공격을 예방하려면 꼭 PreparedStatement를 통한 파라미터 바인딩 방식을 사용해야함
- 자세한 내용은 검색,( SQLInjection - 악의적 쿼리문을 삽입해서 정보를 탈취하거나 오류를 발생)
package hello.jdbc.repository;
// 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;
/*
Statement 를 상속받은 PreparedStatement 인터페이스
Statement : SQL을 그대로 입력
PreparedStatement : 파라미터를 바인해서 SQL을 입력, 기능이 더 많음
*/
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 { // 꼭 finally 연결을 닫아줘야함 -> 안그럼 연결이 계속 유지됨
close(con, pstmt, null);
}
}
/**
* 연결을 닫아주는 메서드를 별도로 추출
* 연결을 닫아줄때 예외가 터질 수도 있어서 한번더 try - catch 로 잡아줘야함
* 보통 이런 형태로 작성함
*/
private void close(Connection con, Statement stmt, ResultSet rs) {
if (con != null) {
try {
con.close();
} catch (SQLException e) {
log.info("error", e);
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
log.info("error", e);
}
}
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
log.info("error", e);
}
}
}
// 커넥션 연결 메서드 추출
private Connection getConnection() {
return DBConnectionUtil.getConnection();
}
}
(3) MemberRepositoryV0Test
- 해당 테스트를 진행 후 H2 데이터베이스에 조회 쿼리문을 날려보면 값이 저장됨
- 이 테스트는 두번 실행하면 PK 중복 오류가 발생하니 delete from member;로 데이터를 날리고 다시 테스트 하거나, "memberV0"의 값을 변경하면서 테스트 하면 됨
package hello.jdbc.repository;
class MemberRepositoryV0Test {
MemberRepositoryV0 repository = new MemberRepositoryV0();
@Test
void save() throws SQLException {
Member member = new Member("memberV0", 10000);
repository.save(member);
}
}
2. JDBC 개발 - 조회
1) MemberRepositoryV0 - findById() 메서드 추가
(1) findById()
- rs = pstmt.executeQuery() : 데이터 변경 시에는 exeCuteUpdate()를 사용하지만 데이터를 조회할 때는 executeQuery()를 사용, 쿼리 결과를 ResultSet에 담아서 반환함
(2) ResultSet
- ResultSet의 구조는 아래 그림과 같은 구조인데 보통 Select 쿼리의 결과가 순서대로 들어가게 됨
- 내부에 있는 커서(cursor)를 이동해서 다음 데이터를 조회하는데 rs.next()를 호출하면 커서가 다음으로 이동하고, 최초의 커서는 데이터를 가리키고 있지 않기 때문에 최초 한번은 호출해야 데이터를 조회할 수 있음
- rs.next()의 결과가 true 면 데이터가 있다는 뜻이고 false면 더이상 커서가 가리키는 데이터가 없다는 뜻
- 이렇게 조회된 데이터를 가지고 member.setMemberId(), member.setMonet()로 조회후 member를 반환
- finById()는 회원 하나를 조회하는 것이 목적이므로 조회 결과가 항상 1건이므로 if을 사용했지만 다수의 건일경우 while문을 사용
/// ...
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()) { // 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);
}
}
/// ...
2) MemberRepositoryV0Test - 멤버 조회 테스트 추가
- 기존 save() 메서드를 curd()로 이름을 변경하고 조회 테스트 코드를 작성
- 롬복의 @Data가 toString()을 오버라이딩해서 보여주기 때문에 로그의 값이 참조값이 아닌 실제 값이 보여지게 됨
- isEqualTo(): findMember.equals(member)를 비교 -> 롤복의 @Data가 해당 객체의 모든 필드를 사용하도록 equals()를 오버라이딩 하기 때문
- 이 테스트도 마찬가지로 동일한 값으로 중복 테스트하면 PK 중복 오류가 발생하니, 값을 삭제하거나 memberId값을 바꿔서 테스트를 진행
package hello.jdbc.repository;
@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());
// @Data 롬복이 toString()을 Override 해서 로그의 결과가 참조값이 아닌 실제 값이 출력됨
log.info("findMember={}", findMember);
// == 비교를 하면 false(참조값 비교), equals()는 true(값 비교)
// isEqualTo()가 true인 이유는 @Data가 객체의 모든 필드 값을 사용하도록 equals()를 Override 함
Assertions.assertThat(findMember).isEqualTo(member);
}
}
3. JDBC 개발 - 수정, 삭제
1) MemberRepositoryV0 - update(), delete() 메서드 추가
- 데이터를 변경하는 쿼리 : executeUpdate() 이용
- 쿼리 결과를 int로 반환(반영된 row의 수)
// ...
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 { // 꼭 finally 연결을 닫아줘야함 -> 안그럼 연결이 계속 유지됨
close(con, pstmt, null);
}
}
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); // 해당 인덱스에 값 저장
int resultSize = pstmt.executeUpdate(); // 실행
log.info("resultSize={}", resultSize);
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally { // 꼭 finally 연결을 닫아줘야함 -> 안그럼 연결이 계속 유지됨
close(con, pstmt, null);
}
}
//...
2) MemberRepositoryV0Test - 멤버 수정, 삭제 테스트 추가
- save시 회원 데이터의 money에 값을 지정해놓고, update()에서 값을 변경해서 테스트진행 후 값이 변경되어있는지 검증
- delete()는 해당 값이 삭제된 후 조회를 하게 되면 NoSuchElementException 예외가 터지는 것을 활용하여 assertThatThrownBy로 해당 예외가 발생했는지 확인해서 검증을 하면 됨
package hello.jdbc.repository;
@Slf4j
class MemberRepositoryV0Test {
MemberRepositoryV0 repository = new MemberRepositoryV0();
@Test
void crud() throws SQLException {
// save
Member member = new Member("member80", 10000);
// ...
// update
repository.update(member.getMemberId(), 50000);
Member updatedMember = repository.findById(member.getMemberId());
assertThat(updatedMember.getMoney()).isEqualTo(50000);
// delete
repository.delete(updatedMember.getMemberId());
// NoSuchElementException 이터지면 검증 OK
assertThatThrownBy(() -> repository.findById(member.getMemberId()))
.isInstanceOf(NoSuchElementException.class);
}
}
** 참고
- sava() ~ delete()까지 테스트 코드를 작성하면 이제 db에 값을 지우지 않아도 계속 반복적으로 테스트가 성공하지만 좋은 방법은 아님
- 예를들어 중간에 알수 없는 이유로 update()까지만 동작하고 예외가 발생해서 멈추게 되면, delete()는 실행되지 않은채 테스트가 종료 되는데 db에 테스트 값이 들어간채로 종료되기 때문에 다시 테스트를 시작할 때 에러가 발생되고, 그러면 DB에 가서 직접 데이터를 지워야 하는 일이 발생함
- 이러한 문제는 트랜잭션을 활용하면 해결할 수 있음