26/07/16 IL(I Learned) - Step 2

Statement, PreparedStatement, ResultSet를 통한 안전한 데이터 조회

데이터베이스에 연결한 이후 실제 SQL을 실행하는 방법을 학습하였다. 단순히 SQL을 실행하는 것에서 끝나는 것이 아니라 StatementPreparedStatement의 차이점, SQL Injection이 발생하는 이유, 그리고 조회 결과를 ResultSet으로 받아 객체로 변환하는 과정까지 직접 구현해보면서 JDBC의 핵심 기능을 이해할 수 있었다.


1) UseStatement

학습 내용상세 내용
StatementSQL 문자열을 그대로 데이터베이스에 전달하여 실행하는 객체이다.
executeQuery()SELECT 문을 실행하고 ResultSet을 반환하는 메서드이다.
SQL 문자열 조합문자열을 이어 붙여 SQL을 만드는 방식은 매우 위험할 수 있다.
SQL Injection사용자의 입력이 SQL 코드의 일부로 실행되는 보안 취약점이다.
ResultSetSELECT 결과를 한 행씩 가져오는 객체이다.
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이다.


SQL Injection이 위험한 이유

공격 예시발생 결과
OR '1'='1'모든 데이터 조회
DELETE FROM users데이터 삭제 가능
UNION SELECT ...다른 테이블 정보 조회 가능
관리자 로그인 우회인증 우회 가능

Statement의 동작 과정

사용자 입력
      │
      ▼
SQL 문자열 생성
      │
      ▼
Statement
      │
      ▼
DB에서 SQL 그대로 실행
      │
      ▼
ResultSet 반환

Statement는 사용자가 입력한 문자열을 그대로 SQL로 해석하기 때문에 보안상 매우 위험하다.

느낀 점내용
SQL Injection 체험실제 문자열 하나만으로 모든 데이터가 조회되는 것을 보며 보안의 중요성을 느꼈다.
Statement의 한계단순하지만 매우 위험한 방식이라는 것을 이해하였다.
사용자 입력 검증입력값은 절대로 SQL 문자열에 직접 이어 붙이면 안 된다는 점을 배웠다.

2) UsePreparedStatement

학습 내용상세 내용
PreparedStatementSQL과 데이터를 분리하여 실행하는 객체이다.
? 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 비교

구분StatementPreparedStatement
SQL 생성문자열 연결Placeholder 사용
SQL Injection발생 가능방지 가능
성능매번 컴파일실행 계획 재사용
사용 빈도거의 없음대부분 사용
실무 사용거의 X사실상 표준

내부 동작 과정

SQL 작성
SELECT * FROM users WHERE id = ?
            │
            ▼
SQL 먼저 컴파일
            │
            ▼
?에 값만 전달
            │
            ▼
DB 실행
            │
            ▼
ResultSet 반환

SQL 구조와 데이터가 분리되므로 매우 안전하다.

느낀 점내용
보안 향상SQL Injection을 원천적으로 막을 수 있다는 점이 인상 깊었다.
성능실행 계획을 재사용하기 때문에 Statement보다 효율적이라는 것을 배웠다.
실무 활용대부분의 프레임워크가 PreparedStatement를 사용하는 이유를 이해하였다.

3) UseResultSet

학습 내용상세 내용
ResultSetSELECT 결과를 저장하는 객체이다.
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())

데이터가 존재하는 동안 반복하는 코드이다.


ResultSet 구조

Before First
      │
      ▼
첫 번째 행
      │
      ▼
두 번째 행
      │
      ▼
세 번째 행
      │
      ▼
After Last

Alias를 사용하는 이유

이번 실습에서는

SELECT

id AS user_id,
name AS user_name,
age AS user_age

FROM users

처럼 Alias를 사용하였다.

그 이유는

  • 컬럼명이 겹칠 수 있기 때문
  • JOIN 시 이름을 구분하기 위해
  • 코드 가독성을 높이기 위해

이다.


Record를 사용하는 이유

조회 결과를

UserInfo

객체로 변환하였다.

객체를 사용하면

  • 데이터 전달이 편리하다.
  • 여러 값을 하나의 객체로 관리할 수 있다.
  • 가독성이 좋아진다.

이는 이후 Repository 패턴에서도 동일하게 활용된다.


느낀 점

느낀 점내용
데이터 읽기 이해ResultSet이 단순한 리스트가 아니라 커서를 기반으로 동작한다는 점을 이해하였다.
객체 변환조회 결과를 Record 객체로 변환하면서 객체지향적인 데이터 관리 방법을 배웠다.
실무 연결MyBatis와 JPA도 결국 ResultSet을 객체로 변환한다는 점을 알게 되어 프레임워크의 내부 동작을 이해하는 데 도움이 되었다.
profile
예 마 함 가보입시더

0개의 댓글