프로젝트 생성
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편 - 데이터 접근 핵심 원리'를 정리한 내용입니다.