JDBC

이정원·2024년 12월 7일
post-thumbnail

1.JDBC 개요

애플리케이션을 개발할 때 중요한 데이터는 대부분 데이터베이스에 보관한다. 클라이언트(웹,모바일)가 애플리케이션 서버를 통해 데이터를 저장하거나 조회하면, 애플리케이션 서버는 다음 과정을 통해서 데이터베이스를 사용한다.

어플리케이션과 서버의 일반적인 사용법

(1) 커넥션 연결,주로 TCP/IP를 사용해서 커넥션을 연결한다.
(2) SQL 전달, 애플리케이션 서버는 DB가 이해할 수 있는 SQL을 연결된 커넥션을 통해 DB에 전달한다.
(3) 결과 응답, DB는 전달된 SQL을 수행하고 그 결과를 응답한다. 애플리케이션 서버는 응답 결과를 활용한다.

여기서 문제점은, 관계형 데이터베이스 종류가 수십개 있는데 어플리케이션 서버에서 특정 데이터베이스에만 맞춰 설계된 SQL을 사용할 경우 유연성이 떨어진다는 점이다.

이러한 문제로 인해 JDBC라는 자바 표준이 등장한다.

JDBC: JDBC(Java Database Connectivity)는 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API다. JDBC는 데이터베이스에서 자료를 쿼리하거나 업데이트하는 방법을 제공한다.


자바는 이렇게 표준 인터페이스를 정의해두었다. 이제부터 개발자는 이 표준 인터페이스만 사용해서 개발하면 된다.JDBC 인터페이스를 각각의 DB 벤더(회사)에서 자신의 DB에 맞도록 구현해서 라이브러리로 제공하는데, 이것을 JDBC 드라이버라 한다.

JDBC 덕분에 추상화에 의존하여 어플리케이션 코드를 그대로 유지할수 있다.

표준화의 한계
데이터베이스를 변경하면 JDBC 코드는 변경하지 않아도 되지만 SQL은 해당 데이터베이스에 맞도록 변경 해야한다.참고로 JPA(Java Persistence API)를 사용하면 이렇게 각각의 데이터베이스마다 다른 SQL을 정의해야 하는 문제도 많은 부분 해결할 수 있다.

2.JDBC와 최신 데이터 접근 기술

JDBC는 1997년에 출시될 정도로 오래된 기술이고, 사용하는 방법도 복잡하다. 그래서 최근에는 JDBC를 직접 사용하기 보다는 JDBC를 편리하게 사용하는 다양한 기술이 존재한다. 대표적으로 SQL MapperORM 기술로 나눌 수 있다.

사용방법은 어플리케이션 로직에서 JDBC로 SQL을 직접 전달하면 된다. 하지만 이 방법은 Raw level로 제공하다 보니 기능이 하나하나 세분화 되어있고 번잡하다.

2-1.SQL Mapper

따라서 JDBC를 편리하게 사용하는 SQL Mapper를 사용함으로써 SQL 응답 결과를 객체로 편리하게 변환, 반복 코드 제거 등의 이점이 있다.(JdbcTemplate, MyBatis)

2-2.ORM


ORM은 개발자가 직접 SQL 쿼리를 작성하지 않고도 객체 지향 프로그래밍 언어의 객체를 데이터베이스와 매핑하여 사용할 수 있도록 지원하는 기술이다. Java Persistence API(JPA)와 같은 ORM 프레임워크를 사용하면, 데이터를 Java 객체에 저장하거나, Java 객체를 데이터베이스에 저장하는 작업을 마치 컬렉션에 데이터를 추가하거나 조회하는 것처럼 간단하게 처리할 수 있다.

JPA는 자바 진영의 ORM 표준 인터페이스이고, 이것을 구현한 것으로 하이버네이트이클립스 링크 등의 구현 기술이 있다.

H2 데이터 베이스에 직접 연결해보자.

ConnectionConst

public abstract class ConnectionConst {
    public static final String URL="jdbc:h2:tcp://localhost/~/test2";
    public static final String USERNAME="sa";
    public static final String PASSWORD="";
}

DBConnectionUtil

@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 IllegalArgumentException(e);
        }
    }
}

테스트

@Slf4j
class DBConnectionUtilTest {
    @Test
    void connection(){
        Connection connection = DBConnectionUtil.getConnection();
        assertThat(connection).isNotNull();
    }
}

실행 결과를 보면 org.h2.jdbc.JdbcConnection를 사용한다. 이것이 바로 H2 데이터베이스 드라이버가 제공하는 H2 전용 커넥션이다. 해당 구현체는 java.sql.Connection 인터페이스를 구현하고 있다. 전체 로직을 확인해보면

JDBC가 제공하는 DriverManager는 라이브러리에 등록된 DB 드라이버들을 관리하고, 커넥션을 획득하는 기능을 제공한다. DriverManager가 등록된 드라이버들에 순서대로 요청을 처리할수 있는지 확인하여 커넥션을 획득하고 구현체가 클라이언트에게 반환된다.
와 최신 데이터 접근 기술

3.JDBC 개발

JDBC를 사용해서 회원(Member) 데이터를 데이터베이스에 관리하는 기능을 개발해보자. 사전에 Member table은 만들어 두어야 한다.

회원 객체

@Data
public class Member {
    private String MemberId;
    private int money;

    public Member(){

    }

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

JDBC를 통해서 회원 객체를 H2 DataBase에 저장하는 코드를 작성하자.
회원 저장 (JDBC - DriverManager 사용)

@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.info("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(); //Exception
            } catch (SQLException e) {
                log.info("error",e);
            }
        }
        if(con!=null){
            try {
                con.close();
            } catch (SQLException e) {
                log.info("error",e);
            }
        }
    }
    private static Connection getConnection() {
        return DBConnectionUtil.getConnection();
    }

}

con은 데이터베이스와의 연결을 관리하는 객체이며, pstmt는 SQL 쿼리를 작성하고 데이터 값을 바인딩하여 실행하기 위한 객체이다.(설정한 인덱스 번호에 해당하는 ?에 바인딩) executeUpdate() 메서드는 준비된 SQL 쿼리를 데이터베이스에 전달하여 실행하는 역할을 한다.

자원을 닫는 작업을 별도의 함수로 정의한 이유는, 각각의 close() 메서드에서 예외가 발생하더라도 나머지 자원 닫기 작업이 정상적으로 수행되도록 보장하기 위함이다. 만약 예외 처리가 없다면, 하나의 close() 호출 중에 다른 자원 해제가 이루어지지 않고 프로그램이 종료될 위험이 있다. 리소스 정리가 보장되도록 finally에서 닫는 함수를 사용해야 한다.

CRUD 테스트

class MemberRepositoryV0Test {

    MemberRepositoryV0 repository=new MemberRepositoryV0();

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

}

실행 결과

성공적으로 save 가 된것을 확인할수 있다.

JDBC를 통해서 회원 객체를 H2 DataBase에서 조회하는 코드를 작성하자.

회원 조회

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

여기서 select 쿼리는 executeQuery()를 사용해야 하고 결과로 받은 rs는 쿼리의 결과가 담긴다. rs.next()로 커서를 한번 이동해야 원하는 값을 얻을수 있고 마찬가지로 리소스를 정리할때 rs->pstmt->con 순으로 정리 하면된다.

테스트

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

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

테스트 결과를 확인해보면 findMember=Member(memberId=memberV1, money=10000)로 정상 동작한다. repository에서 db에서 찾은 값으로 member 객체를 새로 생성하고 테스트에서도 새로 생성해서 다른 객체지만 lombok의 @Data의 EqualsAndHashCode로 인해 같은 값으로 확인할수 있다.

@EqualsAndHashCode: equals()가 true를 반환하면, hashCode()도 동일한 값을 반환해야 한다는 규칙이 있다. 이는 해시 기반의 컬렉션(HashMap, HashSet, Hashtable)에서 객체를 저장하거나 검색할 때 중요한 역할을 한다.해시 기반 컬렉션은 먼저 hashCode()를 사용해 객체를 저장할 위치(버킷)를 결정한다.이후 동일한 hashCode()를 가진 객체들끼리 equals()를 이용해 실제로 동일한 객체인지 확인한다.equals()와 hashCode()가 올바르게 구현되어 있어야, 동일한 객체는 항상 같은 위치에 저장되거나 검색될 수 있다.

3-1.ResultSet

ResultSet은 다음과 같이 생긴 데이터 구조이다. 보통 select 쿼리의 결과가 순서대로 들어간다.

ResultSet 내부에 커서(cursor)를 이동하여 다음 데이터를 조회할수 있다. 최초의 커서는 데이터를 가르키고 있지 않아서 rs.next()를 최초 한번 호출해야 데이터를 조회할수 있다. rs.next()결과가 false이면 더이상 데이터를 가지고 있지 않는것이다.

회원 삭제

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

테스트

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

삭제의 경우 NoSuchElementException 예외가 발생하면 성공이다. 테스트 마지막에 삭제를 수행해야 반복 수행에서 오류가 나지 않는다.(동일 객체 저장시 무결성 제약 조건 위반) 하지만 꼭 마지막에 삭제를 수행한다고 해서 궁긍적인 방법이 아니다. 왜냐하면 중간에 예외가 발생하면 삭제가 수행되지 않고 종료되기 때문이다.

0개의 댓글