제육's 휘발성 코딩
article thumbnail
반응형

프로젝트 생성

image

Build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    //테스트에서 lombok 사용
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
}
  • 테스트 환경에서 롬복을 사용하기 위해 적용

 

H2 데이터베이스 설정

H2 데이터베이스 버전은 1.4.200을 사용했다. 맥환경에서는 설치 후 bin 폴더에서 ./h2.sh를 통해 실행

https://sasca37.tistory.com/13?category=1218302 를 참고하자.

  • 최초 접속시 jdbc:h2:~/jdbc1 후 루트 폴더에 ~/jdbc1.mv.db 파일 생성 확인
  • 이후 접속시 jdbc:h2:tcp://localhost/~/jdbc1

테이블 생성하기

drop table member if exists cascade;
  create table member (
    member_id varchar(10),
    money integer not null default 0,
    primary key (member_id)
  );
insert into member(member_id, money) values ('hi1',10000);
insert into member(member_id, money) values ('hi2',20000);

 

JDBC 이해

JDBC 등장 이유

애플리케이션을 개발할 때 중요한 데이터는 대부분 데이터베이스에 보관한다.

image

  • 커넥션 연결 : 주로 TCP/IP를 사용해서 커넥션 연결
  • SQL 전달 : 애플리케이션 서버는 DB가 이해할 수 있는 SQL을 전달
  • 결과 응답 : DB는 전달된 SQL을 수행하고 그 결과를 응답

과거에는 데이터베이스 별 커넥션 연결, SQL 전달, 결과 응답 등을 새로 학습해야 했다. 이 문제를 해결하기 위해 JDBC라는 자바 표준 인터페이스가 등장했다.

 

JDBC 표준 인터페이스

JDBC는 자바에서 데이터베이스에 접속하도록 지원하는 API다. 이 API를 통해 각 데이터베이스 별 연결해주는 드라이버를 사용하여 사용할 수 있다.

image

  • java.sql.Connection : 연결
  • java.sql.Statement : SQL을 담은 내용
  • java.sql.ResultSet : SQL 요청 응답

 

MySQL 드라이버

image

  • 다음과 같이 데이터베이스 별 드라이버를 사용해서 개발자는 JDBC 표준 인터페이스에만 의존해서 사용하게 됐다. 단 SQL 문법은 각각 다른 점이 있다. 이 부분은 직접 코드를 수정해야한다.

 

JDBC와 최신 데이터 접근 기술

imageimage

  • JDBC를 편리하게 사용하게 지원해주는 SQL MapperORM 기술이 존재한다.

 

데이터베이스 연결

ConnectionConst

package hello.jdbc.connection;

public abstract class ConnectionConst {
  public static final String URL = "jdbc:h2:tcp://localhost/~/jdbc1";
  public static final String USERNAME = "sa";
  public static final String PASSWORD = "";
}
  • 데이터베이스에 접속하는 정보를 상수로 만들어서 넣자.

 

DBConnectionUtil

package hello.jdbc.connection;

import lombok.extern.slf4j.Slf4j;

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

import static hello.jdbc.connection.ConnectionConst.*;

@Slf4j
public class DBConnectionUtil {

  public static Connection getConnection() {
    try {
      Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
      log.info("get connection={}, class={}", connection, connection.getClass());
      return connection;
    } catch (SQLException e) {
      throw new IllegalStateException(e);
    }
  }
}
  • 데이터베이스에 연결하려면 JDBC가 제공하는 DriverManger.getConnection() 을 사용하면 된다. 이렇게 구현하면 라이브러리에 있는 데이터베이스 드라이버를 찾아서 해당 드라이버가 제공하는 커넥션을 반환해준다.

DBConnectionUtilTest

package hello.jdbc.connection;


import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
import static org.assertj.core.api.Assertions.*;

@Slf4j
class DBConnectionUtilTest {

  @Test
  void connection() {
    Connection connection = DBConnectionUtil.getConnection();
    assertThat(connection).isNotNull();
  }
}
  • H2 서버가 켜져있다면 getConnection을 통해 정상적으로 H2 드라이버를 가져오는 것을 확인할 수 있다.

 

JDBC DriverManager 이해

image

  • 라이브러리에 등록된 DB 드라이버들을 관리하고 커넥션 요청이 왔을 때 드라이버들을 탐색하며 맞는 커넥션을 반환하는 기능을 제공한다.

 

JDBC 개발 - 등록

Member

package hello.jdbc.domain;

import lombok.Data;

@Data
public class Member {

    private String memberId;
    private int money;

    public Member() {}

    public Member(String memberId, int money) {
        this.memberId = memberId;
        this.money = money;
    }
}

MemberRepositoryV0 - 회원등록

package hello.jdbc.repository;

import hello.jdbc.connection.DBConnectionUtil;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;

import java.sql.*;


@Slf4j
public class MemberRepositoryV0 {

    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();
    }
}
  • con.prepareStatement(sql) : 데이터베이스에 전달할 SQL과 파라미터로 전달할 데이터를 준비하고 ?에 들어갈 타입에 맞춰서 파라미터를 바인딩 해준다 (파라미터 바인딩으로 SQL Insection 방지).
  • pstmt.executeUpdate()Statement를 통해 준비된 SQL을 커넥션을 통해 실제 데이터베이스에 전달한다. executeUpdate는 int를 반환하는데 영향받은 DB row 수를 반환한다.

MemberRepositoryV0Test - 회원등록

package hello.jdbc.repository;

import hello.jdbc.domain.Member;
import org.junit.jupiter.api.Test;
import java.sql.SQLException;


class MemberRepositoryV0Test {

    MemberRepositoryV0 repository = new MemberRepositoryV0();

    @Test
    void crud() throws SQLException {
        Member member = new Member("memberV0", 10000);
        repository.save(member);
    }
}

 

JDBC 개발 - 조회

MemberRepositoryV0 - 회원 조회 추가

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);
  }
}
  • 조회를 위한 findById를 추가하자. executeQuery()는 결과를 ResultSet에 담아서 반환한다.
  • 데이터를 등록, 수정, 삭제처럼 데이터를 변경하는 쿼리는 executeUpdate()를 사용하면 된다.

MemberRepositoryV0Test - 회원 조회 추가

@Test
void crud() throws SQLException {
  // save
  Member member = new Member("memberV0", 10000);
  repository.save(member);

  // findById
  Member findMember = repository.findById(member.getMemberId());
  log.info("findMember={}", findMember);
  assertThat(findMember).isEqualTo(member);
}

ResultSet

image

  • ResultSet은 내부에 있는 커서를 이동해서 데이터를 탐색한다. 최초의 커서는 데이터를 가리키고 있지 않기 때문에 한 번은 호출해야 데이터를 조회할 수 있다.

 

JDBC 개발 - 수정, 삭제

수정과 삭제도 마찬가지로 executeUpdate()를 사용하면 된다.

MemberRepositoryV0 - 회원 수정 추가

public void update(String memberId, int money) throws SQLException {
  String sql = "update member set money=? where member_id=?";
  Connection con = null;
  PreparedStatement pstmt = null;
  try {
    con = getConnection();
    pstmt = con.prepareStatement(sql);
    pstmt.setInt(1, money);
    pstmt.setString(2, memberId);
    int resultSize = pstmt.executeUpdate();
    log.info("resultSize={}", resultSize);
  } catch (SQLException e) {
    log.error("db error", e);
    throw e;
  } finally {
    close(con, pstmt, null);
  } 
}

MemberRepositoryV0Test - 회원 수정 추가

@Test
void crud() throws SQLException {
  // save
  Member member = new Member("memberV0", 10000);
  repository.save(member);

  // findById
  Member findMember = repository.findById(member.getMemberId());
  log.info("findMember={}", findMember);
  assertThat(findMember).isEqualTo(member);

  // update
  repository.update(member.getMemberId(), 20000);
  Member updatedMember = repository.findById(member.getMemberId());
  assertThat(updatedMember.getMoney()).isEqualTo(20000);
}

MemberRepositoryV0 - 회원 삭제 추가

public void delete(String memberId) throws SQLException {
  String sql = "delete from member where member_id=?";
  Connection con = null;
  PreparedStatement pstmt = null;
  try {
    con = getConnection();
    pstmt = con.prepareStatement(sql);
    pstmt.setString(1, memberId);
    pstmt.executeUpdate();
  } catch (SQLException e) {
    log.error("db error", e);
    throw e;
  } finally {
    close(con, pstmt, null);
  }
}

MemberRepositoryV0Test - 회원 삭제 추가

@Test
void crud() throws SQLException {
  // save
  Member member = new Member("memberV0", 10000);
  repository.save(member);

  // findById
  Member findMember = repository.findById(member.getMemberId());
  log.info("findMember={}", findMember);
  assertThat(findMember).isEqualTo(member);

  // update
  repository.update(member.getMemberId(), 20000);
  Member updatedMember = repository.findById(member.getMemberId());
  assertThat(updatedMember.getMoney()).isEqualTo(20000);

  // delete
  repository.delete(member.getMemberId());
  assertThatThrownBy(() -> repository.findById(member.getMemberId()))
    .isInstanceOf(NoSuchElementException.class);
}

 


본 포스팅은 인프런 - 김영한님의 '스프링 DB 1편 - 데이터 접근 핵심 원리'를 정리한 내용입니다.

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^