Language/Java

[Java] JDBC & JDBCTemplate & NamedParameterJDBCTemplate

j4ko 2024. 8. 23. 20:12
728x90
반응형

목차

     

    개요 

    JDBC와 JDBCTemplate 더불어 NamedParameterJDBCTemplate에 대한 간단한 사용법 모음

     

    1.  Test Schema

    이 글은 JDBC를 사용하여 DataBase에 SQL을 날리는 예제를 적을 것이므로 SQL을 날리게 되는 테이블을 다음과 같이 정의한다.

    CREATE TABLE `Member` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `email` varchar(20) NOT NULL,
      `nickname` varchar(20) NOT NULL,
      `birthday` date NOT NULL,
      `createdAt` datetime NOT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8

     

     

    2. JDBC

    Java에서는 DataBase와 데이터를 입출력하기 위해 JDBC를 제공한다. JDBC는 DBMS의 종류와 상관없이 동일하게 사용할 수 있는 클래스와 인터페이스로 구성된다.

     

    2.1 JDBC Interface and Class

    • DriverManager 클래스는 JDBC Driver를 관리하며 DB와 연결해서 Connection 구현 객체를 생성한다.
    • Connection 인터페이스는 Statement, CallableStatement, PreparedStatement 구현 객체를 생성하며, 트랜잭션 처리 및 DB 연결을 끊을 때 사용한다.
    • Statement 인터페이스는 SQL의 DDL과 DML을 실행할 떄 사용한다. 주로 변경되지 않는 정적 SQL문을 실행할 때 사용한다.
    • PreparedStatement는 Statement와 동일하게 SQL의 DDL, DML문을 실행할 때 사용한다. 차이점은 매개변수화된 SQL문을 사용할 수 있기 때문에 편리성과 보안성이 좋다. 그래서 Statement보다는 PreparedStatement를 주로 사용한다.
    • CallableStatement는 DB에 저장되어있는 프로시저와 함수를 호출할 때 사용한다.
    • ResultSet은 DB에서 가져온 데이터를 읽을 때 사용한다.

     

    2.2 JDBC 사용법

    2.2.1 Class.forName으로 Driver 로딩

    JDBC를 통해 DB와 연결하기 위해 가장 먼저해야할 작업은 JDBC Driver를 메모리로 로딩하는 것이다. Class.forName()를 통해 해당 작업을 수행할 수 있으며 Class.forName()은 문자열로 주어진 JDBC Driver 클래스를 Build Path에서 찾고 메모리로 로딩을 수행한다.

    Class.forName("com.mysql.cj.jdbc.Driver");
    

    이 과정에서 JDBC Driver 클래스의 static 블록이 실행되면서 DriverManager에 JDBC Driver 객체를 등록하게 된다. 만약 해당 Driver를 찾을 수 없다면 ClassNotFoundException이 발생한다.

     

    2.2.2 Connection interface를 통해 DB 연결 설정

    이후 DB와 연결하기 위한 Connection interface를 사용한다.

    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:9901/sample", "root", "1234");
    

    Connection 정보는 사용하는 DataBase 벤더에 따라 다르다는 것에 주의가 필요

     

    2.2.3 PreparedStatement를 이용해 DML 질의

    PreparedStatement를 통해 SQL 질의를 실행할 수 있다. PreparedStatement는 Column의 순서로도 접근할 수 있고 Column명으로도 접근할 수 있다. 이 예제에서는 Column 명으로 접근하는 방식을 사용했다. 앞서 언급했듯 SELECT 문과 같이 사용하는 경우 ResultSet을 통해 DB에서 가져온 데이터를 읽을 수 있다.

    PreparedStatement pstmt = connection.prepareStatement("SELECT * FROM Member");
    ResultSet rs = pstmt.executeQuery();
    while (rs.next()) {
        System.out.println(rs.getInt("Id") + "," + rs.getString("email") + "," + rs.getString("nickname"));
    }
    

    예를 들어 잘못 접근하는 데이터 타입은 “SQLDataException”이 일어난다. 예를 들어 nickname은 String이어야 하지만 Int로 접근하는 경우가 그 예이다.

    Exception in thread "main" java.lang.RuntimeException: java.sql.SQLDataException: Cannot determine value type from string 'string'
    

    SQL 작업은 여러 매개변수를 추가로 사용하는 경우도 빈번하다. INSERT, UPDATE, DELETE가 예시다. 이 경우에는 PreparedStatement에 다음과 같이 매개변수를 추가로 지정할 수 있다.

    String insertSql = "INSERT INTO sample.`Member` (email, nickname, birthday, createdAt) VALUES(?, ?, ?, ?);";
    
    PreparedStatement pstmt = connection.prepareStatement(insertSql);
    pstmt.setString(1, "test@test.com");
    pstmt.setString(2, "test");
    pstmt.setString(3, "2001-01-01");
    pstmt.setString(4, "1999-01-01");
    
    int rows = pstmt.executeUpdate();
    

     

     

    3. JDBC Template

    JDBC의 불편함을 해결하기위해 JDBC Template을 사용할 수 있다. Springframework package를 통해 JDBC Template을 사용할 수 있다.

     

    3.1 JDBC의 불편함 ?

    앞서 정리해 놓은 JDBC에서 발생하는 불편함은 “구조적인 반복”이다. 다음은 JDBC를 사용해 DataBase로부터 데이터를 얻기 위한 과정이다.

    1. Class.forName으로 Driver 로딩 명시
    2. Connection 인터페이스로 DB 연결명시
    3. PreparedStatement를 통해 SQL 질의, 이후 ResultSet을 통해 Column 접근
    4. 2번 3번 과정에서 try ~ catch를 통해 Exception 처리, 이 과정이 끝나면 DataBase 연결에 사용된 Resource를 Close처리

    SQL 하나를 날리기 위해 여러 작업들을 반복해야 한다.

     

    JDBC Template은 이러한 작업들 간편하게 다룬다. 그러나 이 글에서 정리하려는 jdbc template은 사용법에 초점을 맞춘다.

     

    3.2 intelli-j에서 JDBC Template 설정

    JDBC Template은 springframework 패키지에서 제공한다. Spring Initializer를 통해 Spring Boot 프로젝트를 생성할 때 “Spring Data JDBC”를 Dependencies에 추가하면 JDBC Template 사용이 가능하다. 하지만 라이브러리 하나 설치하려고 Spring Boot Project를 통째로 생성하는 건 과하다 싶은 생각이 든다.

    Intelli-j에서 위와 같이 “springframwork.spring.jdbc”를 설치하면 SpringBoot Project를 생성하지 않고도 jdbc template이 사용 가능하다. intelli-j에서 라이브러리를 추가하는 방법은 이 링크를 참조하자.

     

    라이브러리 설치가 끝났으면 다음 package를 import 한 뒤 jdbc template이 사용가능하다.

    import org.springframework.jdbc.core.JdbcTemplate;
    

     

     

    3.3 DriverManagerDataSource를 이용해 연결 설정

    이 글에서는 Spring Boot를 통해 JDBC Template을 사용하는 것이 아니기 때문에 JDBC Template 클래스를 초기화할 때 DataSource interface의 구현 클래스를 지정했다.

     

    Spring/SpringBoot에서 DataSource interface의 구현 클래스는 대표적으로 JDBC DrvierManager를 기반으로 한 DriverManagerDataSource와 HikariCP 커넥션 풀을 기반으로 한 HikariDataSource가 있다고 한다.

     

    이 중에서 따로 설치가 필요 없는 DriverManagerDataSource를 이용해 다음과 같이 JDBC Template을 사용할 수 있었다.

    DriverManagerDataSource driverManager = new DriverManagerDataSource();
    driverManager.setDriverClassName("com.mysql.cj.jdbc.Driver");
    driverManager.setUrl("jdbc:mysql://127.0.0.1:9901/sample");
    driverManager.setUsername("root");
    driverManager.setPassword("1234");
    
    JdbcTemplate jdbcTemplate = new JdbcTemplate(driverManager);
    

     

     

    3.4 Row Mapper를 이용한 결과 매핑

    JDBC를 이용할 때는 ResultSet을 이용해 Column에 하나씩 접근하는 방법을 사용했지만 JDBC Template을 이용할 떄는 RowMapper 클래스를 통해 SQL 실행 후 해당 결과를 Java 클래스로 변환 후 사용할 수 있다.

    import org.springframework.jdbc.core.RowMapper;
    

    예시로 정의한 DDL을 Java 클래스로 정의하면 다음과 같다.

    class Member {
        final private Long id;
        final private String email;
        final private String nickname;
        final private LocalDateTime birthday;
        final private LocalDateTime createdAt;
       ...
    }
    

    다음은 Member클래스에 대해 RowMapper를 이용한 예시다.

    RowMapper<Member> memberRowMapper = new RowMapper<Member>() {
        @Override
        public Member mapRow(ResultSet resultSet, int i) throws SQLException {
            return new Member(
                    resultSet.getLong("id"),
                    resultSet.getString("email"),
                    resultSet.getString("nickname"),
                    resultSet.getObject("birthday", LocalDateTime.class),
                    resultSet.getObject("createdAt", LocalDateTime.class)
            );
        }
    };
    
    RowMapper<Member> memberRowMapper = (ResultSet resultSet, int row) -> new Member(
            resultSet.getLong("id"),
            resultSet.getString("email"),
            resultSet.getString("nickname"),
            resultSet.getObject("birthday", LocalDateTime.class),
            resultSet.getObject("createdAt", LocalDateTime.class)
    );
    

     

     

    3.5 SQL 관련 함수들

    아래는 jdbcTemplate을 이용해 DataBase에 query를 날릴 때 사용하는 예제들이다.

     

    3.5.1 jdbcTemplate.query(sql, rowMapper) : 결과 집합 읽기

    결과 집합, 즉 List로 읽어 들일 때는 query()를 사용한다.

    String sql = "SELECT * FROM Member;";
    
    List<Member> members = jdbcTemplate.query(sql, memberRowMapper);
    for (Member m : members) {
        System.out.println(m.toString());
    }
    
    // OutPut
    **Member{id=1, email='string', nickname='string'}
    Member{id=2, email='string', nickname='string'}
    Member{id=3, email='string', nickname='test'}
    ...**
    

    조회 결과가 없는 경우 빈 결과 집합을 가지고 있다.

     

    3.5.2 jdbcTemplate.queryForObject(sql, rowMapper) : 단일 결과 읽기

    단일 결과를 읽어올 때는 queryForObject()를 사용한다.

    String sql = "SELECT * FROM Member WHERE id =1;";
    Member member = jdbcTemplate.queryForObject(sql, memberRowMapper);
    if (member != null) {
        System.out.println(member.toString());
    }
    

    결과가 없는 경우 “EmptyResultDataAccessException”이 발생한다.

    Exception in thread "main" org.springframework.dao.EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0
    

    결과가 둘 이상인 경우 “IncorrectResultSizeDataAccessException”이 발생한다.

    Exception in thread "main" org.springframework.dao.IncorrectResultSizeDataAccessException: Incorrect result size: expected 1, actual 7
    

     

    3.5.3 jdbc.update(sql, …args) : INSERT, UPDATE, DELETE 시

    INSERT, UPDATE, DELETE 시에는 SQL에 Parameter를 넣어야 한다. 이는 update()를 사용해서 수행할 수 있다. 아래 예시는 INSERT 문을 통한 예시이다.

    String insertSQL = "INSERT INTO Member (email, nickname, birthday, createdAt) VALUES(?, ?, ?, ?)";
    int rows = jdbcTemplate.update(
    	insertSQL,
    	 "test2",
    	 "test2@email.com",
    	 "2002-01-01",
    	 "2002-02-02"
    	 );
    

     

     

    4. NamedParameterJDBCTemplate

    앞서 jdbcTemplate의 update() 메서드에서 SQL 질의 시 특정 파라미터에 대응하는 값은 “?”라는 place holder를 이용하고 해당 Mark에 대응하는 Value를 지정했다.

     

    순서에 따라서 값을 넣어줘야 하므로 가독성이 다소 떨어진다고 볼 수 있는데 NamedParameterJDBCTemplate을 사용하면 Key, Value로 형태로 제어가 가능하다.

     

    4.1 NamedParameterJDBCTemplate 초기화

    Spring/SpringBoot를 사용하지 않는 방식에서 NamedParamaterJDBCTemplate를 초기화시키는 방법은 JdbcTemplate과 방식과 같다.

    NamedParameterJdbcTemplate namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(driverManager);
    

     

    4.2 BeanPropertySqlParameterSource : Java Class를 parameter로 넘기기

    BeanPropertrySqlParameterSource는 Java Class의 instance를 parameter로 사용할 수 있게 만드는 방법이다.

    Member member = new Member(
                    22L,
                    "test3@email.com",
                    "test3",
                    LocalDateTime.now(),
                    LocalDateTime.now()
      );
    
    SqlParameterSource params = new BeanPropertySqlParameterSource(member);
    

    단, Parameter로 넘기고 싶은 클래스의 인스턴스에 Getter가 구현되어있어야 한다.

     

    4.3 MapSqlParameterSource : Key, Value를 명시하기

    MapSqlParamaterSource를 사용하면 이를 Key, Value를 이용해 입력값을 명시적으로 다룰 수 있다. 아래는 INSERT 문을 질의하는 예시다.

    NamedParameterJdbcTemplate namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(driverManager);
    
    String insertSQL = "INSERT INTO Member (email, nickname, birthday, createdAt) VALUES(:email, :nickname, :birthday, :createdAt)";
    MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource()
            .addValue("email", "test2@test2.com")
            .addValue("nickname", "test2")
            .addValue("birthday", "1999-01-01")
            .addValue("createdAt", "1999-01-01");
    
    namedParameterJdbcTemplate.update(insertSQL, mapSqlParameterSource);
    

     

    4.4Keyholder : INSERT 후 PK 얻기

    데이터를 DB에 삽입하고 난 후 생성된 데이터에 대해 PK를 얻는 방법이 필요하다면 KeyHolder를 사용할 수 있다. 그러나 이 방법은 PK 가 AUTO_INCREMENT로 정의된 Table에 대해 가능한 듯하다.

    String insertSQL = "INSERT INTO Member (email, nickname, birthday, createdAt) VALUES(:email, :nickname, :birthday, :createdAt)";
    SqlParameterSource params = new BeanPropertySqlParameterSource(
            new Member(
                    "test4@email.com",
                    "test4",
                    LocalDateTime.now(),
                    LocalDateTime.now()
            )
    );
    
    KeyHolder keyHolder = new GeneratedKeyHolder();
    
    namedParameterJdbcTemplate.update(insertSQL, params, keyHolder);
    
    long key = keyHolder.getKey().longValue();
    System.out.println(key);
    
    728x90
    반응형