프로젝트 생성

Build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//테스트에서 lombok 사용
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
- 테스트 환경에서 롬복을 사용하기 위해 적용
H2 데이터베이스 설정
H2 데이터베이스 버전은 1.4.200을 사용했다. 맥환경에서는 설치 후 bin 폴더에서 ./h2.sh를 통해 실행
https://sasca37.tistory.com/13?category=1218302 를 참고하자.
- 최초 접속시
jdbc:h2:~/jdbc1후 루트 폴더에~/jdbc1.mv.db파일 생성 확인 - 이후 접속시
jdbc:h2:tcp://localhost/~/jdbc1
테이블 생성하기
drop table member if exists cascade;
create table member (
member_id varchar(10),
money integer not null default 0,
primary key (member_id)
);
insert into member(member_id, money) values ('hi1',10000);
insert into member(member_id, money) values ('hi2',20000);
JDBC 이해
JDBC 등장 이유
애플리케이션을 개발할 때 중요한 데이터는 대부분 데이터베이스에 보관한다.

- 커넥션 연결 : 주로 TCP/IP를 사용해서 커넥션 연결
- SQL 전달 : 애플리케이션 서버는 DB가 이해할 수 있는 SQL을 전달
- 결과 응답 : DB는 전달된 SQL을 수행하고 그 결과를 응답
과거에는 데이터베이스 별 커넥션 연결, SQL 전달, 결과 응답 등을 새로 학습해야 했다. 이 문제를 해결하기 위해 JDBC라는 자바 표준 인터페이스가 등장했다.
JDBC 표준 인터페이스
JDBC는 자바에서 데이터베이스에 접속하도록 지원하는 API다. 이 API를 통해 각 데이터베이스 별 연결해주는 드라이버를 사용하여 사용할 수 있다.

java.sql.Connection: 연결java.sql.Statement: SQL을 담은 내용java.sql.ResultSet: SQL 요청 응답
MySQL 드라이버

- 다음과 같이 데이터베이스 별 드라이버를 사용해서 개발자는 JDBC 표준 인터페이스에만 의존해서 사용하게 됐다. 단 SQL 문법은 각각 다른 점이 있다. 이 부분은 직접 코드를 수정해야한다.
JDBC와 최신 데이터 접근 기술


- JDBC를 편리하게 사용하게 지원해주는 SQL Mapper와 ORM 기술이 존재한다.
데이터베이스 연결
ConnectionConst
package hello.jdbc.connection;
public abstract class ConnectionConst {
public static final String URL = "jdbc:h2:tcp://localhost/~/jdbc1";
public static final String USERNAME = "sa";
public static final String PASSWORD = "";
}
- 데이터베이스에 접속하는 정보를 상수로 만들어서 넣자.
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가 제공하는
DriverManger.getConnection()을 사용하면 된다. 이렇게 구현하면 라이브러리에 있는 데이터베이스 드라이버를 찾아서 해당 드라이버가 제공하는 커넥션을 반환해준다.
DBConnectionUtilTest
package hello.jdbc.connection;
import lombok.extern.slf4j.Slf4j;
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();
}
}
- H2 서버가 켜져있다면 getConnection을 통해 정상적으로 H2 드라이버를 가져오는 것을 확인할 수 있다.
JDBC DriverManager 이해

- 라이브러리에 등록된 DB 드라이버들을 관리하고 커넥션 요청이 왔을 때 드라이버들을 탐색하며 맞는 커넥션을 반환하는 기능을 제공한다.
JDBC 개발 - 등록
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;
}
}
MemberRepositoryV0 - 회원등록
package hello.jdbc.repository;
import hello.jdbc.connection.DBConnectionUtil;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import java.sql.*;
@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) {
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 Connection getConnection() {
return DBConnectionUtil.getConnection();
}
}
con.prepareStatement(sql): 데이터베이스에 전달할 SQL과 파라미터로 전달할 데이터를 준비하고?에 들어갈 타입에 맞춰서 파라미터를 바인딩 해준다 (파라미터 바인딩으로 SQL Insection 방지).pstmt.executeUpdate()는Statement를 통해 준비된 SQL을 커넥션을 통해 실제 데이터베이스에 전달한다.executeUpdate는 int를 반환하는데 영향받은 DB row 수를 반환한다.
MemberRepositoryV0Test - 회원등록
package hello.jdbc.repository;
import hello.jdbc.domain.Member;
import org.junit.jupiter.api.Test;
import java.sql.SQLException;
class MemberRepositoryV0Test {
MemberRepositoryV0 repository = new MemberRepositoryV0();
@Test
void crud() throws SQLException {
Member member = new Member("memberV0", 10000);
repository.save(member);
}
}
JDBC 개발 - 조회
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를 추가하자.executeQuery()는 결과를ResultSet에 담아서 반환한다. - 데이터를 등록, 수정, 삭제처럼 데이터를 변경하는 쿼리는
executeUpdate()를 사용하면 된다.
MemberRepositoryV0Test - 회원 조회 추가
@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);
}
ResultSet

ResultSet은 내부에 있는 커서를 이동해서 데이터를 탐색한다. 최초의 커서는 데이터를 가리키고 있지 않기 때문에 한 번은 호출해야 데이터를 조회할 수 있다.
JDBC 개발 - 수정, 삭제
수정과 삭제도 마찬가지로 executeUpdate()를 사용하면 된다.
MemberRepositoryV0 - 회원 수정 추가
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);
}
}
MemberRepositoryV0Test - 회원 수정 추가
@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
repository.update(member.getMemberId(), 20000);
Member updatedMember = repository.findById(member.getMemberId());
assertThat(updatedMember.getMoney()).isEqualTo(20000);
}
MemberRepositoryV0 - 회원 삭제 추가
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);
}
}
MemberRepositoryV0Test - 회원 삭제 추가
@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
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);
}
본 포스팅은 인프런 - 김영한님의 '스프링 DB 1편 - 데이터 접근 핵심 원리'를 정리한 내용입니다.
