Java에서 MySQL 연결 하는 방법 — JDBC 기본부터 쿼리 실행까지
들어가며
SQL과 MySQL을 어느 정도 익히고 나면 자연스럽게 다음 질문이 생긴다. “이걸 실제 프로그램에서 어떻게 쓰는가.” 터미널에서 쿼리를 직접 실행하는 것과, Java 코드 안에서 데이터베이스를 다루는 것은 완전히 다른 차원의 문제다. SQL 문법을 알고 있다는 것과, 그것을 애플리케이션에 녹여낼 수 있다는 것은 별개의 이야기이기 때문이다.
이 지점에서 많은 초보자들이 MyBatis나 JPA 같은 프레임워크를 먼저 접하는 경우가 있다. 검색을 하다 보면 Spring과 MyBatis를 연동하는 예제, Hibernate를 활용한 ORM 구성 같은 내용이 먼저 눈에 들어오기도 한다. 그러나 이 글에서는 의도적으로 그 단계를 건너뛰고 JDBC의 Statement 방식부터 시작한다. 이유는 단순하다. MyBatis나 iBatis는 결국 JDBC 위에서 동작하는 추상화 계층이다. 내부 구조를 모른 채 편의 도구부터 사용하면, 문제가 발생했을 때 원인을 찾지 못하고 막히는 경우가 반드시 생긴다. 연결이 왜 실패하는지, 쿼리가 어떻게 전달되는지, 결과가 어떤 과정으로 돌아오는지를 코드 수준에서 이해하고 있어야 그 위의 프레임워크도 제대로 다룰 수 있다. Statement 방식은 화려하지 않지만, 데이터베이스와 Java가 어떻게 연결되는지를 가장 명확하게 보여주는 방식이다. 오늘은 Java에서 MySQL 연결 하는 내용을 기준으로 하여 어떻게 연결하는지 알아 보도록 하자.
JDBC란 무엇인가
JDBC는 Java Database Connectivity의 약자로, Java 애플리케이션에서 데이터베이스에 접근하기 위한 표준 API다. 쉽게 말하면 Java 코드와 데이터베이스 사이를 연결해 주는 다리 역할을 한다. Java 진영에서는 데이터베이스의 종류에 상관없이 동일한 방식으로 데이터베이스를 다룰 수 있도록 이 표준 인터페이스를 정의해 두었다.
JDBC가 중요한 이유는 데이터베이스 독립성에 있다. MySQL을 사용하든, PostgreSQL을 사용하든, Oracle을 사용하든 JDBC 인터페이스는 동일하다. 데이터베이스마다 제공하는 드라이버만 교체하면 코드의 나머지 부분은 그대로 유지할 수 있다. 즉, JDBC는 특정 데이터베이스에 종속되지 않고 Java 코드를 작성할 수 있게 해주는 추상화 계층이다.
MySQL과 Java를 연결하려면 MySQL JDBC 드라이버, 즉 Connector/J가 필요하다. 이 드라이버는 Maven 프로젝트라면 pom.xml에 의존성을 추가하는 방식으로 설정하고, 그렇지 않다면 JAR 파일을 직접 프로젝트에 포함시켜야 한다. 드라이버가 제대로 설정되지 않으면 연결 단계에서 바로 오류가 발생하기 때문에, 이 부분은 시작 전에 반드시 확인해야 한다.
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>데이터베이스 연결, Connection 객체 이해하기
JDBC를 통해 데이터베이스와 통신하려면 가장 먼저 연결을 생성해야 한다. 이때 사용하는 것이 DriverManager와 Connection 객체다. Connection은 Java 애플리케이션과 데이터베이스 사이의 세션을 나타내며, 이후의 모든 SQL 실행은 이 연결 객체를 통해 이루어진다.
연결을 생성할 때는 데이터베이스의 URL, 사용자 계정, 비밀번호가 필요하다.
MySQL 기준으로 URL은 다음과 같은 형식을 따른다.
String url = "jdbc:mysql://localhost:3306/데이터베이스명";
String user = "root";
String password = "비밀번호";
Connection conn = DriverManager.getConnection(url, user, password);
여기서 jdbc:mysql://은 JDBC가 MySQL 드라이버를 사용한다는 것을 나타내고, localhost:3306은 데이터베이스 서버의 주소와 포트다. 마지막으로 데이터베이스명은 접속할 스키마를 지정한다. 이 세 가지 중 하나라도 잘못 입력하면 연결이 실패한다.
초보자들이 연결 단계에서 자주 겪는 문제는 대부분 이 정보가 정확하지 않거나, MySQL 서버 자체가 실행 중이지 않은 경우다. 연결 오류가 발생했을 때는 먼저 MySQL 서버가 실행 중인지 확인하고, URL과 계정 정보가 올바른지 순서대로 점검하는 것이 빠른 해결 방법이다. 연결 자체가 성립되지 않으면 이후의 모든 작업은 진행할 수 없기 때문에, 이 단계를 가장 먼저 안정적으로 잡아두는 것이 중요하다.
Statement로 쿼리 실행하기
연결이 완료되면 이제 실제로 SQL을 실행할 수 있다. 이때 사용하는 것이 Statement 객체다. Statement는 Connection 객체로부터 생성하며, SQL 문자열을 그대로 전달해서 실행하는 방식으로 동작한다.
SELECT 쿼리를 실행할 때는 executeQuery() 메서드를 사용하고, 결과는 ResultSet 객체로 반환된다. INSERT, UPDATE, DELETE처럼 데이터를 변경하는 쿼리는 executeUpdate() 메서드를 사용하며, 영향을 받은 행의 수가 정수로 반환된다.
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
System.out.println(rs.getString("name"));
System.out.println(rs.getString("email"));
}ResultSet은 쿼리 결과를 행 단위로 순회할 수 있는 객체다. rs.next()를 호출할 때마다 다음 행으로 이동하며, 더 이상 행이 없으면 false를 반환한다. 각 컬럼의 값은 getString(), getInt(), getDate() 등 데이터 타입에 맞는 메서드로 꺼내올 수 있다.
이 구조를 이해하는 것이 핵심이다. 연결을 만들고, Statement로 쿼리를 실행하고, ResultSet으로 결과를 처리하는 이 흐름은 JDBC의 기본 패턴이며, 이후 어떤 프레임워크를 사용하더라도 내부적으로는 이와 동일한 과정이 반복된다.
PreparedStatement, 보안과 성능을 함께 잡아라
Statement 방식으로 기본 흐름을 이해했다면, 실무에서는 반드시 PreparedStatement로 넘어가야 한다. Statement는 SQL 문자열을 그대로 실행하기 때문에 외부 입력값이 쿼리에 직접 포함될 경우 SQL Injection 공격에 취약하다. 예를 들어 사용자가 입력한 값을 그대로 문자열로 이어 붙여 쿼리를 만들면, 악의적인 입력으로 데이터베이스 전체가 노출되거나 삭제되는 상황이 발생할 수 있다.
PreparedStatement는 SQL과 데이터를 분리해서 처리한다. 쿼리의 구조를 먼저 데이터베이스에 전달하고, 실제 값은 나중에 바인딩하는 방식이다. 이렇게 하면 외부 입력값이 SQL 구조에 영향을 미칠 수 없기 때문에 SQL Injection을 근본적으로 차단할 수 있다.
PreparedStatement pstmt = conn.prepareStatement(
"SELECT * FROM users WHERE id = ?"
);
pstmt.setInt(1, userId);
ResultSet rs = pstmt.executeQuery();?는 나중에 값이 들어올 자리를 표시하는 플레이스홀더다. setInt(), setString() 등의 메서드로 순서에 맞게 값을 바인딩하면 된다. 첫 번째 인자는 플레이스홀더의 순서를 나타내며, 1부터 시작한다.
보안 외에도 성능 측면의 이점이 있다. 동일한 쿼리를 반복 실행할 때 PreparedStatement는 쿼리를 한 번만 컴파일하고 이후에는 값만 바꿔서 재사용하기 때문에 처리 속도가 빠르다. Statement는 실행할 때마다 쿼리 전체를 새로 컴파일하기 때문에 반복 실행 환경에서 비효율적이다. 실무에서 Statement 대신 PreparedStatement를 기본으로 사용하는 이유가 바로 여기에 있다.
연결 종료, 반드시 지켜야 하는 원칙
데이터베이스 연결은 네트워크 소켓과 메모리를 점유하는 자원이다. 사용이 끝난 후 연결을 닫지 않으면 자원이 계속 소모되고, 일정 시간이 지나면 연결 풀이 고갈되거나 데이터베이스 서버에 과부하가 걸리는 상황이 발생한다. 이를 연결 누수라고 하며, 실무에서 성능 저하의 원인으로 자주 지목되는 문제 중 하나다.
JDBC에서 자원을 닫는 순서는 열었던 순서의 역순이다. ResultSet을 먼저 닫고, Statement 또는 PreparedStatement를 닫은 다음, 마지막으로 Connection을 닫아야 한다.
try {
Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users");
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
System.out.println(rs.getString("name"));
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (rs != null) rs.close();
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
}
Java 7 이후부터는 try-with-resources 구문을 사용하면 자원 해제를 자동으로 처리할 수 있어 더욱 안전하다. 자원 해제 코드를 직접 작성할 때 실수로 누락하는 경우를 방지할 수 있기 때문에, 가능하다면 이 방식을 사용하는 것이 좋다. 연결 종료는 선택이 아니라 반드시 지켜야 하는 원칙이며, 이 습관이 잡혀 있어야 이후 커넥션 풀을 다루는 단계에서도 실수 없이 코드를 작성할 수 있다.
나오며
JDBC의 Statement 방식은 화려하지 않다. MyBatis처럼 XML로 쿼리를 관리할 수도 없고, JPA처럼 객체 중심으로 데이터를 다룰 수도 없다. 그러나 이 방식을 제대로 이해하고 나면 그 위에 올라가는 모든 프레임워크가 훨씬 명확하게 보이기 시작한다. MyBatis가 내부적으로 PreparedStatement를 어떻게 활용하는지, Spring의 JdbcTemplate이 연결과 종료를 어떻게 자동화하는지를 이해할 수 있는 기반이 바로 이 단계에서 만들어진다.
지금까지 학습해온 SELECT, INSERT, JOIN 같은 SQL 문법은 Java 안에서도 그대로 사용된다. Java에서 데이터베이스를 다루는 것은 새로운 언어를 배우는 것이 아니라, 알고 있는 SQL을 코드 안으로 가져오는 과정이다. 이 흐름이 익숙해졌다면 이제 한 가지를 직접 해보길 권한다. 지금까지 터미널에서 실행했던 SELECT나 INSERT 쿼리를 Java 코드로 옮겨서 실제로 실행해 보는 것이다. 연결을 만들고, PreparedStatement로 쿼리를 실행하고, ResultSet으로 결과를 출력하는 이 한 사이클을 직접 손으로 짜보는 것만으로도 JDBC의 흐름이 완전히 몸에 익는다.
코드가 동작하는 것을 확인했다면 다음 글에서는 트랜잭션 처리를 함께 알아볼 예정이다. 트랜잭션은 데이터 정합성을 지키는 핵심 개념으로, 실무에서 INSERT와 UPDATE를 함께 처리하거나 오류 발생 시 롤백해야 하는 상황에서 반드시 필요하다. 오늘 잡아둔 연결과 실행의 흐름이 그 단계에서도 그대로 이어지니, 오늘 내용을 충분히 익혀두고 넘어가길 바란다.
Add your first comment to this post