본문 바로가기

Java

JDBC 성능 최적화

💡 성능 향상 기법

  1. PreparedStatement 사용의 장점
  2. 연결 풀 (Connection Pool) 사용
    • 데이터 소스 (Data Source)의 개념
    • 연결 풀의 장점(Connection Pool)
  3. 캐싱 전략

 

1. PreparedStatement 사용의 장점

PreparedStatement는 SQL 쿼리를 미리 컴파일하고, 동일한 쿼리를 반복해서 실행할 때 효율적으로 사용할 수 있는 인터페이스이다. 이는 성능과 보안 측면에서 많은 장점을 제공한다.

  • 성능 향상
    • 쿼리 컴파일: SQL 쿼리를 미리 컴파일하여, 쿼리를 여러 번 실행할 때 컴파일 시간을 절약할 수 있다.
    • 쿼리 계획 재사용: 동일한 쿼리를 반복적으로 실행할 때, 쿼리 계획을 재사용하여 실행 시간을 단축할 수 있다.
  • 보안 향상
    • SQL 인젝션 방지: 쿼리와 데이터가 분리되어 있어 SQL 인젝션 공격을 방지할 수 있다.

PreparedStatement는 SQL 쿼리를 미리 컴파일하고, 동일한 쿼리를 반복해서 실행할 때 효율적으로 사용할 수 있는 인터페이스이다. 이는 성능과 보안 측면에서 많은 장점을 제공한다.

  • 성능 향상
    • 쿼리 컴파일: SQL 쿼리를 미리 컴파일하여, 쿼리를 여러 번 실행할 때 컴파일 시간을 절약할 수 있다.
    • 쿼리 계획 재사용: 동일한 쿼리를 반복적으로 실행할 때, 쿼리 계획을 재사용하여 실행 시간을 단축할 수 있다.
  • 보안 향상
    • SQL 인젝션 방지: 쿼리와 데이터가 분리되어 있어 SQL 인젝션 공격을 방지할 수 있다.

 

 

SQLInjectionExample
package ch01;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Scanner;

public class SQLInjectionExample {
	

	public static void main(String[] args) {
		
		try (Connection conn = DBConnectionManager.getConnection()){
			
			Scanner scanner = new Scanner(System.in);
			System.out.print("사용자 입름을 입력하세요 : ");
			String username = scanner.nextLine();
			
			// 취약한 SQL 쿼리 작성해보기(SQL 인젝션이 가능)
			String query = " SELECT * FROM user WHERE name = " + username + " ";
			
			try (Statement stmt = conn.createStatement()){
			   ResultSet resultSet	=  stmt.executeQuery(query);
			   
			   while(resultSet.next()) {
				   System.out.println("사용자 ID : " + resultSet.getInt("id"));
				   System.out.println("사용자 이름 : " + resultSet.getString("name"));
				   System.out.println("사용자 이메일 : " + resultSet.getString("email"));
			   }
			} catch (Exception e) {
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	} // end of main 
}

 

 

 

SQLInjection 예방코드
package ch01;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Scanner;

public class SQLInjectionExample {
	

	public static void main(String[] args) {
		
		try (Connection conn = DBConnectionManager.getConnection()){
			
			Scanner scanner = new Scanner(System.in);
			System.out.print("사용자 입름을 입력하세요 : ");
			String username = scanner.nextLine();
			
			// 취약한 SQL 쿼리 작성해보기(SQL 인젝션이 가능)
			String query = " SELECT * FROM user WHERE name = " + username + " ";
			
			String query2 = " SELECT * FROM user WHERE name = ? ";
			try (Statement stmt = conn.createStatement(); 
				
				// Statement 사용과 인젝션 공격 확인 	
				// ResultSet resultSet	=  stmt.executeQuery(query);
				
				// SQL 인젝션 방지를 위한 PreparedStatement의 사용
				PreparedStatement pstmt = conn.prepareStatement(query2)){
				pstmt.setString(1, username);
				ResultSet resultSet	=  pstmt.executeQuery();
			   
			   while(resultSet.next()) {
				   System.out.println("사용자 ID : " + resultSet.getInt("id"));
				   System.out.println("사용자 이름 : " + resultSet.getString("name"));
				   System.out.println("사용자 이메일 : " + resultSet.getString("email"));
			   }
				
			} catch (Exception e) {
				e.printStackTrace();
			}
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	} // end of main 
}

 

 

 

 

 

 

2. 연결 풀 (Connection Pool)과 데이터 소스 (Data Source)

데이터베이스 연결의 생애주기

1. 애플리케이션 로직에서 데이터베이스 연결 요청
2. 데이터베이스 드라이버 초기화 및 연결 설정
3. TCP/IP 연결 설정
4. 사용자 인증 및 세션 생성
5. 커넥션 생성 완료
6. 데이터베이스 연결 사용 및 종료

일반적으로 데이터베이스 연결은 비용이 큰 작업이다. 데이터베이스 서버와의 연결을 맺고 끊는 과정은 시간이 많이 소요되며, 데이터베이스 서버의 리소스도 소비된다. 그렇기 때문에 매번 데이터베이스 연결을 필요로 하는 요청이 발생할 때마다 새로운 연결을 생성하는 것은 효율적이지 않다.

 

 

 

성능 최적화의 여러 방법중 하나로 아래와 같은 기술들을 적용할 수 있다.

 

💡 커넥션 풀(Connection Pool)과 데이터 소스 (Data Source)는 데이터베이스 연결 관리를 효율적으로 하기 위해 사용하는 개념이다.

 

 

연결 풀 (Connection Pool)

  • 정의: 데이터베이스 연결 풀은 일정 수의 데이터베이스 연결을 미리 생성해두고, 애플리케이션에서 필요할 때마다 이 연결을 재사용하는 기술이다.
  • 목적: 데이터베이스 연결을 효율적으로 관리하여 성능을 향상시키고, 데이터베이스 서버의 부하를 줄이다.
  • 작동 원리: 애플리케이션이 데이터베이스 연결을 요청하면, 연결 풀에서 사용 가능한 연결을 반환한다. 사용이 끝난 연결은 폐기되지 않고 다시 연결 풀에 반환되어 재사용된다.

 

💡 핵심 정리

  • DB 드라이버가 아닌 커넥션 풀에서 커넥션을 가져온다.
  • 커넥션 사용 후 재사용할 수 있도록 커넥션 풀에살아 있는 상태로 반환 한다

커넥션 생성에 필요한 6가지 단계들이 모두 처리된 상태므로, 언제든지 즉시 SQL 문을 데이터베이스로 전달할 수 있어서 사용자 응답 속도가 빨라진다.

 

서버당 최대 커넥션 개수를 제한할 수 있어 DB에 무한정 연결이 생성되는 것을 막아 데이터베이스를 보호하는 효과도 존재한다.

 

 

 

데이터 소스 (Data Source)

  • 정의: 데이터 소스는 데이터베이스 연결 정보를 캡슐화한 객체로, 애플리케이션이 데이터베이스와 상호작용할 때 사용된다. 일반적으로 데이터 소스는 연결 풀을 포함하여 데이터베이스 연결을 관리한다.
  • 목적: 데이터 소스는 데이터베이스 연결 설정을 단순화하고, 연결 풀을 통해 효율적인 연결 관리를 제공한다.
  • 사용 방법: 데이터 소스를 설정하고, 애플리케이션에서 데이터 소스를 통해 데이터베이스 연결을 요청한다.

 

💡 핵심 정리

데이터 소스 (Data Source)는 추상화된 개념으로 설계되어 있으며, 데이터베이스 연결을 관리하고 사용할 수 있는 수준의 인터페이스를 제공한다. 이 추상화된 개념을 구현한 다양한 라이브러리를 사용하여, 프로젝트의 성격이나 정책에 맞게 선택하고 쉽게 사용할 수 있다.

 

 

 

연결 풀과 데이터 소스의 관계

  • 통합 관리: 데이터 소스는 연결 풀을 사용하여 데이터베이스 연결을 관리한다. 데이터 소스를 통해 데이터베이스 연결을 요청하면, 내부적으로 연결 풀에서 관리하는 연결이 반환된다.
  • 연결 추상화: 데이터 소스는 연결 풀을 포함한 다양한 연결 관리 방식을 추상화하여 제공한다. 이를 통해 애플리케이션 개발자는 데이터 소스를 통해 쉽게 데이터베이스 연결을 사용할 수 있다.

 

라이브러리를 프로젝트에 추가 방법

  • Maven을 사용하는 경우 또는 Gradle 빌드 툴을 사용할 수 있다.
  • 라이브러리를 직접 추가하는 방법

https://mvnrepository.com/artifact/com.zaxxer/HikariCP

https://mvnrepository.com/artifact/mysql/mysql-connector-java/8.0.21

https://mvnrepository.com/artifact/org.slf4j/slf4j-api/2.0.0-alpha5

https://mvnrepository.com/artifact/org.slf4j/slf4j-simple/2.0.0

 

 

package com.tenco.quiz.ver3;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

/**
 * 커넥션 풀을 활용하는 예제로 수정해 보자. HikariCP-5.1.0.jar lib 설정
 */
public class DBConnectionManager {

	private static HikariDataSource dataSource;

	private static final String URL = "jdbc:mysql://localhost:3306/quizdb?serverTimezone=Asia/Seoul";
	private static final String USER = "root";
	private static final String PASSWORD = "asd123";

	static {
		// HikariCP 를 사용하기 위한 설정이 필요 하다.
		// HikariConfig --> 제공해줘서 이 클래스를 활용해서 설정을 상세히 할 수 있다.
		HikariConfig config = new HikariConfig();
		config.setJdbcUrl(URL);
		config.setUsername(USER);
		config.setPassword(PASSWORD);
		config.setMaximumPoolSize(10); // 최대 연결 수 설정 10

		dataSource = new HikariDataSource(config);

	}

	public static Connection getConnection() throws SQLException {
		return dataSource.getConnection();
	}

	// 테스트 코드 확인
	public static void main(String[] args) {
		
		try {
			Connection conn = DBConnectionManager.getConnection();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	} // end of main

//  기본 JDBC 드라이버 사용 버전 
//	public static Connection getConnection() throws SQLException {
//		return DriverManager.getConnection(URL, USER, PASSWORD);
//	}

} // end of class
package com.tenco.quiz.ver3;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Scanner;

public class QuizGame2 {
	
	public static void main(String[] args) {
		
		try (Connection conn = DBConnectionManager.getConnection();
				Scanner scanner = new Scanner(System.in)) {
			
			while(true) {
				System.out.println();
				System.out.println("----------------------------------------");
				System.out.println("1. 퀴즈 문제 추가");
				System.out.println("2. 퀴즈 문제 조회");
				System.out.println("3. 퀴즈 게임 시작");
				System.out.println("4. 종료");
				System.out.print("옵션을 선택 하세요: ");
				
				int choice = scanner.nextInt(); // 블로킹 
				
				if(choice == 1) {
					addQuizQuestion(conn, scanner);
				} else if(choice == 2) {
					viewQuizQuestion(conn);
				} else if(choice == 3) {
					playQuizGame(conn, scanner);
				} else if(choice == 4) {
					System.out.println("프로그램을 종료 합니다");
					break;
				} else  {
					System.out.println("잘못된 선택입니다. 다시 시도하세요.");
				}
			}
			
		} catch (Exception e) {
			
		}

	} // end of main

	private static void playQuizGame(Connection conn, Scanner scanner) {
		String sql = " select * from quiz order by rand() limit 1  ";
		
		try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
			ResultSet rs = pstmt.executeQuery();
			
			if(rs.next()) {
				
				String question = rs.getString("question");
				String answer = rs.getString("answer");
				
				System.out.println("퀴즈 문제 : " + question);
				// 버그처리 
				scanner.nextLine();
				System.out.print("당신에 답: ");
				String userAnswer = scanner.nextLine();
				
				if(userAnswer.equalsIgnoreCase(answer)) {
					System.out.println("정답입니다! 점수를 얻었습니다.");
				} else {
					System.out.println("오답입니다!");
					System.out.println("퀴즈 정답은 -->  " + answer);
				}
			} else {
				System.out.println("죄송합니다 아직 퀴즈 문제를 만들고 있습니다.");
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	private static void viewQuizQuestion(Connection conn) {
		String sql = " select * from quiz  ";
	 	try (PreparedStatement pstmt = conn.prepareStatement(sql)){
	 		ResultSet resultSet =  pstmt.executeQuery();
	 		while(resultSet.next()) {
	 			System.out.println("문제 ID : " + resultSet.getInt("id"));
	 			System.out.println("문제 : " + resultSet.getString("question"));
	 			System.out.println("정답 : " + resultSet.getString("answer"));
	 			if(!resultSet.isLast()) {
	 				System.out.println("----------------------------------------");
	 			}
	 		}
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	private static void addQuizQuestion(Connection conn, Scanner scanner) {
		
		System.out.println("퀴즈 문제를 입력하세요: ");
		scanner.nextLine();
		String question = scanner.nextLine();
		System.out.println("퀴즈 정답을 입력하세요: ");
		String answer = scanner.nextLine();
		
		String sql = " insert into quiz(question, answer) values(?, ?) ";
		
		try (PreparedStatement pstmt = conn.prepareStatement(sql)){
			pstmt.setString(1, question);
			pstmt.setString(2, answer);
			int rowsInsertedCount = pstmt.executeUpdate();
			System.out.println("추가된 행의 수 : " + rowsInsertedCount);
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
}  // end of class
728x90

'Java' 카테고리의 다른 글

JavaScript 게시판 만들기 - board-list 기능 만들기  (0) 2024.11.12
JDBC에서의 예외 처리  (1) 2024.06.18
JDBC 트랜잭션 관리와 배치 처리  (0) 2024.06.13
래퍼 클래스 ( Wrapper Class )  (0) 2024.06.12
JDBC 기본 사용법  (0) 2024.06.11