TIL/[TIL]

[TIL]JDBC 기초: 연결, Statement, PreparedStatement, SQL Injection, CRUD 흐름

namerong 2026. 5. 18. 16:10

1. 학습 주제

Java 애플리케이션이 데이터베이스와 연결되는 전체 흐름을 정리했다.
단순히 SQL을 작성하는 것이 아니라, Java 코드 안에서 Connection, Statement, PreparedStatement, ResultSet를 어떻게 사용하고, 왜 JDBCTemplate, DTO, SQL 분리 구조가 필요한지도 함께 이해할 수 있었다.

  • JDBC로 DB 연결하기
  • properties 파일로 접속 정보 분리하기
  • JDBCTemplate로 공통 코드 정리하기
  • Statement와 PreparedStatement 차이 이해하기
  • ResultSet으로 조회 결과 읽기
  • SQL Injection이 왜 위험한지 확인하기
  • PreparedStatement가 왜 더 안전한지 이해하기
  • DTO와 SQL 분리 파일을 활용한 CRUD 구조 익히기

2. JDBC란

JDBC는 Java Database Connectivity의 줄임말로, 자바 프로그램이 데이터베이스와 통신할 수 있게 해주는 표준 API이다.

즉 JDBC를 사용하면 자바 코드에서:

  • DB에 접속하고
  • SQL을 전달하고
  • 결과를 받아오고
  • 연결을 종료할 수 있다

흐름을 아주 단순하게 보면 다음과 같다.

  1. DB 연결 생성
  2. SQL 실행 객체 생성
  3. SQL 실행
  4. 결과 처리
  5. 자원 반납

3. DB 연결

가장 먼저 필요한 것은 Connection 객체이다.

Connection con = DriverManager.getConnection(
    "jdbc:mysql://localhost/employeedb",
    "ohgiraffers",
    "ohgiraffers"
);

Connection은 자바 프로그램과 DB 사이의 연결 통로 역할을 한다.

연결 전에 JDBC 드라이버도 로딩했다.

Class.forName("com.mysql.cj.jdbc.Driver");

즉 DB 연결의 핵심은:

  • 어떤 드라이버를 쓸지 정하고
  • 어떤 DB 주소로 갈지 정하고
  • 어떤 계정으로 접속할지 정하는 것

이다.


4. 접속 정보 분리

예제에서는 DB 접속 정보를 코드에 직접 쓰는 대신 properties 파일로 분리했다.

Properties prop = new Properties();
prop.load(reader);

String driver = prop.getProperty("driver");
String url = prop.getProperty("url");
String user = prop.getProperty("user");
String password = prop.getProperty("password");

이 방식이 중요한 이유는:

  • 코드 수정 없이 설정값만 바꿀 수 있고
  • 유지보수가 쉬워지고
  • 민감한 정보를 코드와 분리할 수 있기 때문이다

즉 실무에서는 접속 정보를 직접 하드코딩하기보다 외부 설정 파일로 분리하는 것이 훨씬 일반적이다.


5. JDBCTemplate

JDBC는 반복 코드가 많다.
예를 들면:

  • Connection 생성
  • Connection 닫기
  • Statement 닫기
  • ResultSet 닫기

이런 코드가 계속 반복된다.

그래서 예제에서는 JDBCTemplate를 만들어 공통 처리 로직을 모아두었다.

public static Connection getConnection() { ... }
public static void close(Connection con) { ... }
public static void close(Statement stmt) { ... }
public static void close(ResultSet rset) { ... }

이 구조의 장점은:

  • 중복 코드 감소
  • 코드 가독성 향상
  • 자원 반납 실수 줄이기

즉 JDBCTemplate는 JDBC에서 자주 반복되는 연결/닫기 로직을 재사용하기 위해 만든 공통 유틸 클래스라고 볼 수 있다.


6. Statement

Statement는 SQL 문자열을 DB로 전달해서 실행하는 객체이다.

stmt = con.createStatement();
rset = stmt.executeQuery("SELECT MENU_NAME, MENU_PRICE FROM TBL_MENU");

조회 쿼리를 실행할 때는 executeQuery()를 사용하고,
결과는 ResultSet으로 받는다.

while(rset.next()) {
    System.out.println(rset.getString("MENU_NAME") + ", " + rset.getInt("MENU_PRICE"));
}

여기서 중요한 점:

  • rset.next()는 다음 행으로 이동
  • 현재 행이 존재하면 true
  • 각 컬럼 값은 getString, getInt 같은 메서드로 꺼낸다

즉 Statement는 가장 기본적인 SQL 실행 방식이다.


7. ResultSet

ResultSet은 SELECT 결과를 담고 있는 객체이다.
조회된 여러 행을 커서 방식으로 하나씩 읽는다.

while(rset.next()) {
    ...
}

읽는 방식은 다음 흐름으로 이해하면 쉽다.

  1. executeQuery() 실행
  2. 결과가 ResultSet에 담김
  3. next()로 한 줄씩 이동
  4. 현재 줄에서 컬럼 값 읽기

즉 ResultSet은 “조회 결과 테이블을 자바에서 한 줄씩 읽어오는 도구”라고 볼 수 있다.


8. Statement의 한계

예제에서 사용자 입력을 문자열로 이어붙여 SQL을 만든 코드가 있었다.

String query =
    "SELECT MENU_NAME, MENU_PRICE FROM TBL_MENU WHERE MENU_CODE = '" + menuCode + "'";

이 방식은 간단해 보이지만 문제가 있다.

  • 문자열 연결이 번거롭다
  • 숫자/문자 타입 구분이 불편하다
  • 입력값에 따라 SQL 구조가 깨질 수 있다
  • SQL Injection에 취약하다

즉 Statement는 동적으로 사용자 입력을 조합할 때 위험할 수 있다.


9. SQL Injection

예제에서 SQL Injection 공격 상황도 보여줬다.

private static String empName = "' OR 1=1 AND EMP_ID = '200";

이 값을 문자열로 이어붙이면 원래 의도한 조건식이 깨지고,
공격자가 원하는 SQL 구조로 바뀔 수 있다.

즉 SQL Injection은:

  • 사용자 입력값이 SQL 문법 일부처럼 동작하면서
  • 원래 의도하지 않은 쿼리가 실행되게 만드는 공격

이다.

이건 JDBC에서 왜 PreparedStatement를 써야 하는지 설명하는 가장 중요한 이유 중 하나다.


10. PreparedStatement

PreparedStatement는 미리 SQL 틀을 준비하고,
사용자 입력은 나중에 값으로만 넣는 방식이다.

String query = "SELECT * FROM EMPLOYEE WHERE EMP_ID = ? AND EMP_NAME = ?";
pstmt = con.prepareStatement(query);
pstmt.setString(1, empId);
pstmt.setString(2, empName);

여기서 ?는 위치 홀더이다.

즉 SQL 구조와 사용자 입력 데이터를 분리한다.

이 방식의 장점은:

  • SQL Injection 방어
  • 가독성 향상
  • 타입에 맞게 값 설정 가능
  • 동적 입력 처리에 적합

즉 실무에서는 Statement보다 PreparedStatement가 훨씬 더 자주 쓰인다.


11. PreparedStatement가 안전한 이유

가장 중요한 차이는 이것이다.

Statement는 입력값을 SQL 문장 일부처럼 이어붙인다.
반면 PreparedStatement는 입력값을 SQL 문법이 아니라 “데이터 값”으로 처리한다.

예를 들어:

pstmt.setString(2, empName);

이렇게 넣으면 입력값 안에 ', OR, 1=1 같은 내용이 있어도
SQL 구조를 바꾸지 못하고 그냥 문자열 데이터로만 인식된다.

즉 PreparedStatement는 SQL Injection 방어에 매우 중요하다.


12. 조회 결과를 DTO로 담기

예제에서는 조회한 한 행의 데이터를 MenuDTO 객체에 담았다.

row = new MenuDTO();
row.setMenuCode(rset.getInt("MENU_CODE"));
row.setMenuName(rset.getString("MENU_NAME"));
row.setMenuPrice(rset.getInt("MENU_PRICE"));
row.setCategoryCode(rset.getInt("CATEGORY_CODE"));
row.setOrderableStatus(rset.getString("ORDERABLE_STATUS"));

그리고 여러 행이면 List<MenuDTO>에 담았다.

menuList = new ArrayList<>();
menuList.add(row);

이 방식의 장점은:

  • DB 결과를 객체 형태로 다룰 수 있다
  • 출력, 전달, 가공이 쉬워진다
  • SQL 결과와 프로그램 로직을 자연스럽게 연결할 수 있다

즉 DTO는 DB 한 행을 자바 객체로 옮겨 담는 그릇 역할을 한다.


13. SQL을 XML로 분리

예제에서는 SQL 문장을 자바 코드에 직접 쓰지 않고 XML 파일에 분리해 두었다.

<entry key="insertMenu">
    INSERT INTO
        TBL_MENU (
            MENU_NAME
            , MENU_PRICE
            , CATEGORY_CODE
            , ORDERABLE_STATUS
        )
    VALUES(?, ?, ?, ?)
</entry>

그리고 자바에서는 이렇게 불러왔다.

prop.loadFromXML(queryStream);
String query = prop.getProperty("insertMenu");

이 방식의 장점은:

  • 자바 코드와 SQL 분리
  • 쿼리 관리 편리
  • SQL 수정 시 자바 코드 수정 최소화
  • 유지보수성 향상

즉 규모가 커질수록 SQL을 별도 파일로 관리하는 구조가 더 중요해진다.


14. CRUD 중 INSERT

예제에서는 INSERT도 PreparedStatement로 처리했다.

pstmt = con.prepareStatement(query);
pstmt.setString(1, "봉골레청국장");
pstmt.setInt(2, 11000);
pstmt.setInt(3, 1);
pstmt.setString(4, "Y");

result = pstmt.executeUpdate();

SELECT와 다른 점은:

  • 조회 결과인 ResultSet을 받지 않는다
  • 대신 영향받은 행 수를 int로 반환한다

즉:

  • executeQuery() : 조회용
  • executeUpdate() : INSERT, UPDATE, DELETE용

이라고 정리하면 된다.


15. JDBC에서 자원 반납이 중요한 이유

JDBC는 사용 후 반드시 닫아줘야 하는 객체가 많다.

  • ResultSet
  • Statement / PreparedStatement
  • Connection

예제에서도 항상 마지막에 닫아주었다.

close(rset);
close(stmt);
close(con);

그리고 보통 “나중에 열린 것부터 먼저 닫는 순서”를 사용한다.

이걸 잊으면:

  • 연결이 계속 남아 있을 수 있고
  • 자원 누수가 발생할 수 있고
  • 프로그램 성능과 안정성에 문제가 생길 수 있다

그래서 JDBC에서는 자원 반납이 매우 중요하다.


16. 전체 흐름 정리

이번 JDBC 학습 흐름은 크게 이렇게 정리할 수 있었다.

16.1 연결

  • 드라이버 로딩
  • Connection 생성
  • properties 파일로 접속 정보 분리

16.2 실행

  • Statement로 기본 SQL 실행
  • PreparedStatement로 안전한 SQL 실행
  • ResultSet으로 조회 결과 읽기

16.3 구조화

  • JDBCTemplate로 공통 로직 재사용
  • DTO로 결과를 객체화
  • XML 파일로 SQL 분리

16.4 CRUD 시작

  • executeUpdate()로 데이터 변경 처리
  • 영향받은 행 수 확인

17. 핵심 정리

  1. JDBC는 자바에서 데이터베이스와 연결하고 SQL을 실행하기 위한 표준 API이다.
  2. Connection은 자바와 DB를 이어주는 연결 통로이다.
  3. 접속 정보는 보통 properties 파일로 분리해 관리한다.
  4. Statement는 기본 SQL 실행 객체이지만 동적 입력 처리에는 취약할 수 있다.
  5. PreparedStatement는 SQL 틀과 입력값을 분리해 더 안전하게 실행한다.
  6. PreparedStatement는 SQL Injection 방어에 매우 중요하다.
  7. ResultSet은 SELECT 결과를 한 행씩 읽어오는 객체이다.
  8. 조회 결과는 DTO에 담아 객체처럼 다루는 것이 편리하다.
  9. SQL을 XML이나 별도 파일로 분리하면 유지보수성이 좋아진다.
  10. executeQuery()는 조회, executeUpdate()는 데이터 변경 작업에 사용한다.
  11. JDBC에서는 ResultSet, Statement, Connection을 반드시 닫아야 한다.
  12. JDBCTemplate는 반복되는 연결/닫기 코드를 줄이기 위한 공통 유틸 구조이다.