Java에서 INSERT, UPDATE, DELETE 실행 방법
들어가며: MyBatis를 쓰는 시대에 왜 JDBC를 먼저 배워야 하는가
데이터베이스를 다루는 모든 애플리케이션은 결국 네 가지 동작으로 귀결된다. 데이터를 생성하고(Create), 조회하고(Read), 수정하고(Update), 삭제하는(Delete) 것. 이 네 가지를 묶어 CRUD라고 부른다.
현업에서는 JDBC를 직접 다루는 경우가 드물다. 대부분의 Java 백엔드 프로젝트는 MyBatis 또는 JPA 같은 프레임워크를 사용한다. MyBatis는 SQL을 XML로 분리해 관리하고, 반복적인 Connection 열기·닫기, PreparedStatement 생성 같은 보일러플레이트 코드를 자동으로 처리해준다. JPA는 한 발 더 나아가 SQL 자체를 직접 작성하지 않아도 되는 환경을 제공한다.
그렇다면 왜 지금 JDBC와 PreparedStatement를 배우는가.
이유는 단순하다. MyBatis도, JPA도 내부적으로는 JDBC 위에서 동작한다. 프레임워크는 개발자가 직접 작성해야 했던 JDBC 코드를 대신 처리해줄 뿐, 데이터베이스와 통신하는 실제 레이어는 여전히 JDBC다. 프레임워크가 추상화해놓은 레이어 아래에서 어떤 일이 일어나는지 모르는 상태에서 MyBatis를 사용하면, 문제가 발생했을 때 원인을 찾기 어렵고 프레임워크의 동작 방식을 깊이 이해하기도 힘들다.
JDBC를 먼저 익히는 것은 돌아가는 길이 아니라, 이후 어떤 프레임워크를 배우더라도 흔들리지 않는 기반을 만드는 과정이다. Connection이 왜 닫혀야 하는지, PreparedStatement가 왜 Statement보다 안전한지, 트랜잭션이 어떤 구조로 동작하는지를 이해한 상태에서 MyBatis를 학습하면 프레임워크의 설계 의도가 훨씬 명확하게 보인다.
이 글에서는 Java에서 INSERT, UPDATE, DELETE를 실행하는 방법을 실제 코드 예제와 함께 단계별로 정리한다. 개념 설명에 그치지 않고, 실제로 실행 가능한 코드를 기준으로 각 구문의 동작 방식과 주의사항까지 함께 다룬다. 참고로 오늘의 글은 아래 글의 심화편이라고 봐도 좋다.

JDBC 기본 구조와 PreparedStatement를 써야 하는 이유
Java에서 데이터베이스를 다룰 때는 JDBC(Java Database Connectivity) 를 사용한다. JDBC는 Java와 데이터베이스를 연결하는 표준 API이며, SQL을 실행할 수 있는 통로 역할을 한다.
JDBC를 통한 데이터 처리는 항상 동일한 흐름을 따른다.
- 데이터베이스 연결 (Connection)
- SQL 준비 (PreparedStatement)
- 파라미터 바인딩
- SQL 실행 (executeUpdate / executeQuery)
- 결과 처리
- 자원 종료 (close)
여기서 SQL을 실행하는 방법은 두 가지다. Statement와 PreparedStatement다. 실무에서는 반드시 PreparedStatement를 사용해야 한다. 이유는 명확하다.
Statement 방식 (사용하지 말 것):
String sql = "SELECT * FROM users WHERE name = '" + name + "'";
Statement stmt = conn.createStatement();
stmt.executeQuery(sql);
이 방식은 SQL 문자열에 사용자 입력값을 직접 이어 붙인다. 만약 name에 ' OR '1'='1이 입력된다면 SQL 전체 구조가 바뀌어 버린다. 이것이 SQL Injection 공격이며, 데이터 전체가 노출되거나 삭제될 수 있다.
PreparedStatement 방식 (올바른 방법):
String sql = "SELECT * FROM users WHERE name = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name);
pstmt.executeQuery();
? 자리에 값을 바인딩하는 방식이기 때문에 SQL 구조 자체가 변경될 수 없다. SQL과 데이터가 분리되어 전달되기 때문에 SQL Injection이 원천 차단된다. 또한 동일한 SQL을 반복 실행할 때 성능상 이점도 있다.
이후 모든 예제는 PreparedStatement를 기준으로 작성한다.
데이터베이스 연결 설정
본격적인 CRUD 예제에 앞서, 데이터베이스 연결 설정을 먼저 확인한다. 예제에서는 MySQL을 기준으로 설명한다.
의존성 추가 (Maven 기준):
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
</dependency>
연결 정보 상수 정의:
public class DBConfig {
public static final String URL = "jdbc:mysql://localhost:3306/testdb?useSSL=false&serverTimezone=Asia/Seoul";
public static final String USER = "myuser";
public static final String PASSWORD = "password";
}
예제에서 사용할 테이블 구조:
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
age INT
);
이 테이블을 기준으로 INSERT, UPDATE, DELETE 예제를 순서대로 작성한다.
INSERT — 데이터 추가하기
INSERT는 테이블에 새로운 행을 추가하는 구문이다. JDBC에서 INSERT를 실행할 때는 executeUpdate() 메서드를 사용하며, 반환값은 영향을 받은 행의 개수다.
기본 INSERT 예제:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class InsertExample {
public static void main(String[] args) {
String sql = "INSERT INTO users (name, email, age) VALUES (?, ?, ?)";
try (Connection conn = DriverManager.getConnection(DBConfig.URL, DBConfig.USER, DBConfig.PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, "홍길동");
pstmt.setString(2, "hong@example.com");
pstmt.setInt(3, 30);
int result = pstmt.executeUpdate();
System.out.println("INSERT 완료 - 영향받은 행: " + result);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
자동 생성된 키(AUTO_INCREMENT) 값 가져오기:
INSERT 실행 후 생성된 id 값을 즉시 가져와야 하는 경우가 있다. 이때는 prepareStatement(sql, Statement.RETURN_GENERATED_KEYS) 옵션을 사용한다.
String sql = "INSERT INTO users (name, email, age) VALUES (?, ?, ?)";
try (Connection conn = DriverManager.getConnection(DBConfig.URL, DBConfig.USER, DBConfig.PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
pstmt.setString(1, "김철수");
pstmt.setString(2, "kim@example.com");
pstmt.setInt(3, 25);
pstmt.executeUpdate();
// 자동 생성된 키 조회
try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
if (generatedKeys.next()) {
long generatedId = generatedKeys.getLong(1);
System.out.println("생성된 ID: " + generatedId);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
회원가입 후 생성된 사용자 ID를 즉시 활용해야 하는 경우, 이 방식을 사용한다.
UPDATE — 데이터 수정하기
UPDATE는 기존 데이터를 변경하는 구문이다. INSERT와 동일하게 executeUpdate()를 사용하며, 반환값은 영향을 받은 행의 개수다.
UPDATE에서 가장 중요한 것은 WHERE 조건이다. 조건을 누락하면 테이블의 모든 행이 변경된다. 이는 서비스 장애로 직결되는 치명적인 실수이며, 실무에서 가장 자주 발생하는 실수 중 하나다.
기본 UPDATE 예제:
public class UpdateExample {
public static void main(String[] args) {
// WHERE 조건 필수 — 없으면 전체 데이터 변경
String sql = "UPDATE users SET email = ?, age = ? WHERE id = ?";
try (Connection conn = DriverManager.getConnection(DBConfig.URL, DBConfig.USER, DBConfig.PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, "hong_new@example.com");
pstmt.setInt(2, 31);
pstmt.setInt(3, 1); // 변경 대상 id
int result = pstmt.executeUpdate();
if (result > 0) {
System.out.println("UPDATE 완료 - 영향받은 행: " + result);
} else {
System.out.println("변경된 데이터 없음 — 조건을 확인하세요.");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
동적 UPDATE — 변경할 컬럼이 조건에 따라 달라지는 경우:
실무에서는 수정할 항목이 요청마다 달라지는 경우가 많다. 이때는 StringBuilder를 활용해 SQL을 동적으로 구성한다.
public static int updateUserSelective(int id, String name, String email) throws SQLException {
StringBuilder sql = new StringBuilder("UPDATE users SET ");
List<Object> params = new ArrayList<>();
if (name != null) {
sql.append("name = ?, ");
params.add(name);
}
if (email != null) {
sql.append("email = ?, ");
params.add(email);
}
// 마지막 쉼표 제거
sql.delete(sql.length() - 2, sql.length());
sql.append(" WHERE id = ?");
params.add(id);
try (Connection conn = DriverManager.getConnection(DBConfig.URL, DBConfig.USER, DBConfig.PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql.toString())) {
for (int i = 0; i < params.size(); i++) {
pstmt.setObject(i + 1, params.get(i));
}
return pstmt.executeUpdate();
}
}
이 방식은 MyBatis의 <set> 태그가 내부적으로 처리하는 방식과 동일한 논리다. 순수 JDBC에서 동적 UPDATE를 구현할 때의 기본 패턴으로 알아두면 유용하다.
DELETE — 데이터 삭제하기
DELETE는 테이블에서 행을 삭제하는 구문이다. INSERT, UPDATE와 동일하게 executeUpdate()를 사용한다.
UPDATE와 마찬가지로 WHERE 조건이 없으면 테이블의 전체 데이터가 삭제된다. DELETE는 CRUD 구문 중 복구가 가장 어렵기 때문에, 실행 전 반드시 조건을 재확인하는 습관이 필요하다.
기본 DELETE 예제:
public class DeleteExample {
public static void main(String[] args) {
// WHERE 조건 필수 — 없으면 테이블 전체 삭제
String sql = "DELETE FROM users WHERE id = ?";
try (Connection conn = DriverManager.getConnection(DBConfig.URL, DBConfig.USER, DBConfig.PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, 1); // 삭제 대상 id
int result = pstmt.executeUpdate();
if (result > 0) {
System.out.println("DELETE 완료 - 삭제된 행: " + result);
} else {
System.out.println("삭제된 데이터 없음 — 조건을 확인하세요.");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
복수 조건 DELETE 예제:
특정 조건에 해당하는 여러 행을 한 번에 삭제해야 하는 경우다.
// 나이가 특정 값 이하인 비활성 사용자 삭제
String sql = "DELETE FROM users WHERE age < ? AND email LIKE ?";
try (Connection conn = DriverManager.getConnection(DBConfig.URL, DBConfig.USER, DBConfig.PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, 20);
pstmt.setString(2, "%@test.com");
int result = pstmt.executeUpdate();
System.out.println("삭제된 행 수: " + result);
} catch (SQLException e) {
e.printStackTrace();
}
IN 절을 활용한 다중 DELETE:
여러 개의 ID를 한 번에 삭제할 때는 IN 절을 활용한다.
List<Integer> ids = List.of(2, 3, 5);
// ? 자리를 ids 개수만큼 동적으로 생성
String placeholders = ids.stream()
.map(id -> "?")
.collect(Collectors.joining(", "));
String sql = "DELETE FROM users WHERE id IN (" + placeholders + ")";
try (Connection conn = DriverManager.getConnection(DBConfig.URL, DBConfig.USER, DBConfig.PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
for (int i = 0; i < ids.size(); i++) {
pstmt.setInt(i + 1, ids.get(i));
}
int result = pstmt.executeUpdate();
System.out.println("삭제된 행 수: " + result);
} catch (SQLException e) {
e.printStackTrace();
}
자원 관리와 트랜잭션 처리
자원 관리 — try-with-resources
JDBC에서 Connection, PreparedStatement, ResultSet은 외부 자원을 사용하는 객체다. 사용이 끝난 후 명시적으로 닫지 않으면 Connection Pool 고갈, 메모리 누수 등의 문제가 발생할 수 있다.
Java 7부터 도입된 try-with-resources 구문을 사용하면 자원이 자동으로 닫힌다.
// try-with-resources: 블록이 끝나면 자동으로 close() 호출
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
// 실행 코드
} catch (SQLException e) {
e.printStackTrace();
}
// 여기서 pstmt.close(), conn.close() 자동 호출
트랜잭션 처리
INSERT, UPDATE, DELETE 여러 작업이 하나의 논리적 단위로 묶여야 하는 경우에는 트랜잭션이 필요하다. 예를 들어 “주문 생성 + 재고 감소”는 두 작업이 모두 성공하거나 모두 실패해야 한다. 하나만 성공하면 데이터 정합성이 깨진다.
Connection conn = null;
try {
conn = DriverManager.getConnection(DBConfig.URL, DBConfig.USER, DBConfig.PASSWORD);
conn.setAutoCommit(false); // 트랜잭션 시작
// 작업 1: 주문 생성
String insertSql = "INSERT INTO orders (user_id, product_id, amount) VALUES (?, ?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(insertSql)) {
pstmt.setInt(1, 1);
pstmt.setInt(2, 10);
pstmt.setInt(3, 2);
pstmt.executeUpdate();
}
// 작업 2: 재고 감소
String updateSql = "UPDATE products SET stock = stock - ? WHERE id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(updateSql)) {
pstmt.setInt(1, 2);
pstmt.setInt(2, 10);
pstmt.executeUpdate();
}
conn.commit(); // 두 작업 모두 성공 시 커밋
System.out.println("트랜잭션 완료");
} catch (SQLException e) {
if (conn != null) {
try {
conn.rollback(); // 하나라도 실패 시 롤백
System.out.println("트랜잭션 롤백");
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
if (conn != null) {
try {
conn.setAutoCommit(true);
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
setAutoCommit(false)로 자동 커밋을 끄고, 모든 작업이 성공하면 commit(), 하나라도 실패하면 rollback()을 호출하는 패턴이다. 실무에서 데이터 정합성을 보장하는 핵심 구조다.
나오며: JDBC CRUD 이후의 다음 단계
이번 글에서 다룬 내용을 정리하면 다음과 같다.
| 구문 | 메서드 | 반환값 | 주의사항 |
|---|---|---|---|
| INSERT | executeUpdate() | 영향받은 행 수 | AUTO_INCREMENT 키 회수 방법 숙지 |
| UPDATE | executeUpdate() | 영향받은 행 수 | WHERE 조건 필수 |
| DELETE | executeUpdate() | 영향받은 행 수 | WHERE 조건 필수, 복구 어려움 |
| SELECT | executeQuery() | ResultSet | — |
INSERT, UPDATE, DELETE는 모두 executeUpdate()를 사용하며, SELECT만 executeQuery()를 사용한다는 점을 반드시 기억하자.
JDBC로 CRUD를 직접 구현해보는 과정은 데이터베이스 연동의 가장 기초적인 레이어를 이해하는 데 있어 중요한 경험이다. 하지만 실무에서는 매번 Connection을 직접 열고 닫고, PreparedStatement를 수동으로 관리하는 방식은 반복 코드가 많고 실수가 발생하기 쉽다.
다음 단계로는 MyBatis를 학습하는 것을 권장한다. MyBatis는 JDBC의 반복적인 보일러플레이트 코드를 제거하면서도 SQL에 대한 완전한 제어권을 유지할 수 있는 SQL Mapper 프레임워크다. JDBC의 동작 원리를 이해한 상태에서 MyBatis를 학습하면, 왜 MyBatis가 등장했는지와 어떤 문제를 해결하는지를 맥락 속에서 파악할 수 있다.
JDBC → MyBatis → Spring Data JPA로 이어지는 흐름을 하나씩 이해하는 것이 Java 백엔드 개발자로 성장하는 가장 탄탄한 경로다.
Add your first comment to this post