[Web] Spring JDBC

JDBC 프로그래밍에는 반복되는 개발 요소가 있다. 스프링 프레임워크가 이러한 작업들을 대신 처리해준다. 따라서 스프링 프레임워크를 사용하면 개발자는 필요한 부분만 개발하면 된다.



Spring JDBC 패키지
- org.springframework.jdbc.core
- org.springframework.jdbc.datasource
- org.springframework.jdbc.object
- org.springframework.jdbc..support

JDBC Template 
jdbc.core에서 가장 중요한 클래스이다. 리소스 생성, 해지를 처리해서 연결을 닫는 것을 잊어 발생하는 문제등을 피할 수 있도록 한다. statement의 생성과 실행을 처리하고 sql 조회, update, 저장 프로시저 호출, resultSet 반복호출 등을 실행한다. JDBC예외가 발생할 경우 org.sprinframework.dao 패키지에 정의되어있는 일반적인 예외로 변환시킨다.

그 외..
JDBC Template만 사용해도 편리하게 사용할 수 있지만 더 편리하게 해주는 몇가지 클래스가 있다.
- NamedParameterJdbcTemplate : ? 대신 이름을 사용하여 바인딩 할 수 있게 해줌
- SimpleJDBCTemplate: 가장 빈번하게 사용되는 작업들을 모아놓은 클래스
- SimpleJDBCInsert : insert를 간편하게 해주는 클래스

jdbc의 사용이 불편해서 나온 기술에는 스프링 jdbc 외에도 다양하게 존재한다. 대표적으로 JPA와 MyBatis가 있다.

DTO : Data Transfer Object
계층간 데이터 교환을 위한 자바빈즈. 여기서 계층이란 컨트롤러 뷰, 비즈니스 계층, 퍼시스턴스 계층을 의미한다. dto는 일반적으로 로직을 가지지 않고 순수한 데이터 객체이다.
일반적으로 getter, setter를 가지고 추가적으로 toString(). equals() 등의 메소드를 가지고 있다.

DAO : Data Access Object
데이터를 조회하거나 조작하는 기능을 전담하도록 만든 객체
보통 데이터베이스를 조작하는 내용을 담고있다.

ConnectionPool
db연결은 비용이 많이 든다. 커넥션 풀은 미리 커넥션을 여러 개 맺어 두고 커넥션이 필요하면 커넥션 풀에게 빌려서 사용한 후 반납한다.

커넥션 풀을 사용할 경우 최대한 빨리 사용하고 반납해야 한다. 만약 커넥션풀에 사용할수있는 커넥션이 더이상 없어지게되면 심각한 오류가 발생할 수 있다.

DataSource
ConnectionPool을 관리하는 목적으로 사용되는 객체이다. DataSource를 이용해 커넥션을 얻어오고 반납하는 등의 작업을 수행한다.

다음의 구조를 가진 예제 프로젝트를 만들어보자



maven 프로젝트를 하나 생성하고 제일 먼저 해야 할 일은 pom.xml을 수정하는 것이다.
jdk 버전을 바꿔주고 spring framework와 jdbc를 사용하기 위한 각종 라이브러리들을 설정해준다.
 <dependencies>
  <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
 
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>${spring.version}</version>
        </dependency>
 
        <!-- basic data source -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-dbcp2</artifactId>
            <version>2.1.1</version>
        </dependency>
 
 
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.45</version>
        </dependency>
        
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
cs

그리고 config 클래스를 관리하기 위한 패키지를 만들어 ApplicationConfig.java 클래스를 생성한다.
package kr.or.connect.daoexam.config;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
 
@Configuration
@Import({ DBConfig.class })
public class ApplicationConfig {
 
}
 
cs
database와 관련된 config 파일은 따로 관리하기위해 import 어노테이션으로 DBConfig 클래스를 지정해놓는다. 이어서 같은 패키지 안에 DBConfig.java를 만든다
@Configuration
@EnableTransactionManagement
public class DBConfig {
    private String driverClassName = "com.mysql.jdbc.Driver";
    private String url = "jdbc:mysql://localhost:3306/connectdb?useUnicode=true&characterEncoding=utf8";
    private String username = "connectuser";
    private String password = "1234";
 
    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }
cs

데이터를 관리하는 DataSource 를 여기서 사용한다. 이 때 import 하는 라이브러리가
import javax.sql.DataSource;
인지를 꼭!! 확인하자.. 나는 여기서 다른 패키지의 라이브러리를 import해서 어디서 오류가 난지 찾는데 한참이 걸렸다.

config 파일을 모두 만들었으니 db 연결이 제대로 되는지 확인이 필요하다. main 패키지를 만들어서 DataSourceTest라는 클래스를 만들었다.
public class DataSouceTest {
 
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        ApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);
        DataSource ds = ac.getBean(DataSource.class);
        Connection conn = null;
        try {
            conn = ds.getConnection();
            if (conn != null)
                System.out.println("접속 성공!");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
 
}
cs

DB연결에 대한 정보는 DBConfig에 있지만 최종적으로는 ApplicationConfig가 이를 import 해오는 것이므로 우리가 호출해야 할 config 파일은 ApplicationConfig이다. 신기하다고 느꼈던 것은 DataSource를 Bean으로 가져올 때는 형변환이 필요 없다는 것. DataSource의 getConnection() 메서드로 디비 연결에 성공하면 "접속 성공" 이라는 메시지가 콘솔에 출력된다.

이번에는 데이터를 저장하는 Dto 클래스를 만든다. 이것또한 dto 패키지를 만들어서 그 안에 생성해준다.
package kr.or.connect.daoexam.dto;
 
public class Role {
    private int roleId;
    private String description;
    
    public int getRoleId() {
        return roleId;
    }
    public void setRoleId(int roleId) {
        this.roleId = roleId;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    @Override
    public String toString() {
        return "Role [roleId=" + roleId + ", description=" + description + "]";
    }
    //객체가 가진 값들을 한번에 확인하기 쉽다
    
}
 
cs

dto를 생성하고 이제 DTO에 접근할 수 있는 DAO를 만들어야 한다. 패키지를 만들고 생성해주자.
public class RoleDaoSqls {
    public static final String SELECT_ALL = "SELECT role_id, description FROM role order by rold_id";
}
 
cs

먼저 SQL 문을 저장하는 DAO 이다. SQL문을 상수로 final 을 사용해 선언해주는데 이때 상수의 이름은 항상 모두 대문자로 쓰기로 정해져있다는 것을 꼭 기억해두자.
이번에는 DTO에 접근하는 DAO를 만든다. DAO의 어노테이션은 Repository로 준다.

import kr.or.connect.daoexam.dto.Role;
 
@Repository
public class RoleDao {
    private NamedParameterJdbcTemplate jdbc;
    private RowMapper<Role> rowMapper = BeanPropertyRowMapper.newInstance(Role.class);
 
    public RoleDao(DataSource dataSource) {
        this.jdbc = new NamedParameterJdbcTemplate(dataSource);
    }
 
    public List<Role> selectAll() {
        return jdbc.query(SELECT_ALL, Collections.emptyMap(), rowMapper);
    }
}
cs

위의 그림에서 본 것 처럼 RoleDao는 NamedParameterJdbcTemplate을과 simpleJdbcInsert를 사용한다.
jdbcTemplate은 바인딩 할 때 ?를 사용한다. 이는 어떤 값이 매핑되는지 알아보기 힘든 문제점이 있고 이것을 해결하기 위해 NamedParameterJdbc를 사용한다. 이 클래스는 ?대신 컬럼명을 이용하여 바인딩을 가능하게 해주기 때문에 사용한다. 생성자 안에 선언해준다.
생성자를 보면 dataSource를 파라미터로 받고 있는데 componentScan으로 객체를 찾았을 때 기본 생성자가 없다면 자동으로 객체를 injection 해준다. 그러므로 이전에 DBConfig에서 @Bean 으로 만들어놓은 DataSource 객체가 받아들여져 Named.. 객체를 생성하게 되는 것이다.
select * 쿼리문을 테스트 할 것이기 때문에 메서드 반환형은 List로 한다.
복잡한 코드들은 namedParameterJdbc가 다 해줄 것이다.
query라는 메서드를 사용하는데 이때 사용되는 파라미터는

( 사용될 쿼리문 , 비어있는 map 객체, RowMapper)

사용될 쿼리문은 이전에 만들어뒀던 roledao.sqls 클래스에 선언한 상수 sql 을 불러오면 되는데 이것을 위해 import static을 사용한 것. 이렇게 해두면 패키지명을 붙일 필요 없이 상수명만 써도 오류가 발생하지 않는다.

두번째 파라미터는 sql문에 바인딩할 값이 있을 때 그 값을 전달하는 목적으로 사용되는 객체이다. 세번째 파라미터는 select 각 하나의 값을 dto에 저장하는 목적으로 사용된다. BeanPropertyRowMapper를 사용해 컬럼의 값을 자동으로 dto에 담아준다. query 메서드는 결과가 여러건일 때 내부적으로 반복하며 dto를 생성하고 생성한 dto를 list에 담아주는 역할을 하고 해당 List를 반환해준다.
java 와 dbms가 컬럼명 / 클래스명을 칭하는 방식이 다른데 예를 들어
dbms 에서 role_id 라고 쓰는 반면 java에서는 카멜 표기법을 사용해 roleId 라고 사용한다. dbms에서는 대소문자의 구분이 없는 반면 java는 대소문자를 구별하기 때문이다.
BeanPropertyRowMapper는 이러한 표기법의 차이를 알아서 처리해주는 기능도 가지고 있다.

이번에도 실행을 테스트 해보기 위해 main 패키지 안에 테스팅 클래스를 생성했다.
public class SelectAllTest {
    public static void main(String[] args) {
        ApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);
 
        RoleDao roleDao = ac.getBean(RoleDao.class);
        List<Role> list = roleDao.selectAll();
 
        for (Role role : list) {
            System.out.println(role);
        }
    }
}
cs

제대로 출력이 되는 것을 볼 수 있다!

insert를 구현해보자. 앞서 말했듯 simpleJDBCInsert클래스를 사용한다.
    private SimpleJdbcInsert insertAction;
 
    public RoleDao(DataSource dataSource) {
        this.jdbc = new NamedParameterJdbcTemplate(dataSource);
        this.insertAction = new SimpleJdbcInsert(dataSource).withTableName("role");
    }
        public int insert(Role role) {
        SqlParameterSource params = new BeanPropertySqlParameterSource(role);
        return insertAction.execute(params);
    }
cs

위와 같은 내용을 추가해준다.
BeanPropertySqlParameterSource는 role_id 와 roleId 의 이름을 맞춰주고 둘을 매핑시킨다. return 으로는 insert된 컬럼 수가 반환된다.

또한 테스트 해보는 클래스를 만들어 출력해본다.
public class JDBCTest {
    public static void main(String[] args) {
        ApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);
 
        RoleDao roleDao = ac.getBean(RoleDao.class);
 
        Role role = new Role();
        role.setRoleId(500);
        role.setDescription("CEO");
 
        int count = roleDao.insert(role);
        System.out.println(count + "건 입력했습니다.");
    }
}
cs

- 1건 입력했습니다. 라고 정상적으로 출력된다.
비슷하게 이번에는 update문을 수행해보자. 새로운 sql문이 필요하다.

'RoleDaoSqls' 에 추가한다.
public static final String UPDATE = "UPDATE role SET description = :description where rold_id=:roleId";
cs

NamedPropertyJDBC 를 사용하기 때문에 ?가 아닌 문자열로 바인딩 할 수 있다.

dao 클래스에 메서드를 추가한다.
public int update(Role role) {
        SqlParameterSource params = new BeanPropertySqlParameterSource(role);
        return jdbc.update(UPDATE, params);
    }
cs
BeanPropertySqlParameterSource는 이전에 : 콜론과 함께 ? 대신 적어놨던 값들에 바인딩 해줄 것이다.

똑같은 테스트 클래스에 아래 코드를 추가해서 동작되는지 확인한다.
        Role role = new Role();
        role.setRoleId(777);
        role.setDescription("ZINI");
        int count = roleDao.update(role);
        System.out.println(count + "건 수정했습니다.");
cs

1건 수정되었습니다. 라는 메세지가 정상 출력된다.

한 건의 select와 delete를 구현해보자.
먼저 sql문을 작성!
    public static final String DELETE_BY_ROLE_ID = "DELETE FROM role where role_id=:roleId";
    public static final String SELECT_BY_ROLE_ID = "SELECT role_id, description FROM role WHERE role_id=:roleId";
cs

다음으로 roleDao에 메서드를 작성한다.
    public int deleteById(Integer id) {
        Map<String, ?> params = Collections.singletonMap("roleId", id);
        return jdbc.update(DELETE_BY_ROLE_ID, params);
    }
 
cs

delete 에서는 id 값 하나만 매핑을 필요로 한다. 위에서 썼던 것 처럼 BeanPropertySqlParameterSouce 객체를 생성하여 바인딩 할 수 있지만 값 하나만 바꾸는 데 굳이 객체를 생성하는 것은 불필요하다 여겨지기 때문에 Map 객체 하나만 만들어서 쓴다고 한다.

    public Role selectById(Integer id) {
        try {
            Map<String, ?> params = Collections.singletonMap("roleId", id);
            return jdbc.queryForObject(SELECT_BY_ROLE_ID, params, rowMapper);
        } catch (EmptyResultDataAccessException e) {
            return null;
        }
    }
cs

한건의 select도 비슷하다. id값 하나만 바꾸면 되기 때문에 simgletonMap을 이용한다. rowMapper는 이전에 선언한 객체로 Role 객체에 값을 매핑시켜 반환한다. 만약 찾으려는 데이터가 없는 경우에 에러를 처리하는 핸들링도 넣어줬다.

확실히 프레임워크 없이 내가 코드를 다 작성할 때보다 엄청나게 짧아졌다. 하지만 처음 보는 무수한 메서드들에 내 머리도 엄청나게 복잡해졌다.. 역시 세상에 거저 되는 일은 없다. 많은 연습으로 익숙해지다보면 얼마나 간편한 시스템인지 익힐 수 있을 것 같다..

No comments:

Powered by Blogger.