티스토리 뷰

2) Spring JDBC 실습

DTO?

  • DTO Data Transfer Object의 약자입니다.
  • 계층간 데이터 교환을 위한 자바빈즈입니다여기서의 계층이란 컨트롤러 뷰, 비지니스 계층, 퍼시스턴스 계층을 의미합니다.
  • 일반적으로 DTO는 로직을 가지고 있지 않고, 순수한 데이터 객체입니다.
  • 필드와 getter, setter를 갖고 있습니다. 추가적으로 toString(), equals(), hashCode()등의 Object 메소드를 오버라이딩 할 수 있습니다.
  • 데이터를 들고 다닐 때 하나씩 들고 다니면 불편하기 때문에 어떤 하나의 가방처럼 만들어서 데이터 들을 한꺼번에 갖고 다니는 용도로 사용한다고 간단하게 생각하시면 좋을 것 같습니다.

DTO의 예

public class ActorDTO {

    private Long id;

    private String firstName;

    private String lastName;

    public String getFirstName() {

        return this.firstName;

    }

    public String getLastName() {

        return this.lastName;

    }

    public Long getId() {

        return this.id;

    }

    // ......

}

 

DAO?

  • DAO Data Access Object의 약자로 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 객체입니다.
  • 보통 데이터베이스를 조작하는 기능을 전담하는 목적으로 만들어집니다.
  • 객체 지향은 어떤 객체가 한 가지 일만 정확하게 제대로 하는 것을 바라잖아요? DAO 객체는 딱 db를 조작하는 일만 하도록 만들어놓은 객체라고 생각하시면 좋을 것 같습니다.

ConnectionPool 이란?

  • DB연결은 비용이 많이 듭니다.
  • 커넥션 풀은 미리 커넥션을 여러 개 맺어 둡니다.
  • 데이터베이스로부터 정보를 읽고 쓰는 프로그램을 실행해보면 프로그램이 DBMS에게 접속하는 시간이 조금 오래 걸리는 걸 볼 수 있습니다.
    이렇게 시간이 오래 걸리거나 자원을 많이 소모하는 것을 비용이 많이 발생한다고 합니다.
    이런 문제를 해결하기 위해서 DBMS와 커넥션을 미리 많이 맺어둔 객체를 사용하는 경우가 있습니다. 이것을 ConnectionPool이라고 합니다.
  • 커넥션이 필요하면 커넥션 풀에게 빌려서 사용한 후 반납합니다.
  • 커넥션을 반납하지 않으면 어떻게 될까요?
    → 
    ConnectionPool을 사용할 때는 Connection을 되도록 빨리 사용하고 빨리 반납을 해야 됩니다그렇지 않으면 ConnectionPool에서 사용 가능한 Connection이 없어서 프로그램이 늦어지거나 심할 경우에는 장애를 발생시킬 수도 있습니다.

  1. 1번 그림에서 Client 1, 2번이 ConnectionPool로부터 Connetion 객체를 얻어서 사용하고 있죠.
  2. 2번 그림에서는 Client 1, 2, 그리고 Client 3이 다시 커넥션을 얻어서 사용하는 것을 볼 수 있고요.
  3. 3번 그림에서는 Client 1 Connection close 하면 사용한 Connection ConnectionPool로 반납이 되고 있는 것을 볼 수 있습니다.
  4. 마지막으로 4번 그림에서는 Client 3 Connect close 해서 반납하고 있는 것이 보여지죠.

DataSource?

  • DataSource는 커넥션 풀을 관리하는 목적으로 사용되는 객체입니다.
  • DataSource를 이용해 커넥션을 얻어오고 반납하는 등의 작업을 수행합니다.
  • DataSource로부터 얻은 Connection close() 메서드는 반납하도록 구현이 되어 있습니다.

Spring JDBC를 이용한 DAO작성 실습

  • 우리가 만들 프로그램은 이런 구조를 가지고 있습니다.
  • 먼저 Spring 컨테이너인 ApplicationContext는 설정 파일로 ApplicationConfig라는 클래스를 읽어들입니다. ApplicationConfig에는 @ComponentScanDAO 클래스를 찾도록 설정할 것입니다. 찾은 모든 DAO 클래스는 Spring 컨테이너가 관리하게 됩니다. 그리고 ApplicationConfig DBConfig 클래스를 import 하게 되고요. DBConfig 클래스에서는 데이터 소스와 트랜잭션 매니저 객체를 생성합니다.
  • DAO는 필드로 NamedParameterJdbcTemplateSimpleJdbcInsert를 가지게 될겁니다. 두 개의 객체는 모두 DataSource를 필요로 할겁니다. 왜냐면 두 개의 객체 모두 SQL의 실행을 편리하게 하도록 Sprint JDBC에서 제공하는 객체이기 때문에 DB 연결을 위해서 내부적으로 DataSource를 사용하기 때문이에요이 두 개의 객체는 RoleDao 생성자에서 초기화를 하고 초기화된 두 개의 객체를 이용해서 RoleDao의 메서드들을 구현하게 됩니다.
  • Spring JDBC를 사용하는 사용자는 파라미터와 SQL을 가장 많이 신경 써야 됩니다. SQL RoleDao SQL의 상수로 정의를 해놓음으로써 나중에 SQL이 변경될 경우에 좀 더 편하게 수정할 수 있도록 하였습니다. 그리고 한 건의 Role 정보를 저장하고, 전달하기 위한 목적으로 Role DTO가 사용되고 있는 것을 볼 수 있습니다.

실습코드(pom, config, datasource)

  • 첫 번째는 커넥션이 잘 연결되는지부터 테스트해보겠습니다.
  • quickstart archetype의 Maven 프로젝트를 생성합니다. Group Id kr.or.connect라고 할 거고요. Artifact Id daoexam이라고 지정합니다.
  • 그 다음으로 pom.xml을 열어서 필요한 라이브러리들을 추가합니다.
  • Java 1.8을 사용하기 위해서 dependencies 하단에다가 build 추가해주고, Spring을 사용해야 하니까 dependency에 spring-context를 추가합니다. Spring JDBC를 사용하기 위해서 spring-jdbc spring-tx 부분을 라이브러리로 추가를 해야 합니다.
  • dependencies 밖에 properties에다가 Spring 버전을 추가해서 버전관리를 한번에 할 수 있도록 합니다. 필수는 아니지만 버전을 따로 써서 각각 바꾸기보다는 한꺼번에 바꿀 수 있게 하기 위해서 프로퍼티를 쓰는 겁니다.
  • 그리고 mysql 데이터베이스를 사용하기 위해 다시 dependency에 mysql에서 제공하는 드라이버를 추가합니다. mysql을 사용할 수 있는 드라이버는 mysql-connector-java라는 라이브러리가 필요할 겁니다.
  • 그리고 DataSource를 추가하는데 DataSource도 종류가 여러 가지가 존재합니다. 우리는 Apache에서 제공하고 있는 commons-dbcp2를 사용하게 할 겁니다.
  • 라이브러리 추가가 끝나면 Maven 업데이트를 합니다.
    여기까지 수행하셨으면 Spring JDBC를 사용하기 위한 준비를 했습니다.

 

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>kr.or.connect</groupId>
  <artifactId>daoexam</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>daoexam</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring.version>4.3.5.RELEASE</spring.version>
  </properties>

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

</project>

 

  • Spring JDBC 구조에 맞는 파일들을 생성하겠습니다. 
  • ApplicationContext가 설정들을 읽어서 공장을 만들수 있도록 설정파일인 ApplicationConfig 클래스를 만듭니다. ApplicationConfig 파일은 설정에 관련된 부분이니까 관리하기 편하게 패키지를 나눈 뒤 파일을 생성합니다. config에 관련된 부분이므로 config라는 패키지를 생성합니다.
  • 이 패키지 안에 ApplicationConfig 클래스를 만듭니다. 이제 이 클래스에 @Configuration 어노테이션을 붙여 설정들에 대한 정보들을 읽을 수 있도록 합니다.
  • 그리고 @Import라고 하는 어노테이션을 사용합니다. Import라는 어노테이션을 이용하게 되면 설정 파일을 여러 개로 나눠서 작성할 수가 있습니다. 이 설정 파일 하나에다가 모든 설정을 다 넣는 게 아니라 데이터베이스 연결에 관련된 설정은 따로 빼서 다른 파일에 작성합니다. 왜냐하면 하나의 클래스가 모든 정보를 갖고 있으면 나중에 유지 보수하기 힘들기 때문입니다. 그래서 DB 관련된 설정은 DBConfig라는 파일에다가 따로 작성할 것이고 해당 파일을 import하기 위해 @Import({DBConfig.class}) 와 같이 코드를 작성합니다.
  • 데이터베이스 설정만 따로 담고있는 DBConfig라는 클래스도 하나 만들어 줍니다. DBConfig Configuration이니까 @Configuration을 붙여줍니다.
  • 어노테이션을 하나 더 추가할 텐데요. 트랜잭션 때문에 필요한 어노테이션인 @EnableTransactionManagement를 추가합니다.

 

  • DBConfig는 데이터베이스 설정에 관련된 부분이므로 알려줘야 하는 정보가 있습니다. driveClassName, url정보(어떤 데이터 베이스에 접속할 건지), username, 그리고 password에 대한 정보입니다.
  • 그런 다음에 Spring JDBC를 이용할 때 우리는 DataSource라는 걸 통해서 DB에 접속하는 부분들을 얻어내기로 했잖아요? 그렇게 하기 위해서는 DataSource를 생성할 수 있는 클래스가 필요합니다. 직접 DataSource 객체를 등록하셔야 되는데요. bean으로 등록해야 합니다. DataSource 라이브러리에 이미 작성되어 있는 객체(javax.sql.DataSource)이므로 @Bean을 이용하여 등록합니다.
  • 메서드를 생성하는데 리턴은 DataSource, 메서드의 이름은 Bean 등록할 때 id로 지정하는 이름과 같게 하여 생성합니다.
  • 그리고 메서드 안에 DataSource가 생성될 때 필요한 정보를 입력합니다. DataSource 객체는 커넥션을 관리할 거기 때문에 JDBC 드라이버라든가 url, username, password 이런 정보들을 알아야 하며 해당 정보를 메서드 안에서 정의합니다.

 

  • 여기까지 하면 가장 최소한의 설정을 한 것입니다. 그러면 이제 데이터베이스에 잘 접속이 되는지 확인해보겠습니다.
  • 확인하기 위한 클래스를 만들건데 우선 패키지를 하나 따로 만들어서 생성하겠습니다. 기존의 패키지에다가 name(kr.or.connect.daoexam.name)이라고 하는 패키지를 추가합니다.
  • 그리고 DataSource가 잘 동작이 되는지 확인하기 위한 클래스니까 DataSourceTest라고 만듭니다DataSourceTest는 실행을 해봐야 되니까 main 메서드를 입력합니다.
  • Spring 컨테이너가 Bean들을 생성하고 Bean들을 관리하기 위해 ApplicationContext 공장을 생성합니다. 어노테이션 정보를 얻기 위해 AnnotationConfigApplicationContext를 생성합니다.
  • 생성자 파라미터에는 관련 설정을 얻기 위해 ApplicationConfig.class를 입력합니다. 그리고 ApplicationConfig는 @Import({DBConfig.class}) 코드로 인해 DBConfig의 정보까지 읽어들입니다.
  • 아무튼 ApplicationConfig.class에서 정보를 읽어들여서 공장을 생성하고 그 공장이 bean들을 생성할 겁니다.
  • 그리고 이 공장을 통해 DataSource 객체를 얻습니다.
  • javax.sqlDataSource를 가리키는 변수를 생성하고 ApplicationContext한테 getBean() 해서 얻어내는데 파라미터에는 DataSource.class를 적어 DataSource를 리턴할 수 있도록 합니다. 그러면 DataSource를 구현하고 있는 실제 객체를 얻어낼 수 있을 겁니다.
  • 현재 확인하려는 건 Connection을 잘 얻어내오는지이므로 Connection 객체를 DataSource를 통해서 얻어내는데 DataSource의 getConnection()이라는 메서드를 이용하시면 Connection 객체를 얻어낼 수 있습니다.
  • 만약 제대로 얻어왔다면 "접속 성공" 이라고 하는 메시지를 콘솔에 출력하고 얻어오지 못하면 아무런 메시지도 보여주지 않을 것입니다. Exception 처리도 적절하게 수행을 해주시면 될 거고요.
  • 그리고 어딘가에 연결한 커넥션을 닫기 위한 처리합니다. 해당 작업은 반드시 실행되어야 하므로 finally 블록에 코딩합니다. if 블록에서 Connectionnull 인지 아닌지 먼저 체크를 하고 null이 아니라면 Connection을 닫아주세요. close()라는 메서드도 throws 되고 있기 때문에 catch exception처리 해줍니다.

 

  • 다시 한번 정리해보면 ApplicationConfig.class에 들어있는 설정 파일을 읽어들여가지고 ApplicationContext를 생성했습니다. ApplicationContext IoC/DI 컨테이너라 가능한 부분입니다.
  • 그리고 이 컨테이너가 가지고 있는 getBean()이라는 메서드를 이용해서 DataSource라는 클래스를 요청하면 공장에서 DataSource를 구현하고 있는 객체를 나한테 리턴을 해줄 것입니다.
  • 그리고 나서 이제 DataSource한테 getConnection()이라는 메서드를 이용해가지고 Connection을 얻어오는 겁니다얻어온 Connection null이 아니라면 접속 잘 된 것 이므로 "접속 성공" 이라고 콘솔에 출력될 것입니다.
  • DataSourceTest를 실행시켜서 제대로 접속 성공이라고 나오면 실습이 성공한 겁니다.

 

ApplicationConfig.java

package kr.or.connect.daoexam.config;

 

import org.springframework.context.annotation.ComponentScan;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Import;

 

@Configuration

@Import({DBConfig.class})

public class ApplicationConfig {

 

}

 

DBConfig.java

package kr.or.connect.daoexam.config;

 

import javax.sql.DataSource;

 

import org.apache.commons.dbcp2.BasicDataSource;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.transaction.annotation.EnableTransactionManagement;

 

@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 = "connect123!@#";

 

    @Bean

    public DataSource dataSource() {

          BasicDataSource dataSource = new BasicDataSource();

        dataSource.setDriverClassName(driverClassName);

        dataSource.setUrl(url);

        dataSource.setUsername(username);

        dataSource.setPassword(password);

        return dataSource;

 

    }

}

 

DataSourceTest.java

package kr.or.connect.daoexam.main;

 

import java.sql.Connection;

 

import javax.sql.DataSource;

 

import org.springframework.context.ApplicationContext;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

 

import kr.or.connect.daoexam.config.ApplicationConfig;

 

public class DataSourceTest {

 

           public static void main(String[] args) {

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

                                             }

                                  }

                      }

           }

 

}

 

실습코드(DTO, DAO, SELECT)

  • DTO 클래스를 위치시키기 위한 패키지를 생성합니다.(kr.or.connect.daoexam.dto)
  • 그리고 Role이라는 DTO 클래스를 생성합니다. Roleint 값으로 roleID라는 값을 하나 가지고 있고 String으로 description을 필드를 가지고 있는 객체입니다. 필드를 작성하면 해당 객체를 이용해서 실제 값들을 넣거나, 꺼내기 위한 getter, setter 메서드도 작성합니다.
  • 마지막으로 객체가 가진 값들을 문자열로 한 번에 보여주기 위해 toString()도 작성합니다.

 

Role.java

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 + "]";

           }

          

}

 

  • 그 다음으로 sql, query 문을 갖고 있는 RoleDaoSqls라는 클래스를 생성합니다.
  • 해당 클래스는 dao라고 하는 패키지(kr.or.connect.daoexam.dao)를 생성하고 안에다가 RoleDaoSqls 클래스를 생성합니다.
  • 그리고 클래스에서 사용하고자 하는 query를 상수 형태로 public static final 해서 상수로 지정하시면 됩니다. 항상 상수는 모든 글자가 대문자로 쓰는 것이 관례이며 모두 대문자이기 때문에 두 단어 이상 됐을 때 '_'를 이용해서 단어를 구분해주는 이런 역할을 수행합니다.

 

RoleDaoSqls.java

package kr.or.connect.daoexam.dao;

 

public class RoleDaoSqls {

           public static final String SELECT_ALL = "SELECT role_id, description FROM role order by role_id";

}

 

  • 이제 데이터를 액세스할 수 있는 오브젝트, DAO를 만들겠습니다. 이번에는 Role이라는 객체에 대한 DAO를 만들 거니까 RoleDao, 이렇게 하나 만듭니다.
  • 이런 DAO 객체에는 저장소의 역할을 한다 이런 의미로 @Repository라는 어노테이션을 붙입니다.
  • DAO는 실행할 때 NamedParameterJdbcTemplate이라든가 SimpleJdbcInsert 같은 객체들을 이용해요. 이 객체들은 Spring JDBC가 실제 JDBC를 조금 편하게 하기 위해서 이미 구현해 놓은 객체들입니다. 현재 우리는 selectAll()을 수행하기 위해 select를 사용할 것이며 이를 위해 NamedParameterJdbcTemplate 객체를 사용합니다. 해당 객체의 메서드인 query(), queryForObject(), update() 이런 메서드를 실행시키기 위해서는 이 객체가 필요하므로 이 객체를 선언해야 합니다.
  • 가장 중요한 것은 지금 필드로 선언되어 있는 NamedParameterJdbcTemplate인NamedParameterJdbcTemplate은 이름을 이용해서 바인딩 하거나, 결과 값을 가져올 때 사용할 수 있습니다.
  • 그 다음에 생성자 부분을 살펴보면 DataSource를 파라미터로 쓰고 있는데 Spring 버전 4.3부터는 ComponentScan으로 객체를 찾았을 때 생성자의 파라미터 객체가 없다면 자동으로 객체를 주입해줍니다. 여기서는 DBConfig에서 Bean으로 등록했던 DataSource가 파라미터로 전달이 됩니다. 그리고 이 DataSource를 받아들여 가지고 NamedParameterJdbcTemplate 객체를 생성하게 됩니다.
  • 다음은 selectAll 메서드를 정의할 차례인데 이 메서드는 DTO인 Role을 여러 건 가져올 거니까 해당 Role List로 받아야 합니다. 실제 가져오는 작업은 NamedParameterJdbcTemplate이 다 해줄 거라고 했죠. 해당 객체가 가지고 있는 메서드 중에 지금 query()라는 메서드를 실행시키게 하면 됩니다.
  • query() 첫 번째 파라미터는 실제 query 문입니다. 그리고 아까 query 문을 어디다 작성해 놓았었냐면 RoleDaoSqls에 했습니다. 여기 들어있는 query를 사용하기 위해서 import static을 사용할 것인데 import static를 사용하면 RoleDaoSqls 객체에 선언된 변수를 클래스 이름 없이 바로 사용할 수 있도록 해주는 명령어입니다.
  • 두 번째 파라미터에는 조건절에서 사용할 객체를 넣습니다. 예를 들면 WHERE 절에 사용할 내용들이 해당됩니다. 여기서는 필요로 하지 않으니 비어있는 맵 객체를 하나 선언하면 됩니다. Collections.EmptyMap() 같은 것을 선언하면 됩니다.
  • 세 번째 파라미터에는 만들어놓은 rowMapper라는 객체를 전달하면 됩니다. sql 문에 바인딩 할 값이 있을 경우에 바인딩 할 값을 전달할 목적으로 사용하고 있는 객체입니다. 그러니까 세 번째 파라미터는 select 한 건, 한 건의 결과를 DTO에 저장하는 목적으로 사용을 하게 됩니다. 그러니까 BeanPropertyRowMapper 객체를 이용해서 column의 값을 자동으로 DTO에 담아주게 됩니다. 그리고 query() 메서드는 결과가 여러 건이었을 때 내부적으로 반복하면서 DTO를 생성하고 생성한 DTO List에다가 담아주는 일을 하여 해당 List를 반환해줍니다.
  • DBMS에서는 column 명이 단어와 단어를 구분할 때 '_'를 사용하고, Java에서는 camel 표기법을 이용해서 단어와 단어가 만날 때 대문자를 사용합니다. 예를 들면 roleId 같은 경우에 DBMS에서는 coulmn 명을 role_id처럼 '_'를 이용해서 적었다면 Java에서는 roleId 처럼 사용합니다. DBMS는 대소문자를 거의 구분하고 있지 않기 때문에 camel 표기법 같은 것을 써봤자 별로 구분이 안됩니다. 반면 자바는 대소문자를 구분하기 때문에 camel 표기법을 이용합니다. 그런데 이 경우 자바와 DBMS 둘의 이름이 달라서 매치가 힘듭니다. 이를 위해 BeanPropertyRowMapperDBMS Java의 이름 규칙을 맞춰주는 기능을 가지고 있습니다.

 

RoleDao.java

package kr.or.connect.daoexam.dao;

 

import static kr.or.connect.daoexam.dao.RoleDaoSqls.*;

 

import java.util.Collections;

import java.util.List;

import java.util.Map;

 

import javax.sql.DataSource;

 

import org.springframework.dao.EmptyResultDataAccessException;

import org.springframework.jdbc.core.BeanPropertyRowMapper;

import org.springframework.jdbc.core.RowMapper;

import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;

import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;

import org.springframework.jdbc.core.namedparam.SqlParameterSource;

import org.springframework.jdbc.core.simple.SimpleJdbcInsert;

import org.springframework.stereotype.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);

           }

 

}

 

  • 어노테이션을 이용해서 Bean ApplicationConfig에다가 등록하기 위해 @ComponentScan 어노테이션을 설정합니다. 그래야 설정 파일을 읽어낼 때 약속된 어노테이션이 붙어있는 객체들을 찾아내서 작업을 진행할 수 있습니다. ComponentScan을 쓰실 때는 basePackages를 지정하서 써야합니다. basePackages로 중괄호를 이용하면 패키지를 여러 개 나열해서도 사용할 수 있습니다.
  • @ComponentScan=(backPackages={"kr.or.connect.daoexam.dao"})
    이 부분을 추가시켰기 때문에 자동으로 Repository가 붙어있는 클래스를 Bean으로 등록합니다.

 

ApplicationConfig.java 에 추가

@ComponentScan(basePackages = { "kr.or.connect.daoexam.dao" })

 

  • 테스트를 위해 SelectAllTest 클래스를 하나 만들어 보겠습니다.
  • AnnotationConfigApplicationContext 를 생성하고 ApplicationContext로부터 getBean() 해서 roleDao를 얻어와서selectAll() 메서드를 수행합니다.
  • 결과가 MySQL의 select문과 일치하는지 비교해봅니다.만약 실행을 시켜봤더니 결과 값이 나오는데 잘못 나오는 경우가 있습니다. description의 값은 제대로 나온 것 같은데 roleId가 다 0인 경우같이요. 이랬을 때는 mysql에 접속하셔가지고 진짜 들어있는 데이터를 한번 확인해볼 필요가 있겠죠. select * from role 실행을 시켜볼게요. 그랬더니 role_id 100, 101, 102, 201 이렇게 제대로 들어있는데 출력된 값은 잘못 가져온 것을 알 수가 있죠. 왜 그랬을까요? column 이름을 한번 볼게요. column 이름이 다 소문자로 지금 role_id 이렇게 돼있거든요. 그러면 이것을 Java 방식으로 바꿔본다면 role_id가 어떻게 쓰여졌겠어요? roleId, 이런 식으로 사용이 됐겠죠. 그러면 우리의 DTO를 잠깐만 봐볼까요? dto 패키지 안에서 DTO 파일에 가서 확인을 해보도록 할게요. 확인을 해봤더니 웬일이에요. role에 오타도 있었고요. ID도 뒤에 대문자가 되면 안 됐겠죠. 지금 이 부분 때문에 문제가 발생을 한 거예요.
  • 여러분들도 문제가 발생하시면 이런 부분들을 잘 찾아보셔야 돼요. roleId 이렇게 들어가 있어야지 될 것 같죠? 필드명이 바뀌면 setter, getter 메서드들도 대소문자 바뀌는 부분들이 문제가 조금 있으니까 setter, getter 메서드는 다시 생성하게 해줄게요. 오히려 그게 오류를 발생시키지 않을 수 있을 것 같아요. 이렇게 잘 수정하셨으면 다시 한번 실행시켜보도록 할게요.
  • 잘 수정하시고 실행하셨더니 이번에는 값이 제대로 나오고 있는 것을 볼 수 있겠죠. 항상 수행하시다가 오류가 발생하거나 이랬을 때 겁내지 마시고 잘 찾아보시면 충분히 이유를 찾아내실 수 있으실 것 같아요. 이렇게 실행하니까 selectAll 하는 메서드를 실행해볼 수 있었고요. Role이라고 하는 테이블에서 모든 데이터를 꺼내오는 예제를 실행해보았습니다.

 

SelectAllTest.java

package kr.or.connect.daoexam.main;

 

import java.util.List;

 

import org.springframework.context.ApplicationContext;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

 

import kr.or.connect.daoexam.config.ApplicationConfig;

import kr.or.connect.daoexam.dao.RoleDao;

import kr.or.connect.daoexam.dto.Role;

 

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

                      }

 

           }

 

}

 

실습코드(INSERT, UPDATE, DELETE)

  • insert 문을 실행하기 위해서는 SimpleJdbcInsert라는 객체가 필요합니다. 그래서 SimpleJdbcInsert를 추가해서 선언하고 NamedParameterJdbcTemplate과 마찬가지로 생성자에서 생성을 합니다. 생성자 부분에서는 insertAction 에다가 SimpleJdbcInsert를 생성을 하는데 dataSource를 넣어서 생성을 하고 withTableName()의 파라미터에 데이터를 넣을 테이블을 입력하면 됩니다. 여기선 role테이블을 사용합니다.
  • 다음에 할 일은 insert() 메서드를 구현하는 것입니다. BeanPropertySqlParameterSource 객체를 생성해서 Role 객체를 파라미터로 받은 뒤 해당 Role 객체에 있는 값을 외부로 바꿔주는데 이때 우리가 이때 선언한 roleId columnrole_id로 알아서 맵 객체를 생성해줄 겁니다. 이렇게 생성한 맵 객체를 SimpleJdbcInsert가 가지고 있는 execute()라는 메서드의 파라미터로 전달을 할 경우에 값이 알아서 저장됩니다.

 

RoleDao.java

package kr.or.connect.daoexam.dao;

 

import static kr.or.connect.daoexam.dao.RoleDaoSqls.*;

 

import java.util.Collections;

import java.util.List;

import java.util.Map;

 

import javax.sql.DataSource;

 

import org.springframework.dao.EmptyResultDataAccessException;

import org.springframework.jdbc.core.BeanPropertyRowMapper;

import org.springframework.jdbc.core.RowMapper;

import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;

import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;

import org.springframework.jdbc.core.namedparam.SqlParameterSource;

import org.springframework.jdbc.core.simple.SimpleJdbcInsert;

import org.springframework.stereotype.Repository;

 

import kr.or.connect.daoexam.dto.Role;

@Repository

public class RoleDao {

           private NamedParameterJdbcTemplate jdbc;

           private SimpleJdbcInsert insertAction;

           private RowMapper<Role> rowMapper = BeanPropertyRowMapper.newInstance(Role.class);

 

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

           }

 

}

 

  • 테스트를 위해 JDBCTest 클래스를 생성합니다. ApplicationContext로부터 getBean()을 통해 RoleDao를 얻어오고 insert 문을 수행하기 전에 DTO 객체인 Role를 생성한 뒤 setter를 통해 값을 넣어줍니다. RoleId 500번이라고 넣고 Description "CEO" 라고 넣겠습니다.
  • 그리고 insert() 메서드를 수행하면 int 형태의 리턴 값이 나오는데 이 부분은 query를 수행하여 들어간 결과 값의 수입니다. 이 숫자를 count 변수에 넣겠습니다.
  • 콘솔에 출력하도록 코딩하고 실행시킨 뒤 조회하여 값이 잘 들어갔는지 확인해봅니다.
  • select 결과 화면

 

JDBCTest.java

package kr.or.connect.daoexam.main;

 

import org.springframework.context.ApplicationContext;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

 

import kr.or.connect.daoexam.config.ApplicationConfig;

import kr.or.connect.daoexam.dao.RoleDao;

import kr.or.connect.daoexam.dto.Role;

 

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 + "건 입력하였습니다.");

                      }

 

}

 

  • update 문은 query 문이 필요합니다. RoleDaoSqls.java에 필요한 query를 추가합니다. 이때 query를 잠깐 보시면 : 하고 나와있는 부분이 있습니다. 이 부분이 나중에 값으로 바인딩 될 부분입니다.

 

RoleDaoSqls.java 에 추가

package kr.or.connect.daoexam.dao;

 

public class RoleDaoSqls {

           public static final String SELECT_ALL = "SELECT role_id, description FROM role order by role_id";

           public static final String UPDATE = "UPDATE role SET description = :description WHERE role_id = :roleId";

}

 

  • 그 다음엔 RoleDao에다가 update 문의 메서드를 생성합니다. NamedParameterTemplate이 가지고 있는 update()라는 메서드를 사용하면 되고 첫 번째 파라미터는 sql, 두 번째 파라미터는 맵 객체를 넣습니다. 맵 객체는 SET과 WHERE절에서 넣어야 하는 매핑해야 하는 값들을 가진 객체라고 생각하면 됩니다.

 

RoleDao.java

package kr.or.connect.daoexam.dao;

 

import static kr.or.connect.daoexam.dao.RoleDaoSqls.*;

 

import java.util.Collections;

import java.util.List;

import java.util.Map;

 

import javax.sql.DataSource;

 

import org.springframework.dao.EmptyResultDataAccessException;

import org.springframework.jdbc.core.BeanPropertyRowMapper;

import org.springframework.jdbc.core.RowMapper;

import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;

import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;

import org.springframework.jdbc.core.namedparam.SqlParameterSource;

import org.springframework.jdbc.core.simple.SimpleJdbcInsert;

import org.springframework.stereotype.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);

           }

 

           public int insert(Role role) {

                      SqlParameterSource params = new BeanPropertySqlParameterSource(role);

                      return insertAction.execute(params);

           }

 

           public int update(Role role) {

                      SqlParameterSource params = new BeanPropertySqlParameterSource(role);

                      return jdbc.update(UPDATE, params);

           }

 

}

 

  • JDBCTest에서 update를 테스트해보겠습니다. Insert 부분이 또 실행되면 오류를 발생시킬 테니까 주석처리합니다.
  • select 결과 중 201번의 description null이므로 이 부분을 "PROGRAMMER"로 수정하겠습니다.
  • Role 객체에 원하는 값을 넣고 roleDao.update()를 실행합니다.
  • 실행했더니 "1 건 수정하였습니다." 하고 결과가 잘 나오고 있는 것을 볼 수 있습니다. 그리고 실제 데이터베이스에 가서 select 문을 수행했더니 201 번인 데이터가 "PROGRAMMER" 하고 제대로 수정이 되어있는 것을 확인할 수가 있습니다.

 

JDBCTest.java

package kr.or.connect.daoexam.main;

 

import org.springframework.context.ApplicationContext;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

 

import kr.or.connect.daoexam.config.ApplicationConfig;

import kr.or.connect.daoexam.dao.RoleDao;

import kr.or.connect.daoexam.dto.Role;

 

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

                      role.setDescription("PROGRAMMER");

                     

                      /*int count = roleDao.insert(role);

                      System.out.println(count + "건 입력하였습니다.");*/

                                 

                      int count = roleDao.update(role);

                      System.out.println(count +  " 건 수정하였습니다.");

           }

 

}

 

실습코드(1SELECT, DELETE)

  • 1건 select delete를 실습해보겠습니다.
  • 먼저 1 select 하는 것과 delete 하는 query 문을 RoleDaoSqls에 추가합니다. SELECT_BY_ROLE_ID라는 queryDELETE_BY_ROLE_ID 이라고 추가합니다.
  • 이번에도 : 뒤에 오는 값을 나중에 실행할 때 파라미터로 가져온 값으로 바인딩 한다는 사실을 기억하면 좋습니다.
  • sql 실습하면서 select 문에다가 column들을 다 나열할 때 * 을 사용하였지만 실제로 column 명을 정확하게 나열하시는 것이 훨씬 의미 전달이 명확할 수가 있습니다.

 

RoleDaoSqls.java에 추가

public static final String SELECT_BY_ROLE_ID = "SELECT role_id, description FROM role where role_id = :roleId";

public static final String DELETE_BY_ROLE_ID = "DELETE FROM role WHERE role_id = :roleId";

 

  • delete 문은 앞서 update 구문과 크게 다르지 않습니다. NamedParameterJdbcTemplate이 갖고 있는 update 메서드를 실행하시면 되고 첫 번째 파라미터에는 query 문이, 두 번째 파라미터에는 Map 객체가 들어오면 됩니다.
  • update와 같이 DTO 객체를 넣어서 Map으로 바꿔주는 SqlParameterSource 같은 것을 사용해도 되지만 delete 같은 경우에는 SQL의 파라미터 값으로 딱 하나만 들어오므로 굳이 SqlParameterSource 객체를 만들어서 쓰기는 조금 복잡할 수도 있습니다.
  • 그래서 지금 쓰는 Collections.singletonMap 같은 경우는 파라미터 값이 여러 개 들어가지 않고 딱 1 건만 넣어서 사용할 때 이렇게도 사용할 수 있습니다.
  • 마지막으로 1 select 하는 경우를 보겠습니다. 여러 건 select 하는 것과 메서드가 다릅니다. 1 select 할 때는 queryForObject라는 메서드를 이용합니다. 첫 번째 파라미터에는 query가 들어오고 두 번째 파라미터로는 sql에 넘길 roleId를 담은 Map이 전달이 됩니다. 만약 1 select 할 때 조건에 맞는 값이 없으면 Exception이 발생하게 됩니다.
  • 이럴 경우에는 그냥 null을 리턴하도록 try-catch를 구현하였습니다. 이 부분을 주의해서 사용해야 합니다. select를 했는데 해당 조건에 맞는 값이 없을 때 결과 값이 제대로 넘어오지 않을 수 있기 때문에 이럴 경우에 Exception이 발생하기 때문에 적절하게 Exception 처리를 해야 합니다.

 

RoleDao.java에 추가

package kr.or.connect.daoexam.dao;

 

import static kr.or.connect.daoexam.dao.RoleDaoSqls.*;

 

import java.util.Collections;

import java.util.List;

import java.util.Map;

 

import javax.sql.DataSource;

 

import org.springframework.dao.EmptyResultDataAccessException;

import org.springframework.jdbc.core.BeanPropertyRowMapper;

import org.springframework.jdbc.core.RowMapper;

import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;

import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;

import org.springframework.jdbc.core.namedparam.SqlParameterSource;

import org.springframework.jdbc.core.simple.SimpleJdbcInsert;

import org.springframework.stereotype.Repository;

 

import kr.or.connect.daoexam.dto.Role;

@Repository

public class RoleDao {

           private NamedParameterJdbcTemplate jdbc;

           private SimpleJdbcInsert insertAction;

           private RowMapper<Role> rowMapper = BeanPropertyRowMapper.newInstance(Role.class);

 

           public RoleDao(DataSource dataSource) {

                      this.jdbc = new NamedParameterJdbcTemplate(dataSource);

                      this.insertAction = new SimpleJdbcInsert(dataSource)

                .withTableName("role");

           }

          

           public int deleteById(Integer id) {

                      Map<String, ?> params = Collections.singletonMap("roleId", id);

                      return jdbc.update(DELETE_BY_ROLE_ID, params);

           }

          

           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;

                      }

           }

 

}

 

  • 첫 번째로 resultRole이라는 변수를 선언하고 selectById()의 리턴을 저장하도록 합니다. Role이 제대로 된 값을 가지고 들어왔는지 출력해봅니다.
  • 두 번째로 delete를 하는데 roleDao.deleteById() 메서드를 호출합니다. 500번을 delete 하도록 합니다. delete 하는 것도 int 값을 리턴하므로 int deleteCount 변수에 값을 저장합니다. 그런 다음 콘솔에 deleteCount + "건 삭제하였습니다." 와 같이 출력하도록 합니다.
  • 마지막으로 제대로 delete가 됐는지 알아보기 위해서 selectById()에다가 500 번인 것을 넣어서 테스트 해봅니다.
  • 테스트 결과 조회, 삭제, 그리고 없는 레코드에 대한 exception 처리가 작동하여 콘솔에 출력된 것을 알 수 있습니다.

 

JDBCTest.java에 추가

Role resultRole = roleDao.selectById(201);

System.out.println(resultRole);

                     

int deleteCount = roleDao.deleteById(500);

System.out.println(deleteCount + "건 삭제하였습니다.");

          

Role resultRole2 = roleDao.selectById(500);

System.out.println(resultRole2);

 

생각해보기

  • JdbcTemplate을 이용하지 않고 NamedParameterJdbcTemplate를 이용하여 DAO를 작성한 이유는 무엇이라고 생각하나요?
    → NamedParameterJdbcTemplate
    는 전통적인 JDBC "?" 플레이스홀더 대신에 이름있는(named) 파라미터를 제공하기 위해서 JdbcTemplate를 감싼다. 이 접근방법은 더 좋은 문서화를 제공하고 SQL 문에 다중 파라미터가 있을 때 사용하기가 쉽다.
    출처: https://blog.outsider.ne.kr/882

참고 자료

[참고링크] Data Access Object Pattern
https://www.tutorialspoint.com/design_pattern/data_access_object_pattern.htm

[참고링크] Using JDBC to Connect to a Database
https://ejbvn.wordpress.com/category/week-2-entity-beans-and-message-driven-beans/day-09-using-jdbc-to-connect-to-a-database/

 

'부스트코스 웹 프로그래밍 > 3. 웹 앱 개발: 예약서비스 1' 카테고리의 다른 글

9. Spring MVC - BE (2)  (0) 2019.08.06
9. Spring MVC - BE (1)  (0) 2019.08.06
8. Spring JDBC - BE (1)  (0) 2019.08.04
7. Spring Core - BE (4)  (0) 2019.08.04
7. Spring Core - BE (3)  (0) 2019.08.03
Comments