안녕하세요. 저는 백엔드 공부를 하고 있는 학생입니다. 스프링과 MySQL을 이용해 서버를 구축하고 개발공부를 하고 있습니다.
MySQL을 이용해 개발을 하던 중 너무나도 자연스럽게 JPA와 같이 ORM, 추상화된 인테페이스만을 사용해 아무 생각없이 개발하고 있는 저의 모습을 확인할 수 있었습니다. 또한 깊이 있는 공부가 부족하다는 것을 깨닫고 이 글을 작성하게 되었습니다.
여기에서부터는 문장의 간결함을 위해서 높임말은 생략하겠습니다.
DB를 사용하기 위해 DB와 애플리케이션 간 통신을 할 수 있는 수단이다.
DB Connection은 DB Driver와 DB 연결 정보를 담는 URL이 필요하다.
Driver | URL | 설명 |
---|---|---|
oracle.jdbc.driver.OracleDriver | jdbc:oracle:thin:@ipaddress:1521:ORA7 | oracle-DB/thin-Driver |
oracle.jdbc.driver.OracleDriver | jdbc:oracle:oci7:@ipaddress:1521:ORA7 | oracle-DB/OCI-Driver |
com.mysql.jdbc.Driver | jdbc:mysql://ip_address:3306/database_naem | mysql |
sun.jdbc.odbc.JdbcOdbcDriver | jdbc:odbc:database_name | JDBC-ODBC Bridge |
com.informix.jdbc.IfxDriver | jdbc:informix-sqli://ip:port/dbName:informixserver=sid | informix-DB |
2-Tier
3-Tier
자바를 이용해 다양한 종류의 RDBMS와 접속하고 SQL문을 수행하여 처리하고자 할 때 사용되는 표준 SQL 인터페이스 API이다.
JDBC가 없었다면 DB마다 연결방식과 통신규약이 따로 있기 때문에 프로그램을 DB에 연결시키기 위해 해당 DB에 관련된 기술적 내용을 습득해야한다.
또한 DB 변경시 많은 변경 사항이 생긴다.
JDBC API를 사용하는 애플케이션의 구조는 자바 애플리케이션
→ JDBC API
→ JDBC Driver
→ DB
로 구성된다.
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
sql = "SELECT * FROM T_BOARD"
// 1. 드라이버 연결 DB 커넥션 객체를 얻음
connection = DriverManager.getConnection(DBURL, DBUSER, DBPASSWORD);
// 2. 쿼리 수행을 위한 PreparedStatement 객체 생성
pstmt = conn.createStatement();
// 3. executeQuery: 쿼리 실행 후
// ResultSet: DB 레코드 ResultSet에 객체에 담김
rs = pstmt.executeQuery(sql);
} catch (Exception e) {
} finally {
conn.close();
pstmt.close();
rs.close();
}
}
자바에서 DB에 직접 연결해서 처리하는 경우 JDBC Driver를 로드하고 커넥션 객체를 받아와야한다.
매번 사용자가 요청을 할 때마다 드라이버를 로드하고 커넥션 객체를 생성해 연결하고 종료하기 때문에 비효율적이다.
→ 그래서 커넥션 풀을 사용한다.
웹 컨테이너(WAS)가 실행되면서 일정량의 Connection 객체를 미리 만들어서 Pool에 저장한다.
클라이언트 요청이 오면 Connection 객체를 빌려주고 해당 객체 임무가 완료되면 다시 Connection 객체를 반납해 Pool에 저장한다.
Container 구동 시 일정 수의 Connection 객체를 생성한다.
애플리케이션이 DBMS 작업을 수행해야 하면, Connection Pool에서 Connection 객체를 받아와 작업을 진행한다. 그리고 Connection Pool에 Connection 객체를 반납한다.
Connection Pool의 종류로는 “common-dbcp2”, “tomcat-jdbc Pool”, “DrvierManager DataSource”, “HikariCP” 등이 있다.
스프링 부트 2.0 이후부터는 커넥션 풀을 관리하기 위해 HikariCP를 채택하여 사용하고 있다.
Hikari CP가 동작하는 방식
→ DB 드라이버를 통해 커넥션을 조회, 연결, 인증, SQL을 실행하는 시간 등 커넥션 객체를 생성하기 위한 과정을 생략할 수 있게 된다.
→ 클라이언트가 DB에 빠르게 접근 가능하다.
커넥션 풀을 크게 설정하면 메모리 소모가 크다. 하지만 많은 사용자가 대기 시간이 줄어든다.
커넥션 풀을 작게 설정하면 그 만큼 대기시간이 늘어난다. 하지만 적은 메모리를 사용한다.
Connection의 주체는 Thread이기 때문에 Thread와 함께 고려해야한다.
public class Application {
private static final String URL = "jdbc:mysql://localhost:3306/dbcp-test";
private static final String ID = "id";
private static final String PASSWORD = "password";
public static void main(String[] args) {
Instant start = Instant.now();
for (int i = 0; i < 100; i++) {
Connection connection = generateConnection();
doInsert(connection);
}
Instant end = Instant.now();
System.out.println("수행시간: " + Duration.between(start, end).toMillis() + " 밀리초");
}
private static void doInsert(final Connection connection) {
try (connection) {
String sql = "INSERT INTO members(name, password) VALUES(?, ?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, "test");
preparedStatement.setString(2, "1234");
preparedStatement.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
}
}
private static Connection generateConnection() {
Connection connection = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection(URL, ID, PASSWORD);
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
}
100번의 Insert
구문을 실행한다.
→ 한번의 for
루프마다 Connection을 생성하는 것을 볼 수 있다.
위 코드의 동작 시간은 약 3610ms 이다.
public class ConnectionPool {
private static final String URL = "jdbc:mysql://localhost:3306/dbcp-test";
private static final String ID = "id";
private static final String PASSWORD = "password";
private final List<Connection> pool;
private int pointer = 0;
public ConnectionPool() {
this.pool = initializeConnectionPool();
}
private List<Connection> initializeConnectionPool() {
return IntStream.range(0, 10)
.mapToObj(ignored -> generateConnection())
.collect(Collectors.toList());
}
private Connection generateConnection() {
Connection connection = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection(URL, ID, PASSWORD);
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
public Connection getConnection() {
Connection connection = pool.get(pointer % 10);
pointer++;
return connection;
}
}
총 10개의 커넥션을 생성해둔다.
public class ApplicationCp {
public static void main(String[] args) {
ConnectionPool connectionPool = new ConnectionPool();
Instant start = Instant.now();
for (int i = 0; i < 100; i++) {
Connection connection = connectionPool.getConnection();
doInsert(connection);
}
Instant end = Instant.now();
System.out.println("수행시간: " + Duration.between(start, end).toMillis() + " 밀리초");
}
private static void doInsert(final Connection connection) {
try {
String sql = "INSERT INTO members(name, password) VALUES(?, ?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, "test");
preparedStatement.setString(2, "1234");
preparedStatement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
커넥션 풀을 사용하여 똑같이 100번의 Insert
구문을 실행하였다.
약 669ms 가 나왔다. 5.39배 성능이 향상되었다.
데이터베이스 커넥션을 생성하고, 해제하는 과정이 굉장히 큰 오버헤드임을 알게되었다.