[Spring DB] JDBC 개발

DaeHoon·2022년 7월 13일
0

1-5 데이터베이스 연결

  • H2를 먼저 실행해두자

ConnectionConst

package hello.jbdc.connection

class ConnectionConst {
  companion object{
    val URL = "jdbc:h2:tcp://localhost/~/test"
    val USERNAME = "sa"
    val PASSWORD = ""
  }
}
  • 데이터베이스 접속에 필요한 기본 정보
  • kotlin에는 static 키워드가 없어, 이를 대체하기 위해 companion object를 사용했다.

DBConnectionUtil

package hello.jbdc.connection

import hello.jbdc.Log
import java.sql.*


class DBConnectionUtil(
) {
  companion object: Log {
    fun getConnection(): Connection {
      try {
        val connection =
          DriverManager.getConnection(ConnectionConst.URL, ConnectionConst.USERNAME, ConnectionConst.PASSWORD)
          logger.info("get connection={}, class={}", connection, connection.javaClass)
          return connection
      } catch (e: SQLException) {
          throw IllegalStateException(e)
      }
    }
  }
}
  • DB에 연결하려면 JDBC가 제공하는 DriverManager.getConnection(..) 를 사용한다.

*companion object?
https://lannstark.tistory.com/141

DBConnectionUtilTest

package hello.jbdc.connection


import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test


class DBConnectionUtilTest {

  @Test
  fun connection(){
    val connection = DBConnectionUtil.getConnection()
    assertThat(connection).isNotNull();
  }
}

실행 결과

DBConnectionUtil - get connection=conn0: url=jdbc:h2:tcp://localhost/~/test  
user=SA, class=class org.h2.jdbc.JdbcConnection

JDBC DriverManager 연결 이해

  • JDBC는 java.sql.Connection 표준 커넥션 인터페이스를 정의한다.
  • H2 데이터베이스 드라이버는 java.sql.Connection 표준 커넥션 인터페이스를 구현한 구현체를 제공한다.

DriverManager 커넥션 요청 흐름

  1. 애플리케이션 로직에서 커넥션이 필요하면 DriverManager.getConnection() 을 호출한다.
  2. DriverManager 는 라이브러리에 등록된 드라이버 목록을 자동으로 인식한다. 이 드라이버들에게 순서대로 다음 정보를 넘겨서 커넥션을 획득할 수 있는지 확인한다.
    • 각각의 드라이버는 URL 정보를 체크해서 본인이 처리할 수 있는 요청인지 확인한다. URL이 "jdbc:h2:tcp://localhost/~/test"일 때 jdbc:h2로 시작하는 것은, h2 데이터베이스에 접근하기 위한 규칙이다
    • 드라이버 목록에 H2가 있으므로 실제 데이터베이스에 연결해서 커넥션을 획득하고 이 커넥션을 클라이언트에 반환한다.
    • 반면에 URL이 jdbc:h2 로 시작했는데 MySQL 드라이버가 먼저 실행되면 이 경우 본인이 처리할 수 없다는 결과를 반환하게 되고, 다음 드라이버에게 순서가 넘어간다.
  3. 이렇게 찾은 커넥션 구현체가 클라이언트에 반환된다.
  • 우리는 H2 데이터베이스 드라이버만 라이브러리에 등록했기 때문에 H2 드라이버가 제공하는 H2 커넥션을 제공받는다. 물론 이 H2 커넥션은 JDBC가 제공하는 java.sql.Connection 인터페이스를 구현하고 있다.
// H2 데이터베이스 드라이버 라이브러리 (Dependency)
runtimeOnly 'com.h2database:h2' //h2-x.x.xxx.jar

1-6 JDBC 개발 - 등록

Member

package hello.jbdc.domain

data class Member(  
var memberId: String,  
var money: Int  
)

MemberRepositoryV0 - 회원 등록

package hello.jbdc.repository

import hello.jbdc.Log
import hello.jbdc.connection.DBConnectionUtil
import hello.jbdc.domain.Member
import java.sql.*


class MemberRepositoryV0(
):Log {
  fun save(member: Member): Member{
    val sql: String = "insert into member(member_id, money) values (?, ?)"

    lateinit var con: Connection
    lateinit var pstmt: PreparedStatement

    try{
      con = getConnection()
      pstmt = con.prepareStatement(sql)
      pstmt.setString(1, member.memberId)
      pstmt.setInt(2, member.money)
      pstmt.executeUpdate()
      return member
    } catch(e: SQLException){
        logger.error("db error", e)
        e.printStackTrace()
        throw e
    } finally {
      close(con, pstmt, null);
    }
  }


  private fun getConnection(): Connection {
    return DBConnectionUtil.getConnection()
  }

  private fun close(con: Connection?, stmt: Statement?, rs: ResultSet?) {
    if (rs != null) {
      try {
        rs.close()
      } catch (e: SQLException) {
        logger.info("error", e)
      }
    }
    if (stmt != null) {
      try {
        stmt.close()
      } catch (e: SQLException) {
        logger.info("error", e)
      }
    }
    if (con != null) {
      try {
        con.close()
      } catch (e: SQLException) {
        logger.info("error", e)
      }
    }
  }
}
  • getConnection(): 이전에 만들어둔 DBConnectionUtil을 통해 데이터베이스 커넥션을 획득한다.
  • sql: 디비에 전달할 SQL
  • con.prepareStatement(sql): 데이터베이스에 전달할 SQL과 파라미터로 전달할 데이터들을 준비한다.
    • sql : insert into member(member_id, money) values(?, ?)"
    • pstmt.setString(1, member.getMemberId()) : SQL의 첫번째 ? 에 값을 지정한다. 문자이므로 setString 을 사용한다.
    • pstmt.setInt(2, member.getMoney()) : SQL의 두번째 ? 에 값을 지정한다. Int 형 숫자이므로 setInt 를 지정한다.
  • pstmt.executeUpdate() : Statement 를 통해 준비된 SQL을 커넥션을 통해 실제 데이터베이스에 전달한다. 참고로 executeUpdate() 은 int 를 반환하는데 영향받은 DB row 수를 반환한다. 여기서는 하나의 row를 등록했으므로 1을 반환한다.

리소스 정리

  • 쿼리를 실행하고 나면 리소스를 정리해야 한다. 여기서는 Connection , PreparedStatement 를 사용했다.
  • 리소스를 정리할 때는 항상 역순으로 해야한다. Connection을 먼저 획득하고 Connection을 통해 PreparedStatement를 만들었기 때문에 리소스를 반환할 때는 PreparedStatement를 먼저 종료하고, 그 다음에 Connection을 종료하면 된다.
  • 여기서 사용하지 않은 ResultSet 은 결과를 조회할 때 사용한다.

MemberRepositoryV0 - 회원 조회

  fun findById(memberId: String): Member{
    val sql = "select * from member where member_id = ?"

    lateinit var con: Connection
    lateinit var pstmt: PreparedStatement
    lateinit var rs: ResultSet

    try{
      con = getConnection()
      pstmt = con.prepareStatement(sql)
      pstmt.setString(1, memberId)

      rs = pstmt.executeQuery()

      if (rs.next()){
        val member = Member(
          memberId = rs.getString("member_id"),
          money = rs.getInt("money")
        )

        return member
      }else{
        throw NoSuchElementException("member not found memberId=" + memberId)
      }
    } catch (e: SQLException){
        logger.error("db error", e)
      throw e
    } finally {
      close(con, pstmt, rs);
    }
  }

MemberRepositoryV0 - 수정, 삭제

  fun update(memberId: String, money: Int){
    val sql = "update member set money=? where member_id=?"

    lateinit var con: Connection
    lateinit var pstmt: PreparedStatement

    try {
      con = getConnection();
      pstmt = con.prepareStatement(sql);
      pstmt.setInt(1, money);
      pstmt.setString(2, memberId);

      val resultSize = pstmt.executeUpdate()
      logger.info("resultSize={}", resultSize)
    } catch (e: SQLException){
        logger.error("db error", e)
        throw e
    } finally {
      close(con, pstmt, null)
    }
  }

  fun delete(memberId: String){
    val sql = "delete from member where member_id=?"

    lateinit var con: Connection
    lateinit var pstmt: PreparedStatement

    try {
      con = getConnection();
      pstmt = con.prepareStatement(sql);
      pstmt.setString(1, memberId);

      pstmt.executeUpdate()

    } catch (e: SQLException){
        logger.error("db error", e)
        throw e
    } finally {
      close(con, pstmt, null)
    }
  }

MemberRepositoryV0Test

package hello.jbdc.repository

import hello.jbdc.Log
import hello.jbdc.domain.Member
import org.assertj.core.api.Assertions.*
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test


internal class MemberRepositoryV0Test: Log{

  private val memberRepositoryV0 = MemberRepositoryV0()

  @Test
  fun crud(){
    // save
    val member = Member("memberV0", 10000);
    memberRepositoryV0.save(member)

    // findById
    val findMember = memberRepositoryV0.findById(member.memberId)
    logger.info("findMember = {}", findMember)
    assertThat(findMember).isEqualTo(member)

    // update : money (10000 -> 20000)
    memberRepositoryV0.update(member.memberId, 20000)
    val updateMember = memberRepositoryV0.findById(member.memberId)
    assertThat(updateMember.money).isEqualTo(20000)

    // delete
    memberRepositoryV0.delete(member.memberId)

    assertThatThrownBy { memberRepositoryV0.findById(member.memberId) }
      .isInstanceOf(NoSuchElementException::class.java)
  }
}
profile
평범한 백엔드 개발자

0개의 댓글