1. JDBC 이해
(1) 클라이언트, 애플리케이션 서버, DB
클라이언트 -> 애플리케이션 서버 -> DB
(2) 애플리케이션 서버 <-> DB
(3) JDBC
원래 DB 마다 DB 사용 코드, 설정 방법 등이 다 달랐음
-> JDBC 표준 인터페이스 제공 : 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 표준 인터페이스
JDBC 는 아래 기능을 표준 인터페이스로 정의해서 제공 -> 이 표준 인터페이스를 구현해서 개발
- Connection - 연결
- Statement - SQL을 담은 내용
- ResultSet - SQL 요청 응답
(4) JDBC 드라이버
JDBC 드라이버 : JDBC 인터페이스를 각 DB에 맞도록 구현해서 라이브러리로 제공
- JDBC 등장
-> 다른 DB로 변경하고 싶을 때 -> 드라이버(JDBC 구현 라이브러리)만 변경하면 됨
-> 애플리케이션 서버의 JDBC 연결 코드는 그대로 유지
-> 하지만, SQL은 해당 DB에 맞도록 변경 !
2. JDBC 접근 기술
(1) JDBC 직접 사용
- 아주 복잡
(2) SQL Mapper
- 장점 : JDBC를 편리하게 사용하도록 도와줌
- SQL 응답 결과를 객체로 편리하게 변환
- JDBC의 반복 코드 제거
- SQL만 직접 작성하면 나머지 번거로운 일을 SQL Mapper가 대신 해결해줌
- 단점 : 개발자가 SQL을 직접 작성
- 대표 기술 : 스프링 JdbcTemplate, MyBatis
(3) ORM 기술
- 객체를 관계형 데이터베이스 테이블과 매핑해주는 기술
- SQL 직접 작성 X , 하지만 깊이있게 학습해야 함
- 객체에 설정정보를 작성해서 JPA에 전달하면 SQL을 동적으로 만들어 실행
- 각 데이터베이스마다 다른 SQL을 사용하는 문제도 해결해줌
- 구현 기술 : 하이버네이트, 이클립스 링크
3. 데이터베이스 연결
※ H2 데이터베이스 서버 먼저 실행해둘 것 ! (h2 파일 -> bin -> h2.bat 실행)
(1) ConnectionConst - 데이터베이스 접속 정보를 상수로 등록
- URL : 저대로 작성하는게 규약임
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 = "";
}
(2) DBConnectionUtil - 데이터베이스 연결 코드
package hello.jdbc.connection;
import lombok.extern.slf4j.Slf4j;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import static hello.jdbc.connection.ConnectionConst.*;
@Slf4j
public class DBConnectionUtil {
public static Connection getConnection() {
try {
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("get connection={}, class={}", connection, connection.getClass());
return connection;
} catch (SQLException e) {
throw new IllegalStateException(e);
}
}
}
- JDBC가 제공하는 DriverManager.getConnection() 사용
-> 라이브러리에 등록되어 있는 데이터베이스 드라이버를 찾아서 해당 드라이버가 제공하는 커넥션 반환
-> 우리는 H2 db를 등록했음 -> H2 데이터베이스 드라이버 작동 -> 데이터베이스와 커넥션 -> 그 결과 반환
(3) DBConnectionUtilTest - 연결 테스트
package hello.jdbc.connection;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
import static org.assertj.core.api.Assertions.*;
@Slf4j
class DBConnectionUtilTest {
@Test
void connection() {
Connection connection = DBConnectionUtil.getConnection();
assertThat(connection).isNotNull();
}
}
테스트 결과
DBConnectionUtil -- get connection=conn0:
url=jdbc:h2:tcp://localhost/~/test user=SA,
class=class org.h2.jdbc.JdbcConnection
class=class org.h2.jdbc.JdbcConnection
: H2 데이터베이스 드라이버가 제공하는 H2 전용 커넥션
-> JDBC 표준 커넥션 인터페이스인 java.sql.Connection 인터페이스를 구현하고 있음
(4) JDBC DriverManager 동작
- JDBC는 java.sql.Connection 표준 인터페이스를 정의함
- H2 데이터베이스 드라이버는 JDBC Connection 인터페이스를 구현한 org.h2.jdbc.JdbcConnection 구현체 제공
* DriverManager 커넥션 요청 흐름
JDBC가 제공하는 DriverManager : 라이브러리에 등록된 DB 드라이버들 관리, 커넥션 획득하는 기능 제공
- 애플리케이션 로직에서 DriverManager.getConnection() 호출 -> 커넥션 요청
- DriverManager는 라이브러리에 등록된 드라이버 목록 자동으로 인식
-> 드라이버들에세 순서대로 아래 정보를 넘겨서 커넥션 획득할 수 있는지 확인- URL ( jdbc:h2:tcp://localhost/~/test )
- USERNAME , PASSWORD 등 접속에 필요한 추가 정보
- 각 드라이버는 URL 정보를 체크해서 본인이 처리할 수 있는 요청인지 확인
ex) jdbc:h2 ~~ : h2 드라이버가 처리할 수 있으므로 실제 데이터베이스에 연결해서 커넥션 획득, 클라이언트에 반환
MySQL 드라이버가 먼저 실행되면 -> 처리할 수 없다는 결과 반환 -> 다음 드라이버로 넘어감
- 커넥션 구현체가 클라이언트에 반환
* build.gradle
현재 H2 데이터베이스 드라이버만 라이브러리에 등록되어있음 -> H2 커넥션 제공받음
-> H2 커넥션은 JDBC가 제공하는 java.sql.Connection 인터페이스를 구현하고 있음
4. JDBC 개발 - JDBC 직접 연결
4-0. 테이블, 샘플 데이터 생성
1) h2.bat 실행 -> h2 콘솔 접속
jdbc:h2:~/test (최초 한번) -> 연결
2) Member 테이블생성
drop table member if exists cascade;
create table member (
member_id varchar(10),
money integer not null default 0,
primary key (member_id)
);
3) Member 객체 (도메인) 생성
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;
}
}
4-1. 등록 (Create)
1) MemberRepositoryV0 - 회원 등록 ( save() )
package hello.jdbc.repository;
import hello.jdbc.connection.DBConnectionUtil;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import java.sql.*;
/**
* 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; // finally에서도 호출해야 하기 때문에 밖에 따로 선언
PreparedStatement pstmt = null;
try {
con = getConnection(); // 커넥션 획득 -> 메소드로 추출
pstmt = con.prepareStatement(sql); // sql에 파라미터 바인딩
pstmt.setString(1, member.getMemberId()); // sql의 첫번째 ?에 문자 저장
pstmt.setInt(2, member.getMoney()); // sql의 두번째 ?에 숫자 저장
pstmt.executeUpdate(); // 실행 (sql을 커넥션을 통해 데이터베이스에 전달)
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) {
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);
}
}
}
private static Connection getConnection() {
return DBConnectionUtil.getConnection();
}
}
(1) 커넥션 획득
- getConnection() : 만들어둔 DBConnectionUtil을 사용해 데이터베이스 커넥션 획득
(2) save() - SQL 전달
- sql : 데이터베이스에 전달할 SQL 정의
- con.prepareStatement(sql) : 데이터베이스에 전달할 SQL과 파리미터로 전달할 데이터 준비
- Statement : SQL을 그대로 전달, PreparedStatement : 파라미터 바인딩한 SQL 전달
- PreparedStatement
- Statement의 자식 타입, ?를 통한 파라미터 바인딩
- SQL Injection 공격 예방하기 위해 그냥 얘를 써라 (파라미터 바인딩을 써야 파라미터를 데이터로 인식)
- pstmt.executeUpdate() : Statement를 통해 준비된 SQL을 커넥션을 통해 실제 데이터베이스에 전달
- executeUpdate()는 int를 반환 -> 영향받은 DB row 수 반환 (예 - 하나 등록하면 1 반환)
(3) 리소스 정리
- 사용 : Connection 획득 -> Connection을 통해 PreparedStatement 만듦
- 반환 : PreparedStatement 종료 -> Connection 종료 (역순)
- 쿼리 실행 후에는 리소스 정리 꼭 필요 !
- 예외가 발생하든 말든 항상 수행되어야 하므로 finally 구문에 작성
- 이 부분을 놓치면 커넥션이 끊어지지 않고 계속 유지되는 문제 (리소스 누수) -> 커넥션 부족으로 인한 장애 발생
* 예외
- 로그를 찍기 위해 예외 잡음
- 다시 예외를 던져주지 않으면 예외가 발생해도 여기서 예외를 먹어버리기 때문에 save()를 호출한 곳에서 정상 처리 되었다고 생각해버림 -> 데이터가 저장되지 않았는데 오류 없는 문제
catch (SQLException e) {
log.error("db error", e);
throw e;
}
2) MemberRepositoryV0Test - 테스트코드로 데이터베이스에 회원 등록
package hello.jdbc.repository;
import hello.jdbc.domain.Member;
import org.junit.jupiter.api.Test;
import java.sql.SQLException;
import static org.junit.jupiter.api.Assertions.*;
class MemberRepositoryV0Test {
MemberRepositoryV0 repository = new MemberRepositoryV0();
@Test
void crud() throws SQLException {
Member member = new Member("memberV0", 10000);
repository.save(member);
}
}
4-2. 조회 (Read)
1) MemberRepositoryV0 - 회원 조회 ( findById() )
@Slf4j
public class 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);
}
}
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);
}
}
}
private static Connection getConnection() {
return DBConnectionUtil.getConnection();
}
}
(1) findById() - 쿼리 실행
- sql : 데이터 조회를 위한 select SQL 준비
- rs = pstmt.executeQuery()
- 데이터 등록, 변경 -> executeUpdate()
- 데이터 조회 -> executeQuery() -> 조회 결과를 ResultSet에 담아서 반환
(2) ResultSet
- ResultSet에는 select 쿼리의 결과가 순서대로 들어감
- select member_id, money -> member_id, money라는 이름으로 데이터 저장
- select * -> 테이블의 모든 컬럼 지정
- ResultSet 내부에 있는 커서(cursor)를 이동해서 다음 데이터 조회
- rs.next() : 커서가 다음으로 이동. 최초의 커서는 데이터 가리키지 X
- rs.next() = true : 커서의 이동 결과 데이터가 있다
- rs.next() = false : 커서가 가리키는 데이터가 없다
- rs.getString("member_id") : 현재 커서가 가리키는 위치의 member_id 데이터를 String 타입으로 반환
- rs.getInt("money") : 현재 커서가 가리키는 위치의 money 데이터를 Int 타입으로 반환
- 보통 rs를 조회할 때는 while()로 조회하지만, findById()에서는 회원 하나를 조회하기 때문에 if 사용
2) MemberRepositoryV0Test - 회원 조회 테스트
@Slf4j
class MemberRepositoryV0Test {
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);
}
}
- Member 객체의 @Data 설정
- toString() 을 적절히 오버라이딩 -> findMember의 참조값이 아니라 실제 데이터로 이쁘게 보임
- equals, hashcode 를 적절히 오버라이딩
(원래 객체의 equal 비교는 주소값을 비교하는데, 얘를 오버라이딩하면 string의 equal 비교처럼 값을 비교할 수 있도록 오버라이딩 가능 -> @Data가 객체의 값을 비교하도록 오버라이딩을 해주는 것)
-> findMember == member : false (동일한 객첸지 비교)
-> findMember equals member : true (값이 같은지 비교 - isEqualTo() 는 내부에서 equals 비교를 해줌 )
4-3. 수정, 삭제 (Update, Delete)
1) MemberRepositoryV0 - 수정
- executeUpdate() : 쿼리 실행 후 영향받은 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 {
close(con, pstmt, null);
}
}
2) 테스트
package hello.jdbc.repository;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import java.sql.SQLException;
import java.util.NoSuchElementException;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
@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());
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);
}
}
* V2 생성 -> 10000 를 20000 으로 수정
3) MemberRepositoryV0 - 삭제
public void delete(String memberId) throws SQLException {
String sql = "delete 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);
}
}
4) 테스트
package hello.jdbc.repository;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import java.sql.SQLException;
import java.util.NoSuchElementException;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
@Slf4j
class MemberRepositoryV0Test {
MemberRepositoryV0 repository = new MemberRepositoryV0();
@Test
void crud() throws SQLException {
// save
Member member = new Member("memberV3", 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);
}
}
* V3 생성 -> 삭제
: 회원 삭제 후 findById() 로 조회 -> findById() 메소드에서 객체 없을 때 NoSuchElementException 발생하도록 만들었음
-> 회원이 없으니까 예외 발생 -> assertThatThrownBy() - 해당 예외 발생하면 검증 성공
'Spring' 카테고리의 다른 글
[인프런/스프링 DB 1편] 3. 트랜잭션 (0) | 2023.06.20 |
---|---|
[인프런/스프링 DB 1편] 2. 커넥션풀과 데이터소스 (0) | 2023.06.19 |
[Spring] 파일저장 transferTo, InputStream OutputStream (0) | 2023.06.06 |
[인프런/스프링 MVC 2편] 11. 파일 업로드, 다운로드 (0) | 2023.06.02 |
[인프런/스프링 MVC 2편] 10. 스프링 타입 컨버터 (0) | 2023.05.31 |