eelseungmin

[Spring] DB 연결 이해

by eelseungmin

JDBC

등장 배경

애플리케이션 서버에서 DB에 접근하기 위해서는 보통 다음 과정을 거친다.

 

1. 커넥션 생성: TCP/IP 프로토콜을 사용해서 서버와 DB 간 커넥션 생성

2. SQL 전달: 서버에서 DB로 SQL을 전달한다.

3. 결과 응답: DB가 서버로 SQL 수행 결과를 전달한다.

 

문제는 여기서 발생한다. 시중의 데이터베이스 종류는 수십 개에 달하고 각각의 DB가 위 과정을 구현하는 방식 또한 전부 다르다는 점이다.

 

이 문제를 해결하기 위해 자바에서 DB를 연결할 수 있도록 하는 표준 기술인 JDBC가 등장했다.

JDBC라는 표준 인터페이스가 등장함으로써 각 DB 개발사에서 JDBC 표준에 맞춘 라이브러리를 제공하게 되었고, 개발자는 DB에 맞춰 JDBC 드라이버만 교체하는 방식으로 DB를 바꿀 수 있게 되었다. 

 

JDBC의 DriverManager는 라이브러리에 등록된 DB 드라이버를 관리하는 한편, 커넥션을 획득하는 기능을 제공한다.

 

DriverManager를 사용한 예시 - 저장

@Slf4j
public class MemberRepository {

  public Member save(Member member) throws SQLException {
    String sql = "insert into member(member_id, money) values(?, ?)";
    Connection con = null;
    PreparedStatement pstmt = null;
    try {
      con = getConnection();
      pstmt = con.prepareStatement(sql);
      pstmt.setString(1, member.getMemberId());
      pstmt.setInt(2, member.getMoney());
      pstmt.executeUpdate();
      return member;
    } catch (SQLException e) {
      log.error("db error", e);
      throw e;
    } finally {
      close(con, pstmt, null);
    }
  }

  private void close(Connection con, Statement stmt, ResultSet rs) {
    if (rs != null) {
      try {
        rs.close();
      } catch (SQLException e) {
        log.info("error", e);
      }
    }
    if (stmt != null) {
      try {
        stmt.close();
      } catch (SQLException e) {
        log.info("error", e);
      }
    }
    if (con != null) {
      try {
        con.close();
      } catch (SQLException e) {
        log.info("error", e);
      }
    }
  }

  private Connection getConnection() {
    return DBConnectionUtil.getConnection();
  }
}

여기서 PreparedStatement는 SQL을 전달하는 역할을 하는 Statement의 자식 타입으로, SQL의 '?', 즉 파라미터 바인딩을 가능케 해 준다. SQL Injection 공격을 예방하는 역할도 하므로 PreparedStatement의 사용은 필수적이라 할 수 있다.

 

DriverManager를 사용한 예시 - 조회(feat. ResultSet)

public Member findById(String memberId) throws SQLException {
  String sql = "select * from member where member_id = ?";
  Connection con = null;
  PreparedStatement pstmt = null;
  ResultSet rs = null;
  try {
    con = getConnection();
    pstmt = con.prepareStatement(sql);
    pstmt.setString(1, memberId);
    rs = pstmt.executeQuery();
    if (rs.next()) {
      Member member = new Member();
      member.setMemberId(rs.getString("member_id"));
      member.setMoney(rs.getInt("money"));
      return member;
    } else {
      throw new NoSuchElementException("member not found memberId=" + memberId);
    }
  } catch (SQLException e) {
    log.error("db error", e);
    throw e;
  } finally {
    close(con, pstmt, rs);
  }
}

 

조회할 때 ResultSet이라는 구조를 사용하는데 처음에는 아무것도 가리키지 않는 상태이므로 rs.next()를 통해 커서를 한 번 이동해야 비로소 첫 번째 row를 조회할 수 있다.

 

Connection Pool, DataSource

Connection Pool

커넥션을 매번 새롭게 획득할 경우

과정은 다음과 같다.

1. DB 드라이버를 통해 커넥션 조회

2. TCP/IP 프로토콜을 사용해 커넥션 연결

3. 연결이 완료되면 DB에 인증을 위한  ID, PW를 전달

4. 전달된 정보를 바탕으로 DB 내부적으로 인증을 마친 뒤, DB 세션을 생성하고 완료 응답을 한다.

5. 드라이버가 커넥션 객체를 생성해서 반환

 

문제는 매 DB 요청마다 이러한 과정이 반복된다는 점이고, 고객의 요청에 대한 응답이 느려지는 결과로 이어진다.

 

커넥션 풀을 사용할 경우

위와 같은 문제를 해결하기 위해 미리 일정 개수의 커넥션을 생성해두고 재사용하는 커넥션 풀을 사용한다.

 

아까와 비슷한 과정을 거쳐 일정 개수의 커넥션을 생성해두고 DB 요청이 필요할 때마다 만들어진 커넥션을 꺼내서 사용하기만 하면 된다. 다 사용하면 커넥션을 죽이는 것이 아니라, 살아있는 상태로 풀에 반환해서 재사용할 수 있도록 한다.

 

커넥션 풀은 커넥션의 최대 개수를 제한해서 DB 연결이 무한정 생성되지 않도록 보호하는 역할도 하며, 디폴트 값은 보통 10개이지만 성능 테스트를 통해 개수를 조절함으로써 성능을 튜닝할 수도 있다.

 

스프링부트 2.0부터는 사용성이 좋고 성능이 뛰어난 커넥션 풀 오픈소스인 HikariCP를 기본으로 사용한다.

 

DataSource

이제 DriverManager가 아닌 Connection Pool을 통해 커넥션을 획득하는 방식을 사용한다.

앞서 DB를 JDBC를 사용해 갈아끼웠듯이 커넥션을 획득하는 방법도 추상화가 되어야 한다는 것을 알 수 있을 것이다.

자바에서 제공하는 DataSource를 이용하면 애플리케이션 코드의 변경 없이도 커넥션 획득 기술을 변경할 수 있다. 

 

 

참조

https://inf.run/AomUA

블로그의 정보

eel.log

eelseungmin

활동하기