데이터베이스에 연결한 이후 실제 SQL을 실행하는 방법을 학습하였다. 단순히 SQL을 실행하는 것에서 끝나는 것이 아니라 Statement와 PreparedStatement의 차이점, SQL Injection이 발생하는 이유, 그리고 조회 결과를 ResultSet으로 받아 객체로 변환하는 과정까지 직접 구현해보면서 JDBC의 핵심 기능을 이해할 수 있었다.
| 학습 내용 | 상세 내용 |
|---|---|
| Statement | SQL 문자열을 그대로 데이터베이스에 전달하여 실행하는 객체이다. |
| executeQuery() | SELECT 문을 실행하고 ResultSet을 반환하는 메서드이다. |
| SQL 문자열 조합 | 문자열을 이어 붙여 SQL을 만드는 방식은 매우 위험할 수 있다. |
| SQL Injection | 사용자의 입력이 SQL 코드의 일부로 실행되는 보안 취약점이다. |
| ResultSet | SELECT 결과를 한 행씩 가져오는 객체이다. |
public class StatementExample {
public static void main(String[] args) {
String inputId = "1234' OR '1'='1";
try (Connection connection = DBUtil.getConnection()) {
String sql =
"SELECT * FROM users WHERE id = '%s'"
.formatted(inputId);
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
while (resultSet.next()) {
System.out.println(resultSet.getString("id"));
System.out.println(resultSet.getString("name"));
System.out.println(resultSet.getInt("age"));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
가장 먼저 사용자의 입력을 SQL 문자열 안에 직접 포함하였다.
String sql =
"SELECT * FROM users WHERE id = '%s'"
.formatted(inputId);
만약 사용자가
admin
을 입력하면 SQL은
SELECT * FROM users
WHERE id='admin'
이 된다.
하지만 실습에서는
1234' OR '1'='1
을 입력하였다.
그러면 실제 실행되는 SQL은
SELECT *
FROM users
WHERE id='1234'
OR '1'='1'
가 된다.
여기서
'1'='1'
은 항상 참(True)이므로 WHERE 조건이 모두 참이 되어 모든 사용자가 조회된다.
즉,
사용자의 입력이 데이터가 아니라 SQL 명령으로 실행되어 버린 것이다.
이것이 SQL Injection이다.
| 공격 예시 | 발생 결과 |
|---|---|
OR '1'='1' | 모든 데이터 조회 |
DELETE FROM users | 데이터 삭제 가능 |
UNION SELECT ... | 다른 테이블 정보 조회 가능 |
| 관리자 로그인 우회 | 인증 우회 가능 |
사용자 입력
│
▼
SQL 문자열 생성
│
▼
Statement
│
▼
DB에서 SQL 그대로 실행
│
▼
ResultSet 반환
Statement는 사용자가 입력한 문자열을 그대로 SQL로 해석하기 때문에 보안상 매우 위험하다.
| 느낀 점 | 내용 |
|---|---|
| SQL Injection 체험 | 실제 문자열 하나만으로 모든 데이터가 조회되는 것을 보며 보안의 중요성을 느꼈다. |
| Statement의 한계 | 단순하지만 매우 위험한 방식이라는 것을 이해하였다. |
| 사용자 입력 검증 | 입력값은 절대로 SQL 문자열에 직접 이어 붙이면 안 된다는 점을 배웠다. |
| 학습 내용 | 상세 내용 |
|---|---|
| PreparedStatement | SQL과 데이터를 분리하여 실행하는 객체이다. |
| ? Placeholder | 나중에 값이 들어갈 위치를 의미한다. |
| setString() | Placeholder에 문자열을 바인딩한다. |
| SQL Injection 방지 | 입력값을 SQL 코드가 아닌 데이터로 처리한다. |
| 실행 계획 재사용 | 동일 SQL은 컴파일 결과를 재사용하여 성능이 향상된다. |
public class PreparedExample {
public static void main(String[] args) {
String searchId = "1234' OR '1'='1";
try (Connection connection = DBUtil.getConnection()) {
String sql =
"SELECT * FROM users WHERE id = ?";
PreparedStatement preparedStatement =
connection.prepareStatement(sql);
preparedStatement.setString(1, searchId);
ResultSet resultSet =
preparedStatement.executeQuery();
while (resultSet.next()) {
System.out.println(resultSet.getString("id"));
System.out.println(resultSet.getString("name"));
System.out.println(resultSet.getInt("age"));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
PreparedStatement는 SQL과 데이터를 분리한다.
String sql =
"SELECT * FROM users WHERE id = ?";
여기서
?
는 아직 값이 없는 자리이다.
이후
preparedStatement.setString(1, searchId);
가 실행되면
사용자의 입력은 SQL이 아니라
단순 문자열 데이터
로 전달된다.
즉,
1234' OR '1'='1
이라는 문자열은
SQL 코드가 아니라
그냥 하나의 문자열로 인식된다.
따라서 SQL Injection이 발생하지 않는다.
| 구분 | Statement | PreparedStatement |
|---|---|---|
| SQL 생성 | 문자열 연결 | Placeholder 사용 |
| SQL Injection | 발생 가능 | 방지 가능 |
| 성능 | 매번 컴파일 | 실행 계획 재사용 |
| 사용 빈도 | 거의 없음 | 대부분 사용 |
| 실무 사용 | 거의 X | 사실상 표준 |
SQL 작성
SELECT * FROM users WHERE id = ?
│
▼
SQL 먼저 컴파일
│
▼
?에 값만 전달
│
▼
DB 실행
│
▼
ResultSet 반환
SQL 구조와 데이터가 분리되므로 매우 안전하다.
| 느낀 점 | 내용 |
|---|---|
| 보안 향상 | SQL Injection을 원천적으로 막을 수 있다는 점이 인상 깊었다. |
| 성능 | 실행 계획을 재사용하기 때문에 Statement보다 효율적이라는 것을 배웠다. |
| 실무 활용 | 대부분의 프레임워크가 PreparedStatement를 사용하는 이유를 이해하였다. |
| 학습 내용 | 상세 내용 |
|---|---|
| ResultSet | SELECT 결과를 저장하는 객체이다. |
| rs.next() | 다음 행으로 이동한다. |
| getString() | 문자열 데이터를 읽는다. |
| getInt() | 정수 데이터를 읽는다. |
| Alias | 컬럼 이름을 별칭으로 변경할 수 있다. |
| Record 활용 | 조회한 데이터를 객체로 변환하여 관리하였다. |
record UserInfo(
String userId,
String userName,
int userAge
) {}
PreparedStatement preparedStatement =
connection.prepareStatement(sql);
ResultSet resultSet =
preparedStatement.executeQuery();
while (resultSet.next()) {
UserInfo userInfo =
new UserInfo(
resultSet.getString("user_id"),
resultSet.getString("user_name"),
resultSet.getInt("user_age")
);
System.out.println(userInfo);
}
SELECT가 실행되면 ResultSet 객체가 생성된다.
ResultSet은
데이터 여러 개를 저장하는 것이 아니라
현재 한 행(Row)을 가리키는 커서(Cursor)를 가지고 있다.
처음에는
Before First
위치에 있다.
resultSet.next();
를 호출하면
첫 번째 행으로 이동한다.
다시 next()
↓
두 번째 행
↓
세 번째 행
↓
없으면 false
를 반환한다.
즉,
while(resultSet.next())
는
데이터가 존재하는 동안 반복하는 코드이다.
Before First
│
▼
첫 번째 행
│
▼
두 번째 행
│
▼
세 번째 행
│
▼
After Last
이번 실습에서는
SELECT
id AS user_id,
name AS user_name,
age AS user_age
FROM users
처럼 Alias를 사용하였다.
그 이유는
이다.
조회 결과를
UserInfo
객체로 변환하였다.
객체를 사용하면
이는 이후 Repository 패턴에서도 동일하게 활용된다.
| 느낀 점 | 내용 |
|---|---|
| 데이터 읽기 이해 | ResultSet이 단순한 리스트가 아니라 커서를 기반으로 동작한다는 점을 이해하였다. |
| 객체 변환 | 조회 결과를 Record 객체로 변환하면서 객체지향적인 데이터 관리 방법을 배웠다. |
| 실무 연결 | MyBatis와 JPA도 결국 ResultSet을 객체로 변환한다는 점을 알게 되어 프레임워크의 내부 동작을 이해하는 데 도움이 되었다. |