이번 포스팅은 JDBC를 사용해서 애플리케이션을 개발해보자.
JDBC를 사용해서 회원(Member) 데이터를 데이터베이스에 관리하는 기능을 개발해본다
※ 시작하기 전에,,
H2 데이터베이스 설정 마지막에 있는 테이블과 샘플 데이터 만들기를 통해서 member 테이블을 미리 만들어두어야 한다.
schema.sql
drop table member if exists cascade;
create table member (
member_id varchar(10),
money integer not null default 0,
primary key (member_id)
);
Memeber
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;
}
}
회원의 ID와 해당 회원이 소지한 금액을 표현하는 단순한 클래스이다.
앞서 만들어둔 테이블을 미리 만들어둔 member 테이블에 데이터를 저장하고 조회할 때 사용한다.
가장 먼저 JDBC를 사용해서 이렇게 만든 회원 객체를 데이터베이스에 저장해보자
MemberRepositoryV0 - 회원 등록
package hello.jdbc.repository;
import hello.jdbc.connection.DBConnectionUtil;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import java.sql.*;
import java.util.NoSuchElementException;
/**
* 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;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, member.getMemberId());
pstmt.setInt(2, member.getMoney());
int count = pstmt.executeUpdate();
return member;
} catch (SQLException e) {
e.printStackTrace();
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(); // SQLException
} 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();
}
}
커넥션 획득
- getConnection() : 이전에 만들어 둔 DBConnectionUtil(https://drcode-devblog.tistory.com/580)을 통해서
데이터베이스 커넥션을 획득한다.
save() - SQL 전달
- sql : 데이터베이스에 전달할 SQL을 정의한다. 여기서는 데이터를 등록해야 하므로 insert sql을 준비한다.
- con.preparedStatement(sql) : 데이터베이스에 전달할 SQL과 파라미터로 전달할 데이터들을 준비한다.
sql : insert into member(member_id, money) values(?,?)
pstmt.setString(1, member.getMemberId()) : SQL의 첫번째 ? 에 값을 지정한다. 문자이므로 setString을 사용한다.
pstmt.setInt(2, member.getMoney()) : SQL의 두번째 ?에 값을 지정한다. Int형 숫자이므로 setInt를 지정한다.
pstmt.executeUpdate() : Statement를 통해 준비된 SQL을 커넥션을 통해 실제 데이터베이스에 전달한다.
참고로 executeUpdate() 은 int를 반환하는데 영향받은 DB row 술수를 반환한다.
여기서는 하나의 row를 등록했으므로 1을 반환한다.
executeUpdate()
int executeUpdate() throws SQLException;
리소스 정리
쿼리를 실행하고 나면 리소스를 정리해야 한다.
여기서는 Connection, PreparedStatement를 사용했다.
리소스를 정리할 때는 항상 역순으로 해야한다.
Connection을 먼저 획득하고 Connection을 통해 PreparedStatement를 만들었기 때문에 리소스를 반환할 때는
PreparedStatement를 먼저 종료하고, 그 다음에 Connection을 종료하면 된다.
참고로 여기서 사용하지 않은 ResultSet은 결과를 조회할 때 사용한다.
※ 주의
리소스 정리는 꼭!!! 해주어야 한다.
따라서 예외가 발생하든, 하지 않든 항상 수행되어야 하므로 finally 구문에 주의해서 작성해야 한다.
만약 이 부분을 놓치게 되면 커넥션이 끊어지지 않고 계속 유지되는 문제가 발생할 수 있다.
이런 것을 리소스 누수라고 하는데, 결과적으로 커넥션 부족으로 장애가 발생할 수 있다.
※ 참고
PreparedStatement는 Statement의 자식 타입인데, ?를 통한 파라미터 바인딩을 가능하게 한다.
참고로 SQL Injection 공격을 예방하려면 PreparedStatement 를 통한 파라미터 바인딩 방식을 사용해야 한다.
※ SQL Injection이란?
SQL 인젝션은 악의적인 공격으로,
애플리케이션에서 입력값을 통해 SQL 쿼리를 실행할 때 발생할 수 있는 취약점이다.
Java에서는 SQL 인젝션을 방지하기 위해 PreparedStatement와 같은 안전한 방법을 사용하는 것이 좋다.
PreparedStatement는 사용자 입력을 동적으로 생성된 SQL 쿼리에 바인딩하는 데 사용되며,
이를 통해 SQL 쿼리의 파라미터가 미리 정의된 형식으로 전달된다.
이를 통해 악의적인 SQL 코드 삽입을 방지할 수 있다.
예를 들어, 일반적인 Statement를 사용한 경우:
String userInput = getUserInput();
String sqlQuery = "SELECT * FROM users WHERE username='" + userInput + "'";
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sqlQuery);
위의 코드에서는 사용자 입력이 직접 SQL 쿼리에 삽입되기 때문에 SQL 인젝션에 취약하다.
대신, PreparedStatement를 사용하여 안전한 코드를 작성할 수 있다:
String userInput = getUserInput();
String sqlQuery = "SELECT * FROM users WHERE username=?";
PreparedStatement preparedStatement = connection.prepareStatement(sqlQuery);
preparedStatement.setString(1, userInput);
ResultSet resultSet = preparedStatement.executeQuery();
이렇게 하면 사용자 입력이 안전하게 SQL 쿼리에 바인딩되어 SQL 인젝션 공격을 방지할 수 있다.
추가로, 보안 측면에서는 입력 유효성 검사와 권한 부여 등의 기법을 사용하여 안전성을 높일 수 있다.
이제 테스트 코드를 사용해서 JDBC로 회원을 데이터베이스에 등록해보자.
MemberRepositoryV0Test - 회원 등록
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.assertThat;
@Slf4j
class MemberRepositoryV0Test {
MemberRepositoryV0 repository = new MemberRepositoryV0();
@Test
void crud() throws SQLException {
// save
Member member = new Member("memberV1", 10000);
repository.save(member);
}
}
실행 결과
데이터베이스에서 select * from member 쿼리를 실행하면 데이터가 저장된 것을 확인할 수 있다.
참고로 이 테스트는 2번 실행하면 PK 중복 오류가 발생한다.
이 경우 delete from member 쿼리로 데이터를 삭제한 다음에 다시 실행하자.
PK 중복 오류
org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Unique index or primary key violation: "PUBLIC.PRIMARY_KEY_8 ON PUBLIC.MEMBER(MEMBER_ID) VALUES 9"; SQL statement:
"C:\Program Files\Java\jdk-17\bin\java.exe" -ea -Didea.test.cyclic.buffer.size=1048576 "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2021.1.2\lib\idea_rt.jar=60066:C:\Program Files\JetBrains\IntelliJ IDEA 2021.1.2\bin" -Dfile.encoding=UTF-8 -classpath "C:\Users\dr854\.m2\repository\org\junit\platform\junit-platform-launcher\1.10.1\junit-platform-launcher-1.10.1.jar;C:\Users\dr854\.m2\repository\org\junit\platform\junit-platform-engine\1.10.1\junit-platform-engine-1.10.1.jar;C:\Users\dr854\.m2\repository\org\opentest4j\opentest4j\1.3.0\opentest4j-1.3.0.jar;C:\Users\dr854\.m2\repository\org\junit\platform\junit-platform-commons\1.10.1\junit-platform-commons-1.10.1.jar;C:\Users\dr854\.m2\repository\org\apiguardian\apiguardian-api\1.1.2\apiguardian-api-1.1.2.jar;C:\Program Files\JetBrains\IntelliJ IDEA 2021.1.2\lib\idea_rt.jar;C:\Program Files\JetBrains\IntelliJ IDEA 2021.1.2\plugins\junit\lib\junit5-rt.jar;C:\Program Files\JetBrains\IntelliJ IDEA 2021.1.2\plugins\junit\lib\junit-rt.jar;C:\DEV\SpringBoot\jdbc\out\test\classes;C:\DEV\SpringBoot\jdbc\out\production\classes;C:\DEV\SpringBoot\jdbc\out\production\resources;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot-starter-jdbc\3.2.1\e3de1c46cd238d55f0c2a5f5ea416fadce000318\spring-boot-starter-jdbc-3.2.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.projectlombok\lombok\1.18.30\f195ee86e6c896ea47a1d39defbe20eb59cd149d\lombok-1.18.30.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot-starter-test\3.2.1\e6a20062864e3a9a0bba0ac3b0c5a819453045b9\spring-boot-starter-test-3.2.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot-starter\3.2.1\bc03d7075fb9d9d4877218db48d5dae3dd72a65d\spring-boot-starter-3.2.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\com.zaxxer\HikariCP\5.0.1\a74c7f0a37046846e88d54f7cb6ea6d565c65f9c\HikariCP-5.0.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.springframework\spring-jdbc\6.1.2\2c9b37e4cc9bfd7e413d4685f09b6c5447d75638\spring-jdbc-6.1.2.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot-test-autoconfigure\3.2.1\bfc34c523b3ab295fb01f46373e903f9729cdd43\spring-boot-test-autoconfigure-3.2.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot-test\3.2.1\142fbe3cfe3370c57d0ed55cca0d8d96e1d6f26e\spring-boot-test-3.2.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\com.jayway.jsonpath\json-path\2.8.0\b4ab3b7a9e425655a0ca65487bbbd6d7ddb75160\json-path-2.8.0.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\jakarta.xml.bind\jakarta.xml.bind-api\4.0.1\ca2330866cbc624c7e5ce982e121db1125d23e15\jakarta.xml.bind-api-4.0.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\net.minidev\json-smart\2.5.0\57a64f421b472849c40e77d2e7cce3a141b41e99\json-smart-2.5.0.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.assertj\assertj-core\3.24.2\ebbf338e33f893139459ce5df023115971c2786f\assertj-core-3.24.2.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.awaitility\awaitility\4.2.0\2c39784846001a9cffd6c6b89c78de62c0d80fb8\awaitility-4.2.0.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.hamcrest\hamcrest\2.2\1820c0968dba3a11a1b30669bb1f01978a91dedc\hamcrest-2.2.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.junit.jupiter\junit-jupiter\5.10.1\6e5c7dd668d6349cb99e52ab8321e73479a309bc\junit-jupiter-5.10.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.mockito\mockito-junit-jupiter\5.7.0\ac2d6a3431747a7986b8f4abef465f72bf3a21ae\mockito-junit-jupiter-5.7.0.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.mockito\mockito-core\5.7.0\a1c258331ab91d66863c983aff7136357e9de056\mockito-core-5.7.0.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.skyscreamer\jsonassert\1.5.1\6d842d0faf4cf6725c509a5e5347d319ee0431c3\jsonassert-1.5.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.springframework\spring-test\6.1.2\c393079051398e02c20d8b24e02822f365123719\spring-test-6.1.2.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.springframework\spring-core\6.1.2\e43c71a9eaca454654621f7d272f15b53c68d583\spring-core-6.1.2.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.xmlunit\xmlunit-core\2.9.1\e5833662d9a1279a37da3ef6f62a1da29fcd68c4\xmlunit-core-2.9.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot-autoconfigure\3.2.1\b100d2d21d45dddd740d496357ca6f3813d777d0\spring-boot-autoconfigure-3.2.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot\3.2.1\faa2ce019bee68a8d17529d0a08ebc427f927e13\spring-boot-3.2.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot-starter-logging\3.2.1\df8ec78dc87885298998ca3c9bd603ee7bfe5b8\spring-boot-starter-logging-3.2.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\jakarta.annotation\jakarta.annotation-api\2.1.1\48b9bda22b091b1f48b13af03fe36db3be6e1ae3\jakarta.annotation-api-2.1.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.yaml\snakeyaml\2.2\3af797a25458550a16bf89acc8e4ab2b7f2bfce0\snakeyaml-2.2.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.slf4j\slf4j-api\2.0.9\7cf2726fdcfbc8610f9a71fb3ed639871f315340\slf4j-api-2.0.9.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.springframework\spring-tx\6.1.2\48b9ff81c43345320779b402f1bb33b9f46ec4e9\spring-tx-6.1.2.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.springframework\spring-beans\6.1.2\abf52f2254975a3b1e95b2b63fb8b01d891cdc51\spring-beans-6.1.2.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\jakarta.activation\jakarta.activation-api\2.1.2\640c0d5aff45dbff1e1a1bc09673ff3a02b1ba12\jakarta.activation-api-2.1.2.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\net.minidev\accessors-smart\2.5.0\aca011492dfe9c26f4e0659028a4fe0970829dd8\accessors-smart-2.5.0.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\net.bytebuddy\byte-buddy\1.14.10\8117daf4a612122eb4f517f66adff778cb8b4737\byte-buddy-1.14.10.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.junit.jupiter\junit-jupiter-params\5.10.1\c8f15d4e99940c4564098af78c10809c00fdca06\junit-jupiter-params-5.10.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.junit.jupiter\junit-jupiter-api\5.10.1\eb90c7d8bfaae8fdc97b225733fcb595ddd72843\junit-jupiter-api-5.10.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\net.bytebuddy\byte-buddy-agent\1.14.10\90ed94ac044ea8953b224304c762316e91fd6b31\byte-buddy-agent-1.14.10.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\com.vaadin.external.google\android-json\0.0.20131108.vaadin1\fa26d351fe62a6a17f5cda1287c1c6110dec413f\android-json-0.0.20131108.vaadin1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.springframework\spring-jcl\6.1.2\285eb725861c9eacf2a3e4965d4e897932e335ea\spring-jcl-6.1.2.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.springframework\spring-context\6.1.2\15df19852991220556b4462a366269b8e15278eb\spring-context-6.1.2.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\ch.qos.logback\logback-classic\1.4.14\d98bc162275134cdf1518774da4a2a17ef6fb94d\logback-classic-1.4.14.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.apache.logging.log4j\log4j-to-slf4j\2.21.1\d77b2ba81711ed596cd797cc2b5b5bd7409d841c\log4j-to-slf4j-2.21.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.slf4j\jul-to-slf4j\2.0.9\9ef7c70b248185845f013f49a33ff9ca65b7975\jul-to-slf4j-2.0.9.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.ow2.asm\asm\9.3\8e6300ef51c1d801a7ed62d07cd221aca3a90640\asm-9.3.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.apiguardian\apiguardian-api\1.1.2\a231e0d844d2721b0fa1b238006d15c6ded6842a\apiguardian-api-1.1.2.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.junit.platform\junit-platform-commons\1.10.1\2bfcd4a4e38b10c671b6916d7e543c20afe25579\junit-platform-commons-1.10.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.opentest4j\opentest4j\1.3.0\152ea56b3a72f655d4fd677fc0ef2596c3dd5e6e\opentest4j-1.3.0.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.springframework\spring-aop\6.1.2\a247bd81df8fa9c6a002b95969692bfd146a70b2\spring-aop-6.1.2.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.springframework\spring-expression\6.1.2\98786397734b27b7c8843a6b01a7fa34d40d6806\spring-expression-6.1.2.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\io.micrometer\micrometer-observation\1.12.1\fbd0e0e9b6a36effd53e0eee35b050ed1f548ae5\micrometer-observation-1.12.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\ch.qos.logback\logback-core\1.4.14\4d3c2248219ac0effeb380ed4c5280a80bf395e8\logback-core-1.4.14.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.apache.logging.log4j\log4j-api\2.21.1\74c65e87b9ce1694a01524e192d7be989ba70486\log4j-api-2.21.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\io.micrometer\micrometer-commons\1.12.1\abcc6b294e60582afdfae6c559c94ad1d412ce2d\micrometer-commons-1.12.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\com.h2database\h2\2.2.224\7bdade27d8cd197d9b5ce9dc251f41d2edc5f7ad\h2-2.2.224.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.junit.jupiter\junit-jupiter-engine\5.10.1\6c9ff773f9aa842b91d1f2fe4658973252ce2428\junit-jupiter-engine-5.10.1.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.objenesis\objenesis\3.3\1049c09f1de4331e8193e579448d0916d75b7631\objenesis-3.3.jar;C:\Users\dr854\.gradle\caches\modules-2\files-2.1\org.junit.platform\junit-platform-engine\1.10.1\f32ae4af74fde68414b8a3d2b7cf1fb43824a83a\junit-platform-engine-1.10.1.jar" com.intellij.rt.junit.JUnitStarter -ideVersion5 -junit5 hello.jdbc.repository.MemberRepositoryV0Test,crud
22:15:07.678 [main] INFO hello.jdbc.connection.DBConnectionUtil -- get connection=conn0: url=jdbc:h2:tcp://localhost/~/dbAccess user=SA, class=class org.h2.jdbc.JdbcConnection
org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Unique index or primary key violation: "PUBLIC.PRIMARY_KEY_8 ON PUBLIC.MEMBER(MEMBER_ID) VALUES 14"; SQL statement:
insert into member(member_id, money) values(?, ?) [23505-199]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:457)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:427)
at org.h2.message.DbException.get(DbException.java:205)
at org.h2.message.DbException.get(DbException.java:181)
at org.h2.index.BaseIndex.getDuplicateKeyException(BaseIndex.java:103)
at org.h2.mvstore.db.MVSecondaryIndex.checkUnique(MVSecondaryIndex.java:220)
at org.h2.mvstore.db.MVSecondaryIndex.add(MVSecondaryIndex.java:196)
at org.h2.mvstore.db.MVTable.addRow(MVTable.java:546)
at org.h2.command.dml.Insert.insertRows(Insert.java:180)
at org.h2.command.dml.Insert.update(Insert.java:132)
at org.h2.command.CommandContainer.update(CommandContainer.java:133)
at org.h2.command.Command.executeUpdate(Command.java:267)
at org.h2.server.TcpServerThread.process(TcpServerThread.java:398)
at org.h2.server.TcpServerThread.run(TcpServerThread.java:175)
at java.base/java.lang.Thread.run(Thread.java:842)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:520)
at org.h2.engine.SessionRemote.readException(SessionRemote.java:650)
at org.h2.engine.SessionRemote.done(SessionRemote.java:619)
at org.h2.command.CommandRemote.executeUpdate(CommandRemote.java:237)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:216)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:174)
at hello.jdbc.repository.MemberRepositoryV0.save(MemberRepositoryV0.java:27)
at hello.jdbc.repository.MemberRepositoryV0Test.crud(MemberRepositoryV0Test.java:22)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:728)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:218)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:214)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:139)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:69)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:198)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:169)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:93)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:58)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:141)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:57)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:103)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:85)
at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47)
at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:63)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Unique index or primary key violation: "PUBLIC.PRIMARY_KEY_8 ON PUBLIC.MEMBER(MEMBER_ID) VALUES 14"; SQL statement:
insert into member(member_id, money) values(?, ?) [23505-224]
org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Unique index or primary key violation: "PUBLIC.PRIMARY_KEY_8 ON PUBLIC.MEMBER(MEMBER_ID) VALUES 14"; SQL statement:
insert into member(member_id, money) values(?, ?) [23505-199]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:457)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:427)
at org.h2.message.DbException.get(DbException.java:205)
at org.h2.message.DbException.get(DbException.java:181)
at org.h2.index.BaseIndex.getDuplicateKeyException(BaseIndex.java:103)
at org.h2.mvstore.db.MVSecondaryIndex.checkUnique(MVSecondaryIndex.java:220)
at org.h2.mvstore.db.MVSecondaryIndex.add(MVSecondaryIndex.java:196)
at org.h2.mvstore.db.MVTable.addRow(MVTable.java:546)
at org.h2.command.dml.Insert.insertRows(Insert.java:180)
at org.h2.command.dml.Insert.update(Insert.java:132)
at org.h2.command.CommandContainer.update(CommandContainer.java:133)
at org.h2.command.Command.executeUpdate(Command.java:267)
at org.h2.server.TcpServerThread.process(TcpServerThread.java:398)
at org.h2.server.TcpServerThread.run(TcpServerThread.java:175)
at java.base/java.lang.Thread.run(Thread.java:842)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:520)
at org.h2.engine.SessionRemote.readException(SessionRemote.java:650)
at org.h2.engine.SessionRemote.done(SessionRemote.java:619)
at org.h2.command.CommandRemote.executeUpdate(CommandRemote.java:237)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:216)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:174)
at hello.jdbc.repository.MemberRepositoryV0.save(MemberRepositoryV0.java:27)
at hello.jdbc.repository.MemberRepositoryV0Test.crud(MemberRepositoryV0Test.java:22)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
Process finished with exit code -1
JDBC 개발 - 조회
이번에는 JDBC를 통해 이전에 저장한 데이터를 조회하는 기능을 개발해보자
MemberRepositoryV0 - 회원 조회 추가
import java.util.NoSuchElementException;
public Member findById(String memberId) {
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.info("error", e);
} finally {
close(con, pstmt, rs);
}
return null;
}
findById() - 쿼리 실행
- sql : 데이터 조회를 위한 select SQL 을 준비한다.
- rs = pstmt.executeQuery() 데이터를 변경할 때는 executeUpdate() 를 사용하지만,
데이터를 조회할 때는 executeQuery()를 사용한다.
executeQuery()는 결과를 ResultSet에 담아서 반환한다.
executeQuery()
ResultSet executeQuery() throws SQLException;
ResultSet
- ResultSet은 다음과 같이 생긴 데이터 구조이다. 보통 select 쿼리의 결과가 순서대로 들어간다.
: 예를 들어서 select member_id, money 라고 지정하면 member_id, money 라는 이름으로 데이터가 저장된다.
: 참고로 select * 을 사용하면 테이블의 모든 컬럼을 다 지정한다.
- ResultSet 내부에 있는 커서(cursor)를 이동해서 다음 데이터를 조회할 수 있다.
- rs.next() 를 최초 한번은 호출해야 데이터를 조회할 수 있다.
: rs.next() 의 결과가 true 이면 커서의 이동 결과 데이터가 있다는 뜻이다.
: rs.next() 의 결과가 false 이면 더이상 커서가 가리키는 데이터가 없다는 뜻이다.
- rs.getString('member_id") : 현재 커사가 가리키고 있는 위치의 member_id 데이터를 String 타입으로 반환한다.
- rs.getInt("money") : 현재 커서가 가리키고 있는 위치의 money 데이터를 int 타입으로 반환한다.
ResultSet 결과 예시
참고로 이 ResultSet의 결과 예시는 회원이 2명 조회되는 경우이다.
1-1 에서 rs.next() 를 호출한다.
1-2의 결과로 cursor가 다음으로 이동한다. 이 경우 cursor가 가리키는 데이터가 있으므로 true를 반환한다.
2-1에서 rs.next()를 호출한다.
2-2의 결과로 cursor가 다음으로 이동한다. 이 경우 cursor가 가리키는 데이터가 있으므로 true를 반환한다.
3-1에서 rs.next()를 호출한다.
3-2의 결과로 cursor가 다음으로 이동한다. 이 경우 cursor가 가리키는 데이터가 없으므로 false를 반환한다.
findById()에서는 회원 하나를 조회하는 것이 목적이다.
따라서 조회 결과가 항상 1건이므로 while 대신에 if 를 사용한다.
다음 SQL을 보면 PK인 member_id를 항상 지정하는 것을 확인할 수 있다.
SQL : select * from member where member_id = ?
MemberRepositoryV0Test - 회원 조회 추가
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.assertThat;
@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);
log.info("member != findMember {}", member.equals(findMember));
assertThat(findMember).isEqualTo(member);
}
}
실행 결과
MemberRepositoryV0Test - findMember=Member(memberId=memberV0, money=10000)
- 회원을 등록하고 그 결과를 바로 조회해서 확인해본다.
- 참고로 실행 결과에 member 객체의 참조 값이 아니라 실제 데이터가 보이는 이유는
롬복의 @Data가 toString()을 적절히 오버라이딩해서 보여주기 때문이다.
- isEqualTo() : findMember.equals(member) 를 비교한다.
결과가 참인 이유는 롬복의 @Data는 해당 객체의 모든 필드를 사용하도록 equals()를 오버라이딩 하기 때문이다.
참고
이 테스트는 2번 실행하면 PK 중복 오류가 발생한다.
이 경우 delete from member 쿼리로 데이터를 삭제한 다음에 실행해야한다.
PK 중복 오류
org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Unique index or primary key violation: "PUBLIC.PRIMARY_KEY_8 ON PUBLIC.MEMBER(MEMBER_ID) VALUES 9"; SQL statement:
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.info("error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
executeUpdate() 는 쿼리를 실행하고 영향받은 row수를 반환한다.
여기서는 하나의 데이터만 변경하기 때문에 결과로 1이 반환된다.
만약 회원이 100명이고, 모든 회원의 데이터를 한번에 수정하는 update sql을 실행하면 결과는 100이 된다
MemberRepositoryV0Test - 회원 수정 추가
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.assertThat;
@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);
log.info("member != findMember {}", member.equals(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);
}
}
회원 데이터의 money를 10000 → 20000으로 수정하고,
DB에서 데이터를 다시 조회해서 20000으로 변경되었는지 검증한다.
실행 로그
MemberRepositoryV0 - resultSize=1
pstmt.executeUpdate() 의 결과가 1인 것을 확인할 수 있다.
이것은 해당 SQL에 영향을 받은 로우 수가 1개라는 뜻이다.
데이터베이스에서 조회하면 memberV4 의 money가 20000 으로 변경된 것을 확인할 수 있다.
select * from member;
참고
이 테스트는 2번 실행하면 PK 중복 오류가 발생한다.
이 경우 delete from member 쿼리로 데이터를 삭제한 다음에 다시 실행한다.
이번에는 회원을 삭제해보자.
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);
int resultSize = pstmt.executeUpdate();
log.info("resultSize={}", resultSize);
} catch (SQLException e) {
log.info("error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
쿼리만 변경되고 내용은 update와 거의 같다.
MemberRepositoryV0Test - 회원 삭제 추가
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.assertThat;
@Slf4j
class MemberRepositoryV0Test {
MemberRepositoryV0 repository = new MemberRepositoryV0();
@Test
void crud() throws SQLException {
// save
Member member = new Member("memberV5", 10000);
repository.save(member);
// findById
Member findMember = repository.findById(member.getMemberId());
log.info("findMember={}", findMember);
log.info("member != findMember {}", member.equals(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());
Assertions.assertThatThrownBy(() -> repository.findById(member.getMemberId()))
.isInstanceOf(NoSuchElementException.class);
}
}
회원을 삭제한 다음 findById()를 통해서 조회한다.
회원이 없기 때문에 NoSuchElementException이 발생한다.
assertThatThrownBy는 해당 예외가 발생해야 검증에 성공한다.
※ 참고
마지막에 회원을 삭제하기 때문에 테스트가 정상 수행되면, 이제부터는 같은 테스트를 반복해서 실행할 수 있다.
물론 테스트 중간에 오류가 발생해서 삭제 로직을 수행할 수 없다면 테스트를 반복해서 실행할 수 없다.
트랜잭션을 활용하면 이 문제를 깔끔하게 해결할 수 있다
'스프링 > 스프링 DB 접근' 카테고리의 다른 글
[Spring] DB Lock (1) | 2024.12.18 |
---|---|
[Spring] 트랜잭션 이해 - 트랜잭션 개념 (0) | 2024.12.17 |
[Spring] DataSource 적용 예제(DriverManager & HikariDataSource) (0) | 2024.12.17 |
[Spring] 커넥션 풀과 데이터 소스 개념 (0) | 2024.01.19 |
[Spring] JDBC의 이해와 JDBC 관련 기술 (+ H2 Database 설치), 데이터베이스 연결 (0) | 2024.01.18 |
댓글