728x90
- spring boot db dependency
- JDBC API
- H2 Database
- Lombok
- test code에서 lombok 사용 시, dependency 추가
- test code에서 @Slfj4 사용 하기 위함
testComileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
1. H2 db 설정
- 개발, 테스트 용도로 사용하는 가벼운 db
- sql 작성 화면 제공
- 설치
- 다운로드 및 설치 : https://www.h2database.com
- h2 다운로드 버전확인: https://www.h2database.com/html/download-archive.html
- h2 데이터베이스 버전은 스프링 부트 버전에 맞춘다. : External Libraries -> h2database:version
- 실행
- terminal에서 다운로드 받은 h2 경로 이동
- 권한 주기: chmod 755 h2.sh
- 실행: ./h2.sh
- web browser 자동 실행 후 h2 db 정상 작동 방법
- web browser h2 접속 방법
- jdbc:h2:~/test (최초 한번)
- 연결 시험을 호출하면 오류가 발생
- 연결을 직접눌러주어야 한다.
- ~/test.mv.db 파일 생성 확인
~ 경로에서 $ ls -arlth 수행 하면 확인 가능
- 최초 생성 이후
- jdbc:h2:tcp://localhost/~/test 으로 접속
- jdbc:h2:~/test (최초 한번)
2. JDBC 이해
- app 개발시 중요 데이터는 대부분 db에 보관
2.1. 기본 작동 원리
- app 과 db 일반적 사용 방법
- 커넥션 연결 : tcp/ip를 사용해서 connection 연결
- SQL 전달 : db가 이해할 수 있는 'SQL문'을 연결된 connection을 통해 db에 전달
- 결과 응답 : db에 정상적으로 'SQL문'이 수행 된 후, 결과를 app에게 응답 해준다.
2.2. server와 db 변경
- sql회사 마다 connection, sql 전달, 결과 응답 방법이 다 다르다.
- 관계형 db만 수십개 존재
- 문제점
- 다른 종류의 db 변경시 코드도 함께 변경이 된다.
- 각각의 db마다 connection 연결, sql 전달, result 응답 받는 방법을 새로 학습 해야함
- 해결방안
- jdbc 자바 표준이 등장
3. JDBC 표준 인터페이스
- 자바에서 db에 접속할 수 있도록 하는 자바 api
- jdbc는 db에 data를 qoery하거나 update하는 방법을 제공
- JDBC 표준 인터페이스
- 하나의 인터페이스가 아니라 Connection, Statement, ResultSet이란 각각의 interface를 가지고 있는 것을 JDBC 표준 인터페이스라고 한다.
- db 회상의 driver 또한 위 3가지 interface를 구현한 구현체들의 묶음을 말하는 것
- 하나의 인터페이스가 아니라 Connection, Statement, ResultSet이란 각각의 interface를 가지고 있는 것을 JDBC 표준 인터페이스라고 한다.
- 사용
- 이제 db가 무엇이지에 관계 없이 jdbc 표준 인터페이스의 3가지의 method만 사용하면 된다.
- 각각의 db 회사들은 jdbc 표준 인터페이스를 상속받아 구현한 구현체를 제공한다.
- 구현체 == 드라이버
- 각각의 db 회사들은 jdbc 표준 인터페이스를 상속받아 구현한 구현체를 제공한다.
이 구현체들을 jdbc 드라이버라고 한다.
- 각각의 db 회사들은 jdbc 표준 인터페이스를 상속받아 구현한 구현체를 제공한다.
- 문제점 해결
- db를 변경할 때 JDBC 구현 library만 변경하면 된다.
-> db를 변경해도 app server의 사용 코드를 그대로 유지 - JDBC 표준 인터페이스 사용법만 학습하면 된다.
- db를 변경할 때 JDBC 구현 library만 변경하면 된다.
- jdbc 표준화의 한계
-> jdbc의 한계라기 보단 jdbc로 connect, preparedStatement, resultset을 공통화 했지만 sql문 자체가 통일이 안되는 한계가 있음을 의미
- 각각의 db마다 sql, data type의 일부 사용법이 다르기 때문에 ansi sql이라는 표준이 있기는 하지만 일반적인 부분만 공통화 했기 때문에 한계가 존재한다.
- 대표적 예로 실무에서 사용하는 페이징 sql은 각각의 db마다 사용법이 다르다
- 결론
- jdbc 코드는 변경하지 않아도 되지만 sql문은 해당 db에 맞도록 변경해야한다.
- jpa 사용시 db마다 다른 sql의 정의 문제를 일부 해결 가능하다.
4. jdbc와 최신 데이터 접근 기술
4.1. sql Mapper
- sql Mapper
- 장점
- jdbc를 편리하게 사용하도록 해준다.
= jdbc의 connect, statement, resultset + close 의 무한 반복 단계를 편리하게 해줌 - sql 응답 결과를 객체로 편리하게 변환
- jdbc의 반복 코드를 제거
- jdbc를 편리하게 사용하도록 해준다.
- 단점
- 개발자가 sql을 직접 작성해야한다.
- 대표 기술
- jdbcTemplate
- MyBatis
- 장점
4.2. ORM 기술
- ORM
- 관계형 db table과 mapping 해주는 기술
- 반복적인 SQL을 직접 작성하지 않고, ORM 기술이 개발자 대신에 sql을 동적으로 만들어 실행
- 대표기술
- JPA interface
- 하이버네이트 구현체
- 이클립스링크 구현체
- JPA interface
4.3. 정리
- sql mapper 이든 ORM 이든 내부에서는 모두 jdbc를 사용한다.
- jdbc가 어떻게 동작하는지 기본 원리 정도는 알아야 한다.
5. 데이터베이스 연결
5.1. Connection 방법
- h2 db 서버를 먼저 실행
- db connect를 위한 정보 abstract class - 객체로 생성하지 못하도록 하기 위함
package hello.jdbc.connection;
// 상수를 사용하기 위한 class로 객체 생성을 막기 위해 abstract class로 사용
public abstract class ConnectionConst {
public static final String URL = "jdbc:h2:tcp://localhost/~/test"; // h2 db 접근 규약
public static final String USERNAME = "sa";
public static final String PASSWORD = "";
}
- JDBC를 사용해서 실제 db에 연결하는 코드 작성
package hello.jdbc.connection;
import static hello.jdbc.connection.ConnectionConst.*;
@Slf4j
public class DBConnectionUtil {
// jdbc가 제공하는 Connection class
public static Connection getConnection(){
try{
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD); // h2 Connection 구현체 가지고 온다. // org.h2.jdbc.JdbcConnection
log.info("get connection={}, class={}", connection, connection.getClass());
return connection;
}catch (SQLException e){
throw new IllegalStateException(e); // checked exception을 runtime으로 가지고 온다 정도로 이해
}
}
}
- db 연결: JDBC에서 제공하는 DriverManger.getConnection(..) 사용
- org.h2.jdbc.JdbcConnection
- H2 db driver가 제공하는 h2 전용 Connection
- 해당 Connection은 JDBC 표준 connection인 java.sql.Connection interface를 구현한 구현체
5.2. JDBC Drivermanager 연결 이해
- 어떻게 jdbc가 app server의 h2 driver를 찾은 건지 대한 내용
- JDBC가 제공하는 DriverManager : 라이브러리에 등록된 DB 드라이버들을 관리하고, 커넥션을 획득하는 기능을 제공
- 해당 db driver 찾는 방법
- 애플리케이션 로직에서 커넥션이 필요하면 DriverManager.getConnection() 을 호출한다.
- DriverManager 는 라이브러리에 등록된 드라이버 목록을 자동으로 인식
- 이 드라이버들에게 순서대로 다음 정보를 넘겨서 커넥션을 획득할 수 있는지 확인
- 정보 : url, username, password
- 이렇게 찾은 커넥션 구현체가 클라이언트에 반환
- H2 커넥션은 JDBC가 제공하는 java.sql.Connection 인터페이스를 구현
6. JDBC 개발
- 요약
- 앞서 만든 db 드라이버 connection method를 이용해서 connection 연결
- 해당 connection을 통해서 메서드를 이용해서 사용할 sql 쿼리문을 작성하고 preparedStatement에 저장
- prepared statement의 setTYPE() method를 이용해서 ?에 들어갈 값 지정
- pstmt의 executeUpdate(), executeQuery()를 통해서 실제 db에 넣는 과정 수행
- close하기
String sql = "insert into member(member_id, money) values(? , ?)";
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
pstmt.setInt(1, memberId);
rs = pstmt.executeQuery();
int = pstmt.executeUpdate();
// try - catch - finally
finally - close()
- domain = DTO = VO
package hello.jdbc.domain;
import lombok.Data;
@Data
public class Member {
private String memberId;
private int money;
public Member(){}
public Member(String memberId, int money){
this.memberId = memberId;
this.money = money;
}
}
6.1. JDBC 개발 - 등록
package hello.jdbc.repository;
/**
* JDBC - DriverManager 사용
*/
@Slf4j
public class MemberRepositoryV0 {
// 1. 등록
public Member save(Member member) throws SQLException {
// sql : 데이터베이스에 전달할 SQL을 정의
String sql = "insert into member(member_id, money) values(? , ?)";
Connection con = null;
PreparedStatement pstmt = null; // sql params를 바인딩 받는 class
try {
con = getConnection(); // 1. connection 연결
pstmt = con.prepareStatement(sql); // 2. db랑 소통
pstmt.setString(1, member.getMemberId()); // sql string values의 첫번째 ?
pstmt.setInt(2, member.getMoney()); // sql string values의 두번째 ?
// dcl에 따라서 실행 메서드 명이 다르다.
pstmt.executeUpdate(); // 등록
return member;
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null); // close하는 과정에서 예외발생시 뒤에것도 close안되니깐 method로 분리
}
}
// h2 db Connection
private Connection getConnection() {
return DBConnectionUtil.getConnection();
}
// close() 예외 처리
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);
}
}
}
}
- 커넥션 획득
- getConnection() : 이전에 만들어둔 DBConnectionUtil 를 통해서 db connection 획득
- getConnection() : 이전에 만들어둔 DBConnectionUtil 를 통해서 db connection 획득
- SQL 전달
- PreparedStatement 는 Statement 의 자식 타입인데, ? 를 통한 파라미터 바인딩을 가능하게 한다.
- 참고로 SQL Injection 공격을 예방하려면 PreparedStatement 를 통한 파라미터 바인딩 방식을 사용
- sql : 데이터베이스에 전달할 SQL을 정의
- con.prepareStatement(sql) : 데이터베이스에 전달할 SQL과 파라미터로 전달할 데이터들을 준비
- sql
- pstmt.setString(1, member.getMemberId()) : SQL의 첫번째 ? 에 값을 지정
문자이므로 setString 을 사용 - pstmt.setInt(2, member.getMoney()) : SQL의 두번째 ? 에 값을 지정
Int 형 숫자이므로 setInt 를 지정
- sql
- pstmt.executeUpdate()
- Statement 를 통해 준비된 SQL을 커넥션을 통해 실제 데이터베이스에 전달
- int를 반환하는데 영향받은 DB row 수를 반환
- 리소스 정리
- 쿼리를 실행하고 나면 리소스를 정리해야 한다. 여기서는 Connection , PreparedStatement를 사용했다. 리소스를 정할 때는 항상 역순
- 참고로 여기서 사용하지 않은 ResultSet은 결과를 조회할 때 사용
- 리소스 정리는 꼭! 해주어야 한다.
- 예외가 발생하든, 하지 않든 항상 수행되어야 하므로 finally 구문에 주의해서 작성해야한다.
- 이 부분을 놓치게 되면 커넥션이 끊어지지 않고 계속 유지되는 문제가발생
이런 것을 리소스 누수라고 하는데, 결과적으로 커넥션 부족으로 장애가 발생
- 쿼리를 실행하고 나면 리소스를 정리해야 한다. 여기서는 Connection , PreparedStatement를 사용했다. 리소스를 정할 때는 항상 역순
6.2. JDBC 개발 - 조회
package hello.jdbc.repository;
/**
* JDBC - DriverManager 사용
*/
@Slf4j
public class MemberRepositoryV0 {
// 2. 조회
public Member findById(String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
Connection con = null; // db와 연결
PreparedStatement pstmt = null; // sql문에 추가 값 넣고 실행
ResultSet rs = null; // select query 문의 결과를 담고 있는 통
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
// dcl에 따라서 실행 메서드 명이 다르다.
// 조회시에는 Query() 사용, 등록시 update 사용
rs = pstmt.executeQuery(); // 조회
// db 존재 여부 확인
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);
}
}
// h2 db Connection
생략
// close() 예외 처리
생략
}
- JDBC 표준 인터페이스 마지막 - ResultSet
- ResultSet
- 데이터 구조 - table 모양
- 보통 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 타입으로 반환
- 조회하는 개수가 가늠이 안될 경우에는 while문으로 조회를 수행
6.3. JDBC 개발 - 수정
package hello.jdbc.repository;
/**
* JDBC - DriverManager 사용
*/
@Slf4j
public class MemberRepositoryV0 {
// 3. 수정
public void update(String memberId, int money) throws SQLException {
String sql = "update member set money=? where member_id = ?";
Connection con = null; // db와 연결
PreparedStatement pstmt = null; // sql문에 추가 값 넣고 실행
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);
}
}
// h2 db Connection
생략
// close() 예외 처리
생략
}
6.4. JDBC 개발 - 제거
package hello.jdbc.repository;
/**
* JDBC - DriverManager 사용
*/
@Slf4j
public class MemberRepositoryV0 {
// 4. 삭제
public void delete(String memberId) throws SQLException {
String sql = "delete from member where member_id = ?";
Connection con = null; // db와 연결
PreparedStatement pstmt = null; // sql문에 추가 값 넣고 실행
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 {
close(con, pstmt, null);
}
}
// h2 db Connection
생략
// close() 예외 처리
생략
}
6.5. test 코드
package hello.jdbc.repository;
import static org.assertj.core.api.Assertions.*;
@Slf4j
class MemberRepositoryV0Test {
MemberRepositoryV0 repository = new MemberRepositoryV0();
@Test
void crud() throws SQLException {
// save
Member member = new Member("memberV1", 10000);
repository.save(member);
// findById
Member findMember = repository.findById(member.getMemberId());
log.info("findMember={}", findMember);
log.info("findMember == member {}", findMember == member); // false
log.info("findMember.equals(member) {}", findMember.equals(member)); // true
// 다른 객체 member이지만 equals가 되는 이유는 @Data lombok 기능에서 equals를 오버라이딩 했기 때문
// -> 마냥 좋은 방법은 아닌 듯. effective java에서 equals overriding을 지양했다.
assertThat(findMember).isEqualTo(member);
// update: money: 10000 -> 20000
repository.update(member.getMemberId(), 20000);
Member updatedMember = repository.findById(member.getMemberId());
assertThat(updatedMember.getMoney()).isEqualTo(20000);
// delete
repository.delete(member.getMemberId());
assertThatThrownBy(()->repository.findById(member.getMemberId()))
.isInstanceOf(NoSuchElementException.class);
}
}
- Test 코드는 반복적으로 수행 될 수 있는 코드가 좋은 코드이다.
- 위와 같이 crud 가 모두 수행되는 code로 반복이 잘 수행되지만 중간에 예외가 발생하게 되고, 예외를 처리 후 다시 test code 수행시 수동으로 변경하는 과정이 생긴다.
- 이를 해결하기 위해서 transaction을 사용한다.