📐 우리가 서버 개발 과정에서 사용하는 수많은 DB 라이브러리들은 이 문제를 어떻게 해결할까요?
DB 라이브러리는 데이터베이스와 애플리케이션 사이의 통신을 도와주는 소프트웨어 도구들을 말하며, 대표적으로 낮은 수준의 드라이버/커넥터와 높은 수준의 추상화 프레임워크(ORM 등)가 있습니다.
SQL 쿼리를 직접 작성하고 실행하며, 결과 집합을 직접 다루게 됩니다.
저수준에서 데이터베이스와의 직접적인 통신을 다루므로, SQL 쿼리를 직접 작성하고 파라미터 바인딩, 결과 처리 등을 개발자가 직접 관리해야 합니다.
데이터베이스 작업을 보다 추상화하여, 객체지향 프로그래밍 방식이나 함수 호출을 통해 데이터베이스에 접근할 수 있게 도와줍니다.
내부적으로는 앞서 언급한 저수준 드라이버(JDBC, ODBC 등)를 사용하지만, 직접 SQL을 작성하는 수고는 줄어듭니다.
ORM (Object-Relational Mapping):
데이터베이스 테이블과 애플리케이션의 객체(클래스)를 매핑하여, 객체를 다루듯이 데이터베이스 작업을 수행할 수 있게 해줍니다.
저수준 라이브러리 위에 구축된 고수준 추상화 도구입니다.
Java: Hibernate, JPA
PHP: Laravel
Python: Django ORM, SQLAlchemy
Ruby: ActiveRecord (Ruby on Rails)
Node.js: Sequelize, TypeORM
=> 내부적으로는 parameterized query 방식을 사용하여 보안 문제를 예방합니다.
SQL Query Builder:
// users 테이블에서 username과 password가 일치하는 레코드를 조회하는 예제
knex('users')
.select('*')
.where('username', '=', 'hi')
.andWhere('password', '=', 'hi')
.then(rows => {
console.log('조회 결과:', rows);
})
.catch(err => {
console.error('쿼리 실행 오류:', err);
})
.finally(() => {
// 연결 종료
knex.destroy();
});SQL 인젝션 방어와 쿼리의 재사용을 위해 SQL 코드와 데이터를 분리하는 목적에서 사용되지만, 약간 개념이 다릅니다.
prepare() 메서드를 통해 쿼리를 준비bind 또는 setParameter 같은 메서드로 파라미터 전달둘다 SQL 코드와 데이터를 분리하여 보안과 성능을 개선하려는 목적을 갖지만,
사용자가 직접 prepared statement 객체를 다루는지(Prepared Statement)
내부적으로 자동으로 준비 과정을 처리하는지(Parametrized Query)
의 차이가 있습니다.
import java.sql.*;
public class Main {
// 테이블 생성 메서드
public static void createTable(Connection conn) throws SQLException {
try (Statement stmt = conn.createStatement()) {
// 기존 테이블이 있으면 삭제
stmt.execute("DROP TABLE IF EXISTS users");
// 테이블 생성
String sql = "CREATE TABLE users (" +
"id int auto_increment primary key, " +
"username varchar(10), " +
"password varchar(10)" +
")";
stmt.execute(sql);
}
}
// 더미 데이터 삽입
public static void insertData(Connection conn) throws SQLException {
try (Statement stmt = conn.createStatement()) {
stmt.execute("INSERT INTO users (username, password) VALUES ('hihi', 'password')");
stmt.execute("INSERT INTO users (username, password) VALUES ('hoho', 'password')");
}
}
/**
* SQL Injection 공격에 취약한 로그인 (입력값을 직접 SQL에 포함)
* @return 로그인 성공 시 true, 실패 시 false
*/
public static boolean vulnerableLogin(Connection conn, String username, String password) throws SQLException {
String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
System.out.println("실행 쿼리 (취약): " + query);
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query)) {
return rs.next();
}
}
/**
* 안전한 로그인 메서드 (PreparedStatement)
* @return 로그인 성공 시 true, 실패 시 false
*/
public static boolean safeLogin(Connection conn, String username, String password) throws SQLException {
String query = "SELECT * FROM users WHERE username = ? AND password = ?";
System.out.println("실행 쿼리 (안전): " + query);
try (PreparedStatement pstmt = conn.prepareStatement(query)) {
pstmt.setString(1, username);
pstmt.setString(2, password);
try (ResultSet rs = pstmt.executeQuery()) {
return rs.next();
}
}
}
}
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*;
import java.sql.*;
public class MainTest {
private Connection conn;
@BeforeEach
public void setUp() throws Exception {
// H2 연결
Class.forName("org.h2.Driver");
conn = DriverManager.getConnection("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1", "sa", "");
Main.createTable(conn);
Main.insertData(conn);
}
@AfterEach
public void tearDown() throws SQLException {
if (conn != null && !conn.isClosed()) {
conn.close();
}
}
@Test
public void 취약로그인() throws SQLException {
boolean result = Main.vulnerableLogin(conn, "hihi", "password");
assertTrue(result, "로그인 성공");
}
@Test
public void 취약SQLInjection공격() throws SQLException {
boolean result = Main.vulnerableLogin(conn, "hihi", "anything' OR '1'='1");
assertTrue(result, "취약 쿼리에 Injection 공격 성공");
}
@Test
public void 안전로그인() throws SQLException {
boolean result = Main.safeLogin(conn, "hihi", "password");
assertTrue(result, "로그인 성공");
}
@Test
public void 안전SQLInjection공격() throws SQLException {
boolean result = Main.safeLogin(conn, "hihi", "anything' OR '1'='1");
assertFalse(result, "pstmt 쿼리에 Injection 공격 성공");
}
}

참고 문헌