JDBC

꾸준하게 달리기~·2023년 8월 7일
0

스프링 + 자바

목록 보기
11/20
post-thumbnail
post-custom-banner

들어가기 앞서

(해당 포스팅은, H2, Spring 구성 등등의 기본적인 내용은 생략했습니다!)

MyBatis, JPA, Spring data JPA, JDBC Template, Querydsl 등 DB 관련 기술 스택들이 많이 있다.

대다수 실무에서는 Spring data JPA를 사용하고, 나도 프로젝트를 할 때는 항상 Spring data JPA를 사용했다.

왜 그럴까?

ORM 기술, 객체 관계 매핑 기술으로 인해 직관적이고 굉장히 편하다.
백문이 불여일타, 바로 아래 코드를 보자.

@Entity
public class Member extends Auditable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long memberId;

    @Column(nullable = false, unique = true)
    private String name;

}

위의 Member 클래스에서,
@Entity,
@Id,
@GeneratedValue(strategy = GenerationType.IDENTITY),
@Column
이러한 애너테이션을 보면, 직관적으로 해석할 수 있다.

@Entity를 보면 어.. Member 클래스가 하나의 엔터티구나!
@Id를 보면 오우 그리고 아이디, 즉 PK는 memberId 변수이구나!
@Column을 보면, 어.. 이거는 그럼 하나의 열을 의미하겠구나, 즉 테이블에서 하나의 필드변수가 되겠네..!
이런식으로 직관적으로 이해 가능하다.

그리고
테이블을 떠나서도, Spring data JPA의 기능을 통해

public interface BoardRepository extends JpaRepository<Board, Long> {

    List<Board> findAllByPick(int pickNum);
    @Query("SELECT b FROM Board b JOIN b.boardTag bt JOIN bt.tag t WHERE t.tagName LIKE %:tagName%")
    Page<Board> findByTagNameContaining(@Param("tagName") String tagName, Pageable pageable);
}

이와 같이 쿼리문 작성 + findBy를 통해 DB에서 조회할 수도 있다.

물론 Spring data JPA가 아닌 다른 기술 스택들도, JDBC보다는 더 편하게 이해할 수 있도록 조성되어있다.
그럼 처음부터 이렇게 간단했을까?

당연하게도 아니다..

내가 @Entity 애너테이션을 하나 붙인다고
어떻게 DB에서 테이블이 되도록 만들것이며,
내가 @Column 애너테이션을 하나 붙인다고
어떻게 테이블의 열이 될 수 있을것인가?

이렇게 편하게 DB를 사용하게 해주는 코드들은,
JPA 기반으로 만들어져 있고,
JPA는 JDBC API를 기반으로 만들어진 내용이다.

즉, JDBC API가 관계형 DB기반 API 이므로,
이해해두면 대부분의 RDB 툴들을 이해하기가 쉽다.
코틀린이 자바 기반으로 만들어진 언어이므로, 자바를 잘 알고 있다면 코틀린 사용에 대해 이해를 쉽게 할 수 있다 라는 정도로 이해하면 된다!




JDBC (Java Database Connectivity)

우리는 객체지향 프로그래밍을 한다.
온 세상이 객체인것처럼, 코드와 DB를 연결해주는 객체 또한 존재해야 한다. 해당 객체가 커넥션, Connection이다.

DB와 소스코드는 위의 사진과 같이 커넥션 객체를 통해 연결된다.
그런데, Oracle, MySQL, MsSQL 등 각각의 DB마다
위 사진의 과정의 코드가 다르다.

그렇기에 개발자들은 DB마다 연결과 커넥션 객체 사용법을 알고 있어야 했다.

이러한 문제때문에, JDBC라는 표준 DB API를 사용한다.
그리고, 그러한 JDBC API를 기반으로 각각의 DB 회사가 제공하는 라이브러리를 통해 사용한다.



h2 설정, 테이블 생성.

H2 database에서,

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);

위 명령을 사용하여 테이블을 만들고 데이터를 생성했다.
(JPA에서는 직접 테이블 생성할 필요 없이 @Entity면 된다!)

또한,
기존엔 설정파일에 넣어놓았던 DB 위치를
공부 용도이기 때문에 간단하게 클래스에 넣어놓았고,
JDBC가 DB에 접근하기 위해서 방금 위에서 말한 Connection 클래스의 객체를 얻는 매서드도 만들었다.

(JPA에서는 yml파일 + 커넥션 객체는 알아서 생성하고 닫아줌)

public abstract class ConnectionConst {

    //간단한 CRUD와 JDBC를 설명하기 위해, 어플리케이션.yml 설정파일이 아닌
    //클래스에 변수를 생성해서 연결시켜주기 위해 작성한 클래스입니다!
    private static final String URL = "jdbc:h2:tcp://localhost/~/test";
    private static final String USERNAME = "sa";
    private static final String PASSWORD = "";
}

-----------------------------------------------------------------

@Slf4j
public class DBConnectionUtil {
    public static Connection getConnection() {
        try {
            Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
            //아까 만든 변수들로 커넥션 객체 얻기 (DriverManager객체가 DB 드라이버 관리, 커넥션 획득 기능)
            log.info("get connection={}, class={}", connection, connection.getClass());
            //잘 얻어졌는지 확인하기
            return connection;
            //해당 객체 반환
        } catch (SQLException e) { //예외처리
            throw new IllegalStateException(e);
        }

    }
}


이렇게 만들어준 getConnection()을 테스트 하기 위해 다음과 같은 테스트를 만들었고, 테스트는 성공했다.

@Slf4j
public class DBConnectionUtilTest {

    @Test
    void connectionTest() {
        Connection connection = DBConnectionUtil.getConnection();
        Assertions.assertThat(connection).isNotNull();
        log.info("커넥션 객체 얻는 테스트 완료, 각각의 DB마다 커넥션 클래스명은 다르다. 여기서는 org.h2.jdbc.JdbcConnection");
    }
}

이제 해당 Connection 객체로 DB와의 상호작용을 수행하면 된다.

커넥션을 얻기 위한 설정, 직접 테이블 생성 등 내가 작성한 코드는, Spring data JPA로 넘어가며 전부 자동화된다.




JDBC CRUD

아래 class와 위에서 설정해준 커넥션을 기반으로,
JDBC를 이용하여 CRUD를 수행하는 예제를 설명하겠다.

@Data
public class Member {

    private String memberId;
    private int money;

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

    public Member() {
        
    }
}

Spring data JPA에서는 아래와 같이 직접 sql문을 만들고 커넥션을 열어서 바인딩하여 사용할 수 있다.

아래의 MemberRepository에는, CRUD의 모든 기능이 들어가 있다.
주석에 각각의 코드에 대한 설명 또한 있다!

@Slf4j
public class MemberRepository {

    public Member save(Member member) throws SQLException{ //생성
        
        String sql = "insert into member(member_id, money) values (?, ?)";
        //물음표만 바인딩해줄 String sql

        Connection con = null; //커넥션
        PreparedStatement statement = null; //DB에 쿼리 날아갈 객체

        try {
            con = getConnection();
            //DBConnectionUtil 클래스에서 가져온 커넥션 객체 얻기.
            //해당 커넥션은, 정해진 URL, USERNAME, PASSWORD를 기반으로 DriverManager이 얻어온 H2 커넥션 객체이다.

            statement = con.prepareStatement(sql);
            //해당 sql문을 커넥션에 장착하여 PreparedStatement에 넘김
            
            statement.setString(1, member.getMemberId());
            statement.setInt(2, member.getMoney());
            //각각 PreparedStatement 객체에 sql문의 (?, ?) 에 바인딩해줌
            
            statement.executeUpdate();
            //statement를 DB에 날리기

            return member;
        }
        catch (SQLException e) {
            log.error("db 에러", e);
            throw e;
        }
        finally {
            close(con, statement, null);
            //커넥션과 PreparedStatement 객체는 닫아줘야 한다. 닫아주지 않으면 계속해서 열려있게됨.
            //그렇게 된다면 커넥션 부족으로 장애 발생 가능.
        }
    }

    public Member findById(String memberId) throws SQLException { //조회
        String sql = "select * from member where member_id = ?";

        Connection con = null;
        PreparedStatement statement = null;
        ResultSet rs = null;

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

            rs = statement.executeQuery();
            //위의 sql쿼리문을 실행했을때의 결과, 말 그대로 ResultSet, 결과 Set

            if(rs.next()) { //결과 rs에 데이터가 있다면 찾아온 member 객체를 반환해준다 + next를 사용해준 횟수에 따라 해당 횟수 번째의 객체 반환
                Member member = new Member();
                member.setMemberId(rs.getString("member_id"));
                member.setMoney(rs.getInt("money"));
                return member;
            }
            else {
                throw new NoSuchElementException("너가 준 " + memberId + " 이 멤버아이디 DB에 없어!");
            }
        }
        catch (SQLException e) {
            log.error("db error, e");
            throw e;
        }
        finally {
            close(con, statement, rs);
        }
    }


    public void update(String memberId, int money) throws SQLException { //수정
        String sql = "update member set money=? where member_id=?";

        Connection con = null;
        PreparedStatement statement = null;

        try {
            con = getConnection();
            statement = con.prepareStatement(sql);
            statement.setInt(1, money);
            statement.setString(2, memberId);
            int resultSize = statement.executeUpdate(); //수행해준 행의 수, 즉 1이 나와야함
            log.info("resultSize={}", resultSize);
        }
        catch (SQLException e) {
            log.error("db 에러", e);
            throw e;
        }
        finally {
            close(con, statement, null);
        }
    }


    public void delete(String memberId) throws SQLException {
        String sql = "delete member where member_id=?";

        Connection con = null;
        PreparedStatement statement = null;

        try {
            con = getConnection();
            statement = con.prepareStatement(sql);
            statement.setString(1, memberId);
            statement.executeUpdate();
        }
        catch (SQLException e) {
            log.error("db 에러", e);
            throw e;
        }
        finally {
            close(con, statement, null);
        }
    }



    //여기 아래부턴 private 매서드
    private void close(Connection con, PreparedStatement statement, ResultSet rs) { 
        //열어준 객체들을 닫아줄 매서드

        if(rs != null) {
            try {
                rs.close();
            }
            catch (SQLException e) {
                log.info("error", e);
            }
        }


        if(statement != null) {
            try {
                statement.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();
    }

}

해당 코드를 작성하고, 아래의 테스트코드를 작성했다.

@Slf4j
class MemberRepositoryTest {

    MemberRepository repository = new MemberRepository();

    @Test
    void cruTest() throws SQLException {

        //저장
        Member member = new Member("1", 10000);
        repository.save(member);

        //조회
        Member findMember = repository.findById(member.getMemberId());
        log.info("찾은 멤버={}", findMember);
        Assertions.assertThat(findMember).isEqualTo(member); //@Data의 toString때문에 통과 가능 (인스턴스 주소 비교 x)

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

    @Test
    void deleteTest() throws SQLException {
    	//삭제
        repository.delete("1");
    }
}

이제 테스트코드를 실행하기 전, 설명을 잘 듣도록 하자.
cruTest를 통해
memberId 1, money 10000 인 member를 저장, (save)
memberId 1인 member를 조회, (findById)
memberId 1인 member의 money를 20000으로 수정한다. (update)

그렇다면 DB의 결과는,
멤버아이디 1, money 20000인 member가 한명 있어야 한다.


그다음 deleteTest를 실행해 memberId 1 인 member를 삭제한다.

DB의 결과는 멤버가 아예 없어야 한다.

보기 좋게 성공했다.

음..
Spring data JPA만 사용하다보니,
ORM 기술에 익숙해지고 실제 SQL문을 통해 쿼리를 작성할 일은 거의 없는데,
해당 기술들이 이러한 JDBC API를 기반으로 발전되어 만들어졌고,
개발자들의 개발을 편하게 해준다.

그리고
JDBC API는
DriverManager를 통해 각각 DB에 맞는 커넥션 객체가 존재하고,
해당 객체를 찾아 관리해준다.

기본적으로 DB는 크게 보면 위와 같은 방식으로 관리된다.
그렇기에 해당 내용을 알아놓는것은 중요하다고 생각한다.



정리

과거엔 DB마다 연결하고 SQL문을 뿌려주고, 결과를 응답받는 방식이 다 달랐다.
getConnextion(), prepareStatement(String), executeUpdate()

그래서 자바 진영에서 JDBC라는 표준 인터페이스를 만들어줬고,
해당 표준 인터페이스를 기반으로 DB회사에서 라이브러리를 만들어서 제공해줬다.

그래서 개발자들은 JDBC 표준 인터페이스 사용법을 학습하면 되고,
해당 인터페이스로 대부분 DB를 동일하게 적용할 수 있다.



소스코드 : https://github.com/ingeon2/JDBC

profile
반갑습니다~! 좋은하루 보내세요 :)
post-custom-banner

2개의 댓글

comment-user-thumbnail
2023년 8월 7일

코드가 영어라 어려워요 한글로 만들어주세요

1개의 답글