티스토리 뷰

2) 레이어드 아키텍처(Layered Architecture) 실습

방명록 만들기 실습

  • Spring JDBC를 이용한 Dao 작성
  • Controller + Service + Dao
  • 트랜잭션 처리
  • Spring MVC에서 폼 값 입력받기
  • Spring MVC에서 redirect하기
  • Controller에서 jsp에게 전달한 값을 JSTL, EL을 이용해 출력하기

방명록 요구사항

  설명

결과 화면 및 관련 테이블 쿼리

1
  • 방명록 정보는 guestbook 테이블에 저장된다.
  • id 컬럼은 자동으로 입력된다.(id 컬럼은 순번을 나타내기 위한 것)
  • id, 이름, 내용, 등록일을 지정한다.

CREATE TABLE guestbook (

  id bigint(20) unsigned NOT NULL AUTO_INCREMENT,

  name varchar(255) NOT NULL,

  content text,

  regdate datetime,

  PRIMARY KEY (id)

);

2
  • http://localhost:8080/guestbook/ 을 요청하면 자동으로 /guestbook/list 로 리다이렉팅한다.
  • 방명록이 없으면 건수는 0이 나오고 아래에 방명록을 입력하는 폼이 보여진다.
3
  • 입력 폼에서 이름과 내용을 입력하고 등록버튼을 누르면 /guestbook/write URL로 입력한 값을 전달하여 저장한다.
  • 값이 저장된 이후에는 /guestbook/list/로 리다이렉트 된다.
4
  • 입력한 한건의 정보가 보여진다. (캡쳐화면의 이미지는 입력된 방명록을 삭제한 이후에 입력해서 ID 15가 되었다. ID값은 순번에 불과합니다.)
  • 방명록 내용과 폼 사이의 숫자는 방명록 페이지 링크이며 방명록 5건당 1페이지로 설정한다.
5
  • 방명록이 6건 입력되자 아래 페이지 수가 2건 보여진다. 1페이지를 누르면 /guestbook/list?start=0을 요청하고, 2페이지를 누르면 /guestbook/list?start=5를 요청하게 된다.
  • /guestbook/list /guestbook/list?start=0과 결과가 같다. start 값을 가져가지 않으면 초기화면과 같다는 의미이다.
6
  • 방명록에 글을 쓰거나, 방명록의 글을 삭제할 때는 Log테이블에 클라이언트의 ip주소, 등록(삭제) 시간, 등록/삭제(method 컬럼) 정보를 데이터베이스에 저장한다.
  • 사용하는 테이블은 log이다.
  • id는 자동으로 입력되도록 한다.

CREATE TABLE log (

  id bigint(20) unsigned NOT NULL AUTO_INCREMENT,

  ip varchar(255) NOT NULL,

  method varchar(10) NOT NULL,

  regdate datetime,

  PRIMARY KEY (id)

);

 

방명록 클래스 다이어그램 

  • 웹 레이어 설정 파일: web.xml, WebMvcContextConfiguration.java
  • 비즈니스, 리포지토리 레이어 설정 파일: ApplicationConfig.java, DBConfig.java

 

  • 이번 방명록 웹 애플리케이션에서는 설정 파일이 그림처럼 네 개가 필요합니다.
  • 먼저 web.xml 파일에서는 두 개의 자바 config 파일에 대해서 설정합니다.
  • 하나는 DispatcherServlet이 읽어들이는 WebMvcContextConfiguration 이고 또 하나는 ApplicationContextListener가 읽어들일 ApplicationConfig 입니다. 그리고 ApplicationConfig 파일에서는 DBConfig import 합니다.

  • 방명록 웹 애플리케이션에서는 URL 요청을 처리하는 핸들러인 GuestbookController가 있고 해당 컨트롤러는 비즈니스 로직을 가지고 있는 서비스 객체를 사용합니다. 해당 서비스 객체는 GuestbookService라는 인터페이스를 구현한 GuestbookServiceImpl이라고 하는 구현 클래스로 구성을 하게 될 것입니다. GuestbookServiceImpl에서는 logDao GuestbookDao를 이용해서 비즈니스 로직을 수행합니다.
  • LogDao는 저장만 하고 나머지 작업들은 수행하지 않습니다. 그렇기 때문에 별도로 sql은 필요가 없습니다.
  • GuestbookDao에서는 여러 개의 sql 작업을 수행하기 때문에 별도의 sql 파일들이 필요합니다. 그런 sql들은 GuestbookDaoSqls 파일에다가 sql을 관리하게 해줄 겁니다.
  • 그리고 각각 사용할 DTO도 준비합니다.
  • 뷰의 역할을 수행하게 하기 위해서 list.jsp를 작성합니다. index.jsp redirect 용도로 사용할 것입니다.

실습(Configuration 설정)

  • maven 프로젝트를 생성합니다. archetype은 webapp, artifactid는 guestbook이라 설정합니다.
  • 생성하고 나면 pom.xml에 프로퍼티에 utf-8 설정과 spring.version 4.3.5.RELEASE을 등록하고 jackson2.version 2.8.6도 등록을 합니다. spring-context, spring-webmvc, servlet 3.1, jsp 2.3.1, jstl 1.2, spring-jdbc, spring-tx(스프링 트랜잭션), mysql-connector-java 5.1.45, commons-dbcp2 2.1.1(datasource를 사용하기 위한 부분), jackson-databind, jackson-datatype-jdk8 의 라이브러리를 추가하고 jdk를 추가하기 위해 build plugin에 내용을 입력합니다. 그리고 저장 후에 메이븐 업데이트를 합니다.

 

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/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>kr.or.connect</groupId>
  <artifactId>guestbook</artifactId>
  <packaging>war</packaging>
  <version>0.0.1-SNAPSHOT</version>
  <name>guestbook Maven Webapp</name>
  <url>http://maven.apache.org</url>
  <properties>
    <project.build.sourceEncording>UTF-8</project.build.sourceEncording>
    <spring.version>4.3.5.RELEASE</spring.version>
    <!-- jackson -->
    <jackson2.version>2.8.6</jackson2.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <scope>provided</scope>
    </dependency>

    <!-- https://mvnrepository.com/artifact/javax.servlet.jsp/javax.servlet.jsp-api -->
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>javax.servlet.jsp-api</artifactId>
      <version>2.3.1</version>
      <scope>provided</scope>
    </dependency>

    <!-- https://mvnrepository.com/artifact/jstl/jstl -->
    <dependency>
      <groupId>jstl</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-tx -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.45</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-dbcp2 -->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-dbcp2</artifactId>
      <version>2.1.1</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>${jackson2.version}</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jdk8 -->
    <dependency>
      <groupId>com.fasterxml.jackson.datatype</groupId>
      <artifactId>jackson-datatype-jdk8</artifactId>
      <version>${jackson2.version}</version>
    </dependency>

  </dependencies>
  <build>
    <finalName>guestbook</finalName>
    <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>

 

  • Navigator에서 main 밑에 java폴더 만들고 웹 모듈 바꿔준 뒤 이클립스를 재시작합니다. 이 후 Properties - Project Facets 에서 Dynamic Web Module 확인을 합니다.
  • 먼저 설정 파일들을 생성합니다. 설정 파일 패키지인 kr.or.connect.guestbook.config 를 생성합니다.
  • 그리고 WebMvcContextConfiguration 클래스를 하나 생성합니다. 해당 클래스는 WebMvcConfigurerAdapter를 상속받도록 합니다.
  • 파일이 생성되면 설정파일임을 알려주기 위해서 @Configuration 어노테이션을 설정하고 기본적인 설정들을 자동으로 하기 위해서 @EnableWebMvc 어노테이션 사용합니다. 그다음 controller 클래스를 자동으로 읽어오도록 @ComponentScan 어노테이션을 사용합니다. 마지막으로 기본 설정 외의 설정을 위해 상속받은 클래스에서 필요한 메서드들을 오버라이드합니다.
  • addResourceHandlers 메서드를 오버라이딩하여 /css, /img, /js 이렇게 들어오는 URL 요청을 처리할 수 있도록 설정합니다.

 

  • 그다음 configureDefaultServletHandling 메서드를 오버라이딩하여 default servlet handler를 사용해서 매핑 정보가 없는 URL 요청을 SpringDefaultServletHttpRequsetHandler가 처리하도록 설정합니다. 이 부분을 통해 매핑이 없는 URL이 넘어왔을 때 WAS default servlet static 한 자원을 읽어서 보여줄 수 있게끔 설정합니다.
  • addViewController 메서드는 특정 URL에 대한 처리를 컨트롤러 클래스를 작성하지 않고 매핑할 수 있도록 해주는 부분입니다. "/"라고 요청이 들어오면 "index"라는 이름의 뷰로 보여주도록 설정합니다.

 

  • getInternalResourceViewResolver 메서드를 오버라이딩하여 view resolver가 뷰의 이름을 가지고 어떤 뷰인지에 대한 정보를 얻을 수 있게 합니다. 여기서는 resolver에다가 Prefix Suffix를 지정하게 함으로써 해당 뷰의 정확한 위치를 알려줄 수 있도록 합니다.

 

  • 이제 데이터베이스에 관련된 설정을 위해 DBConfig.java 클래스를 생성합니다. 이 클래스는 TransactionManagementConfigurer를 구현합니다.
  • 역시 설정파일이므로 @Configuration이라는 어노테이션을 설정합니다. 그다음에 @EnableTransactionManagement 라는 어노테이션을 설정합니다. 이 어노테이션은 트랜잭션과 관련된 설정을 자동으로 해줍니다. , 사용자 간의 트랜잭션 처리를 위한 PlatformTransactionManager를 설정하기 위해서는 TransactionManagementConfiugurer를 구현하고 annotationDrivenTransactionManager 메서드를 오버라이딩 해야 됩니다. 그리고 해당 메서드에서는 트랜잭션을 처리할 PlatformTransactionManager 객체를 반환하게 하면 됩니다.

 

  • DB에 연결하기 위한 접속정보를 필드로 선언하고 DataSource를 사용하기 위해 Bean을 등록합니다.

 

  • ApplicationConfig 클래스도 작성합니다. 설정 파일이니까 @Configuration을 설정합니다. 여기서는 dao service에 구현되어 있는 컴포넌트들을 읽어오니까 @ComponentScanbasePackages에다가 각각의 패키지를 지정하고 있습니다. 그리고 DBConfig에 설정된 내용을 가져오기 위해 @Import 어노테이션을 사용합니다.

 

  • 마지막으로 web.xml에다가 필요한 부분들을 설정합니다.
  • 맨 윗줄에 웹 모듈 2.3 지정되어 있던 것을 수정해줍니다.
  • servlet-mapping에 url-pattern을 / 로 설정하여 모든 요청을 받을 수 있도록 합니다.

 

  • 그리고 DispatcherServlet을 프론트 서블릿으로 등록합니다.

 

  • init-param을 통해 사용할 ApplicationContext를 설정합니다. 그리고 DispatcherServlet이 실행될 때 사용할 설정 파일의 위치를 지정합니다.

 

  • 그리고 나서 아까 나누었던 설정 파일에 대한 설정을 합니다. 레이어드 아키텍처의 특징 상 하나가 다 갖고 있게 하지 않기에 프레젠테이션 부분과 나머지 부분들을 분리시켰습니다. 지금 비즈니스 로직 쪽에서 사용되는 것은 DBConfig ApplicationConfig에 설정되어 있으니까 이 부분을 읽어들일 수 있도록 해야 합니다.
  • 해당 부분을 읽어들일 수 있도록 하는 것이 listener입니다. listener는 어떤 특정한 이벤트가 일어났을 때 동작하는 것입니다. 지금 등록되어 있는 리스너는 ContextLoaderListener 입니다. 이것은 run on server 를 수행할 때, Context가 로딩될 때 ContextLoaderListener를 읽어서 수행한다는 의미입니다. 즉, Context가 로딩되는 이벤트가 일어났을 때 해당 클래스를 실행시키는 리스너입니다.

 

  • ContextLoaderListener가 실행되면 리스너를 등록하기 전에 등록한 context-param을 참고하게 됩니다. contextConfigLocation에 ApplicationConfig가 매핑되어 있는데 이름과 클래스 위치를 입력하면 됩니다.
  • 어쨌든 Context가 로딩될 때 이 리스너가 실행되는데 이 리스너가 실행될 때 설정 파일인 ApplicaionConfig를 읽어서 실행하게 됩니다. 그리고 contextClass AnnotationCofigWebApplicationContext가 ApplicationContext로써 사용된다는 설정입니다.

 

  • 마지막으로 filter를 추가합니다. filter는 요청이 수행되기 전, 응답이 나가기 전이때 한 번씩 걸쳐서 수행하도록 해주는 부분인데요. 이 애플리케이션에는 Spring이 제공하고 있는 CharacterEncodingFilter를 하나 등록하려고 합니다. 해당 부분은 한글 인코딩 처리를 도와줍니다.
  • 대부분의 값들은 정해진 값이며 지정할 수 있는 부분은 param-value 뿐입니다. 인코딩을 뭘로 할 건지 설정할 수 있는 부분입니다. UTF-8로 하고 있었으니까 UTF-8로 맞춰주시면 됩니다. url-pattern은 이 필터를 어디까지 적용할지 설정하는 부분입니다. 모든 요청에 대해서 적용을 하려면 /* 과 같이 지정합니다. 또한 특정한 URL에만 지정할 수도 있습니다.

 

  • 여기까지 완료하면 webapp WEB-INF views 라는 디렉터리를 만들고 index.jsp 파일을 생성합니다. 해당 파일은 "list"라는 요청으로 redirect 하는 역할을 합니다.(메이븐을 만들 때 있었던 초기 index.jsp는 미리 삭제합니다.)

 

  • 실행 시켰을 때 URL이 "guestbook/list" 하고 나오면 addViewControllers 메서드와 index.jsp의 sendRedirect가 성공적으로 작동했다는 것입니다. 404가 나온 이유는 아직 list 요청에 대한 처리를 하지 않았기 때문입니다. 추후 작업이므로 크게 신경쓰지 않아도 됩니다. 여기까지 기본적인 설정들을 처리하였습니다.

실습코드

WebMvcContextConfiguration.java

package kr.or.connect.guestbook.config;

 

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;

import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;

import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import org.springframework.web.servlet.view.InternalResourceViewResolver;

 

@Configuration

@EnableWebMvc

@ComponentScan(basePackages = { "kr.or.connect.guestbook.controller" })

public class WebMvcContextConfiguration extends WebMvcConfigurerAdapter{

 

    @Override

    public void addResourceHandlers(ResourceHandlerRegistry registry) {

        registry.addResourceHandler("/css/**").addResourceLocations("/css/").setCachePeriod(31556926);

        registry.addResourceHandler("/img/**").addResourceLocations("/img/").setCachePeriod(31556926);

        registry.addResourceHandler("/js/**").addResourceLocations("/js/").setCachePeriod(31556926);

    }

 

    // default servlet handler를 사용하게 합니다.

    @Override

    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {

        configurer.enable();

    }

  

    @Override

    public void addViewControllers(final ViewControllerRegistry registry) {

                     System.out.println("addViewControllers가 호출됩니다. ");

        registry.addViewController("/").setViewName("index");

    }

   

    @Bean

    public InternalResourceViewResolver getInternalResourceViewResolver() {

        InternalResourceViewResolver resolver = new InternalResourceViewResolver();

        resolver.setPrefix("/WEB-INF/views/");

        resolver.setSuffix(".jsp");

        return resolver;

    }

}

 

DBConfig.java

package kr.or.connect.guestbook.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.jdbc.datasource.DataSourceTransactionManager;

import org.springframework.transaction.PlatformTransactionManager;

import org.springframework.transaction.annotation.EnableTransactionManagement;

import org.springframework.transaction.annotation.TransactionManagementConfigurer;

 

@Configuration

@EnableTransactionManagement

public class DBConfig implements TransactionManagementConfigurer {

           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;

           }

 

           @Override

           public PlatformTransactionManager annotationDrivenTransactionManager() {

                      return transactionManger();

           }

 

           @Bean

           public PlatformTransactionManager transactionManger() {

                      return new DataSourceTransactionManager(dataSource());

           }

}

 

ApplicationConfig.java

package kr.or.connect.guestbook.config;

 

import org.springframework.context.annotation.ComponentScan;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Import;

 

@Configuration

@ComponentScan(basePackages = { "kr.or.connect.guestbook.dao",  "kr.or.connect.guestbook.service"})

@Import({ DBConfig.class })

public class ApplicationConfig {

 

}

 

web.xml

<?xml version="1.0" encoding="UTF-8"?>

<web-app>

 

           <display-name>Spring JavaConfig Sample</display-name>

           <context-param>

                      <param-name>contextClass</param-name>

                      <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext

                      </param-value>

           </context-param>

           <context-param>

                      <param-name>contextConfigLocation</param-name>

                      <param-value>kr.or.connect.guestbook.config.ApplicationConfig

                      </param-value>

           </context-param>

           <listener>

                      <listener-class>org.springframework.web.context.ContextLoaderListener

                      </listener-class>

           </listener>

 

           <servlet>

                      <servlet-name>mvc</servlet-name>

                      <servlet-class>org.springframework.web.servlet.DispatcherServlet

                      </servlet-class>

                      <init-param>

                                  <param-name>contextClass</param-name>

                                  <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext

                                  </param-value>

                      </init-param>

                      <init-param>

                                  <param-name>contextConfigLocation</param-name>

                                  <param-value>kr.or.connect.guestbook.config.WebMvcContextConfiguration

                                  </param-value>

                      </init-param>

                      <load-on-startup>1</load-on-startup>

           </servlet>

           <servlet-mapping>

                      <servlet-name>mvc</servlet-name>

                      <url-pattern>/</url-pattern>

           </servlet-mapping>

 

           <filter>

                      <filter-name>encodingFilter</filter-name>

                      <filter-class>org.springframework.web.filter.CharacterEncodingFilter

                      </filter-class>

                      <init-param>

                                  <param-name>encoding</param-name>

                                  <param-value>UTF-8</param-value>

                      </init-param>

           </filter>

           <filter-mapping>

                      <filter-name>encodingFilter</filter-name>

                      <url-pattern>/*</url-pattern>

           </filter-mapping>

</web-app>

 

index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"

    pageEncoding="UTF-8"%>

<%

           response.sendRedirect("list");

%>

 

실습(DTO, DAO작성 및 repository layer 작성)

  • guestbook 테이블하고 log라는 테이블을 생성합니다.(혹시 모르니 show full columns from 테이블; 명령어로 컬럼이나 테이블의 charset을 확인합니다.)

 

  • dto 패키지를 생성하고 Guestbook Log DTO 클래스를 만듭니다.
  • Guestbook DTO에 테이블이 가지고 있는 필드들을 선언하고 각각의 getter, setter, toString 메서드를 만듭니다. 그리고 regdate필드는 Date 클래스를 타입으로 씁니다.
  • Log라는 클래스도 테이블이 가지고 있는 필드들을 선언하고 각각의 getter, setter, toString 메서드를 생성합니다.
  • 다음은 DAO 패키지를 만들고 LogDao.java를 생성합니다DAO 위에 @Repository 어노테이션을 설정합니다.
  • 생성자에서 usingGeneratedKeyColumns을 사용하고 있는데 이것은 id가 자동으로 입력되도록 하는 것입니다. 즉, id column의 값을 자동으로 입력하도록 설정하고 있는겁니다.
  • 그리고 insertAction.executeAndReturnKey 메서드는 insert 문은 내부적으로 생성해서 실행을 하고 자동으로 생성된 id 값을 리턴하게 됩니다. 이때 리턴된 값인 id를 사용할 수도 있겠죠.

 

  • 다음은 GusetbookDao에서 사용할 query들을 관리하는 GuestbookDaoSqls.java를 생성합니다. query를 상수로 지정하여 관리합니다.
  • limit를 사용하고 있는데 limit를 사용하시면 시작 값, 끝날 때의 값을 설정해서 수만큼 select 할 수 있습니다.

 

  • 그다음엔 GuestbookDao를 생성하고 @Repsoitory 어노테이션을 붙여줍니다. 그리고 import static을 사용하여 query 문들을 사용할 수 있도록 추가합니다. 또한 각각의 메서드들을 작성합니다.

 

  • 해당 부분이 잘 동작하는지 GuestbookDaoTest.java를 작성하여 테스트 해보겠습니다.
  • 참고로 이렇게 개발하고 있는 곳에 main 메서드를 가진 클래스를 이용하여 테스트를 하는 것이 좋지 않습니다.
  • 대신 JUnit 같은 단위 테스트를 할 수 있는 도구들을 사용하는 것이 좋습니다.
  • 어쨌든 여러분이 어떤 메서드를 하나씩 구현을 하고 나서 main 메서드를 이용해서라도 반드시 그 메서드가 제대로 동작하는지를 꼭 테스트하는 게 중요합니다.
  • 다시 돌아와서 테스트하기 위해서 ApplicationContext를 준비하고 GuestbookDao를 getBean()으로 불러내 DAO에서 구현한 메서드가 작동하는지 확인합니다. insert를 호출해보겠습니다.

 

  • 이에 대한 동작 결과로 id 1 하고 리턴이 되고 있는 것을 확인할 수 있습니다.
  • 그리고 mysql에서 select * from guestbook; 해보면 방금 입력한 데이터가 잘 들어가 있는 것을 확인할 수 있습니다.( MySQL 5.7 Command Line Client - Unicode로 접속해야 한글이 안 깨집니다)

 

  • 그리고 LogDao도 테스트 해봅니다. LogDao를 얻어와서 Log DTO를 생성하고 값을 채워서 insert를 해봅니다.

 

  • 실제 테이블에 가서 조회하면 데이터가 잘 들어가 있는 것을 확인할 수 있습니다.

여기까지 레이어드 아키텍처 중에 가장 오른쪽에 있었던 Repository Layer 쪽을 작성해보았습니다.

실습코드

Guestbook.java

package kr.or.connect.guestbook.dto;

 

import java.util.Date;

 

public class Guestbook {

           private Long id;

           private String name;

           private String content;

           private Date regdate;

           public Long getId() {

                      return id;

           }

           public void setId(Long id) {

                      this.id = id;

           }

           public String getName() {

                      return name;

           }

           public void setName(String name) {

                      this.name = name;

           }

           public String getContent() {

                      return content;

           }

           public void setContent(String content) {

                      this.content = content;

           }

           public Date getRegdate() {

                      return regdate;

           }

           public void setRegdate(Date regdate) {

                      this.regdate = regdate;

           }

           @Override

           public String toString() {

                      return "Guestbook [id=" + id + ", name=" + name + ", content=" + content + ", regdate=" + regdate + "]";

           }

}

 

Log.java

package kr.or.connect.guestbook.dto;

 

import java.util.Date;

 

public class Log {

           private Long id;

           private String ip;

           private String method;

           private Date regdate;

           public Long getId() {

                      return id;

           }

           public void setId(Long id) {

                      this.id = id;

           }

           public String getIp() {

                      return ip;

           }

           public void setIp(String ip) {

                      this.ip = ip;

           }

           public String getMethod() {

                      return method;

           }

           public void setMethod(String method) {

                      this.method = method;

           }

           public Date getRegdate() {

                      return regdate;

           }

           public void setRegdate(Date regdate) {

                      this.regdate = regdate;

           }

           @Override

           public String toString() {

                      return "Log [id=" + id + ", ip=" + ip + ", method=" + method + ", regdate=" + regdate + "]";

           }

}

 

LogDao.java

package kr.or.connect.guestbook.dao;

 

import javax.sql.DataSource;

 

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.guestbook.dto.Log;

 

@Repository

public class LogDao {

           private NamedParameterJdbcTemplate jdbc;

    private SimpleJdbcInsert insertAction;

 

    public LogDao(DataSource dataSource) {

        this.jdbc = new NamedParameterJdbcTemplate(dataSource);

        this.insertAction = new SimpleJdbcInsert(dataSource)

                .withTableName("log")

                .usingGeneratedKeyColumns("id");

    }

 

           public Long insert(Log log) {

                      SqlParameterSource params = new BeanPropertySqlParameterSource(log);

                      return insertAction.executeAndReturnKey(params).longValue();

           }

}

 

GuestbookDaoSqls.java

package kr.or.connect.guestbook.dao;

 

public class GuestbookDaoSqls {

           public static final String SELECT_PAGING = "SELECT id, name, content, regdate FROM guestbook ORDER BY id DESC limit :start, :limit";

           public static final String DELETE_BY_ID = "DELETE FROM guestbook WHERE id = :id";

           public static final String SELECT_COUNT = "SELECT count(*) FROM guestbook";

}

 

GuestbookDao.java

package kr.or.connect.guestbook.dao;

 

import java.util.Collections;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

 

import javax.sql.DataSource;

 

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.guestbook.dto.Guestbook;

 

import static kr.or.connect.guestbook.dao.GuestbookDaoSqls.*;

 

@Repository

public class GuestbookDao {

            private NamedParameterJdbcTemplate jdbc;

               private SimpleJdbcInsert insertAction;

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

 

               public GuestbookDao(DataSource dataSource) {

                   this.jdbc = new NamedParameterJdbcTemplate(dataSource);

                   this.insertAction = new SimpleJdbcInsert(dataSource)

                           .withTableName("guestbook")

                           .usingGeneratedKeyColumns("id");

               }

              

               public List<Guestbook> selectAll(Integer start, Integer limit) {

                                Map<String, Integer> params = new HashMap<>();

                                params.put("start", start);

                                params.put("limit", limit);

                   return jdbc.query(SELECT_PAGING, params, rowMapper);

               }

 

 

                      public Long insert(Guestbook guestbook) {

                                  SqlParameterSource params = new BeanPropertySqlParameterSource(guestbook);

                                  return insertAction.executeAndReturnKey(params).longValue();

                      }

                     

                      public int deleteById(Long id) {

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

                                  return jdbc.update(DELETE_BY_ID, params);

                      }

                     

                      public int selectCount() {

                                  return jdbc.queryForObject(SELECT_COUNT, Collections.emptyMap(), Integer.class);

                      }

}

 

GuestbookDaoTest.java

package kr.or.connect.guestbook.dao;

 

import java.util.Date;

 

import org.springframework.context.ApplicationContext;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

 

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

import kr.or.connect.guestbook.dto.Log;

 

public class GuestbookDaoTest {

 

           public static void main(String[] args) {

                      ApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);

//                    GuestbookDao guestbookDao = ac.getBean(GuestbookDao.class);

//                   

//                    Guestbook guestbook = new Guestbook();

//                    guestbook.setName("강경미");

//                    guestbook.setContent("반갑습니다. 여러분.");

//                    guestbook.setRegdate(new Date());

//                    Long id = guestbookDao.insert(guestbook);

//                    System.out.println("id : " + id);

                     

                      LogDao logDao = ac.getBean(LogDao.class);

                      Log log = new Log();

                      log.setIp("127.0.0.1");

                      log.setMethod("insert");

                      log.setRegdate(new Date());

                      logDao.insert(log);

           }

 

}

 

실습(Service Layer 작성)

  • Service Layer 부분도 따로 관리하기 위해 패키지를 생성합니다. 서비스 인터페이스들만 가지고 있을 패키지인 service라는 패키지와 실제로 구현체를 가지고 있을 service.impl 패키지를 각각 생성합니다.
  • service 패키지에는 GuestbookService라는 인터페이스를 생성합니다.
  • 방명록 요구 사항(방명록 정보를 페이지 별로 읽어오기, 페이징 처리를 위한 전체 건 수 구하기, 방명록 저장하기, id에 해당하는 방명록 삭제, Log 테이블에 ip를 비롯한 관련 정보 저장)을 구현하기 위한 메서드를 정의합니다.

 

  • impl 패키지에 인터페이스를 구현하는 클래스인 GuestbookServiceImpl.java를 생성합니다이 클래스는 서비스이기 때문에 @Service 어노테이션을 설정합니다. 그리고 이 부분은 service layer에 해당합니다.
  • 서비스 내부에서 GuestbookDao를 사용하기 위해 @Autowired 어노테이션을 붙여 필드에 선언합니다. 그러면 알아서 여기에 bean을 생성해서 주입을 시켜줍니다. LogDao도 마찬가지로 선언해서 어노테이션을 붙여줍니다.
  • 그리고 GuestbookService를 구현하여 메서드들을 모두 오버라이딩합니다.

 

  • getGuestbooks 메서드는 guestbook 목록을 가져옵니다. 이때 guestbookDao의 selectAll 메서드를 사용합니다. start와 limit을 지정하여 조회하는데 limit의 값은 GuestbookService에서 Limit = 5 하고 지정을 해놓았습니다. 이렇게 상수를 지정하면 나중에 페이징하는 수를 조정하고 싶을 때 service에서 LIMIT를 변경하면 됩니다.
  • selectAll() 메서드가 Guestbook List를 리턴하면 별다른 처리 없이 list를 바로 리턴하도록 합니다.
  • 이때 이 메서드는 읽기만 하는 메서드잖아요? 그래서 읽기만 하는 메서드는 트랜잭션을 처리할 때 @Transactional이라는 어노테이션을 붙여주시면 내부적으로 readOnly라는 형태로 connection을 사용하게 됩니다.

 

  • deleteGuestbook 메서드는 guestbookDao의 deleteById 메서드를 이용합니다. 그러면 deleteById는 입력받아온 id에 해당하는 값을 가지고 삭제를 하게 되고 삭제가 제대로 일어나면 삭제된 갯수를 리턴합니다.
  • delete가 정상적으로 수행되면 Log를 남길 겁니다. Log에 어떤 정보를 남길지는 요구 사항에 나와있는 대로 처리하면 됩니다.
  • Log 테이블에 저장하기 위해 Log 객체를 생성하고 받아온 ip를 log.setIp() 넣어 실행합니다. 그리고 setMethod() 지금 현재 이 메서드는 delete가 수행됐으니까 "delete"를 넘겨주고 그다음에 setRegdate() 는 현재 날짜를 넘겨줍니다. log에 필요한 정보들을 다 채웠으니까 logDao가 가지고 있는 insert()라는 메서드를 호출하여 insert 시킵니다.
  • 리턴은 아까 받아낸 deleteCount를 리턴합니다. 이러deleteGuestbook 메서드가 수행됐을 겁니다.
  • 이 메서드는 readOnly는 아니에요. 그래서 readOnly로 수행되면 트랜잭션이 적용이 안 될 거라서 @Transactional이라는 어노테이션에 (readOnly=false)로 지정하면 됩니다.

 

  • addGuestbook은 Guestbook에다가 데이터 한 건을 넣습니다. 테이블에 넣을 guestbook 정보는 앞에 컨트롤러 단에서 넘겨줄 겁니다이렇게 받아온 guestbook에게 regdate 값이 없을 것이므로 Date 객체를 생성해서 값을 지정해 줍니다.
  • 그다음에 guestbookDao의 insert 메서드를 수행합니다. 그러면 id 값을 리턴받을텐데 Long 타입의 id가 리턴되어서 올 겁니다. idinsert 할 때 자동으로
  • 만들어진 값입니다.
  • 그리고 guestbook에 데이터가 입력되었으니 Log를 남깁니다. 이때 메서드는 insert이며 시간은 실제 실행되는 시간을 넣으면 됩니다. 그리고 logDao의 insert 메서드를 사용합니다.
  • 리턴값으로는 입력했던 guestbook을 리턴하게 하면 됩니다.
  • 그리고 readOnly 속성을 false로 바꿔줍니다. 그래야 DB에 제대로 입력됩니다.

 

  • 마지막에 있는 getCount() 같은 경우는 전체 건수를 출력하며 페이징 처리에서 사용됩니다. guestbookDaoselectCount() 라는 메서드로 전체 건수를 구할 수 있습니다.
  • 테스트를 하기 위해 impl 쪽에다가 GuestbookServiceTest.java 클래스를 만들어보겠습니다.
  • 객체 생성하기 위해서 ApplicationContext 만들고 GuestbookService 객체 얻어옵니다. 필요한 부분 작성 후에 insert부터 Java Application으로 테스트합니다.

 

  • add를 수행해보면 콘솔에 잘 출력되는 것을 볼 수 있습니다. 또한 mysql에서 확인해보면 guestbook에 데이터가 잘 들어가고 있는 것도 확인할 수가 있습니다.

 

  • 테이블의 데이터를 보면 id 번호가 하나 없습니다. 반드시 자동으로 생성되는 id1, 2, 3, 4 이렇게 일련 된 번호로 나오지 않아도 괜찮습니다. 왜 이렇냐면 실제 id가 생성이 됐었는데 아까 한 번 실패해서 그렇습니다. 실패한 id에 대해서는 다시 가져간다거나 그렇지 않습니다. 그냥 입력되다가 실패된 겁니다. 그러니까 별로 신경 쓰지 않아도 됩니다.
  • 그리고 guestbook만 볼 게 아니라 log 테이블도 한번 살펴보겠습니다.
  • log 테이블에도 보니까 insert가 들어와 있는 것을 볼 수 있습니다. 그런데 log는 addGuestbook에서 insert 해서 입력되도록 처리했습니다. 이전에 입력이 됐는데 log insert 하다가 ip때문에 Exception이 발생된 거잖아요. 그러니까 guestbook 테이블은 입력이 되었는데 Exception이 발생됐기 때문에 log 테이블은 입력이라는 자체가 일어나지 않았을 겁니다. 그런데 어쨌든 log 부분에서 Exception이 발생됐기 때문에 메서드에서 guestbook에서는 이미 insert를 성공했었지만 메서드 전체가 취소되고 있는 걸 볼 수 있습니다. 그리고 이게 트랜잭션입니다.
  • 다시 한 번 테스트를 수행해보겠습니다. 이번에는 이렇게 바꿔서 한번 수행을 해보겠습니다.

 

  • 실행을 시켜보면 에러 없이 잘 수행되고 guestbook 테이블에도 제대로 입력이 됐고 log 테이블에도 제대로 입력이 되고 있는 것을 볼 수 있습니다.


 

  • 아까 Exception이 발생됐을 때 이미 처리는 해봤지만 이번에는 일부러 Exception을 발생시켜서 확인해보겠습니다.

 

  • 실행을 시키면 RuntimeException이 발생하는 것을 볼 수 있습니다. 그렇다는 것은 guestbook 테이블에 넣는 것까지 실행되었다는 뜻입니다. 그리고 그 다음 라인에서 Exception이 발생을 했다는 건데 지금 addGuestbook()를 트랜잭션으로, 그러니까 메서드 전체가 나눌 수 없는 하나의 작업 단위입니다. 즉, guestbook 테이블에 넣는 부분이 다시 취소되며 메서드 시작부터 끝까지 다 성공해야지만 전체가 성공이 됩니다.
  • mysql에서 select를 해보면 guestbook도 아까 입력한 값이 입력되어 있지 않습니다.
  • 트랜잭션 처리 때문에 이렇게 되니까 @Transactional 되어 있는 부분을 주석으로 처리하시고 실행을 시켜보겠습니다. 처리하다가 exception 부분에서 오류가 발생했습니다. 그런데 데이터베이스에서 조회해보면 그럼에도 불구하고 guestbook에는 입력이 된 것을 볼 수 있습니다. log에서는 입력되지 않았습니다. 왜냐하면 Exception 전까지의 insert는 수행되었기 때문입니다.
  • 다시 정리하면, @Transactional 때문에 이 메서드 전체가 성공이 되어야만 insert 하는 2개의 부분이 모두 들어가며 전체가 성공하지 않으면 다 취소가 되는 것입니다.
    (예제가 끝나면 exception 코드 지우고 @Transactional 을 원래대로 돌려줍니다.)

실습코드

GuestbookService.java

package kr.or.connect.guestbook.service;

 

import java.util.List;

 

import kr.or.connect.guestbook.dto.Guestbook;

 

public interface GuestbookService {

           public static final Integer LIMIT = 5;

           public List<Guestbook> getGuestbooks(Integer start);

           public int deleteGuestbook(Long id, String ip);

           public Guestbook addGuestbook(Guestbook guestbook, String ip);

           public int getCount();

}

 

GuestbookServiceImpl.java

package kr.or.connect.guestbook.service.impl;

 

import java.util.Date;

import java.util.List;

 

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import org.springframework.transaction.annotation.Transactional;

 

import kr.or.connect.guestbook.dao.GuestbookDao;

import kr.or.connect.guestbook.dao.LogDao;

import kr.or.connect.guestbook.dto.Guestbook;

import kr.or.connect.guestbook.dto.Log;

import kr.or.connect.guestbook.service.GuestbookService;

 

@Service

public class GuestbookServiceImpl implements GuestbookService{

           @Autowired

           GuestbookDao guestbookDao;

          

           @Autowired

           LogDao logDao;

 

           @Override

           @Transactional

           public List<Guestbook> getGuestbooks(Integer start) {

                      List<Guestbook> list = guestbookDao.selectAll(start, GuestbookService.LIMIT);

                      return list;

           }

 

           @Override

           @Transactional(readOnly=false)

           public int deleteGuestbook(Long id, String ip) {

                      int deleteCount = guestbookDao.deleteById(id);

                      Log log = new Log();

                      log.setIp(ip);

                      log.setMethod("delete");

                      log.setRegdate(new Date());

                      logDao.insert(log);

                      return deleteCount;

           }

 

           @Override

           @Transactional(readOnly=false)

           public Guestbook addGuestbook(Guestbook guestbook, String ip) {

                      guestbook.setRegdate(new Date());

                      Long id = guestbookDao.insert(guestbook);

                      guestbook.setId(id);

                     

//                    if(1 == 1)

//                               throw new RuntimeException("test exception");

//                              

                      Log log = new Log();

                      log.setIp(ip);

                      log.setMethod("insert");

                      log.setRegdate(new Date());

                      logDao.insert(log);

                     

                     

                      return guestbook;

           }

 

           @Override

           public int getCount() {

                      return guestbookDao.selectCount();

           }

          

          

}

 

GuestbookServiceTest.java

package kr.or.connect.guestbook.service.impl;

 

import java.util.Date;

 

import org.springframework.context.ApplicationContext;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

 

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

import kr.or.connect.guestbook.dto.Guestbook;

import kr.or.connect.guestbook.service.GuestbookService;

 

public class GuestbookServiceTest {

 

           public static void main(String[] args) {

                      ApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);

                      GuestbookService guestbookService = ac.getBean(GuestbookService.class);

                     

                      Guestbook guestbook = new Guestbook();

                      guestbook.setName("kang kyungmi22");

                      guestbook.setContent("반갑습니다. 여러분. 여러분이 재미있게 공부하고 계셨음 정말 좋겠네요^^22");

                      guestbook.setRegdate(new Date());

                      Guestbook result = guestbookService.addGuestbook(guestbook, "127.0.0.1");

                      System.out.println(result);

                     

           }

 

}

 

실습(Controller, JSP 작성)

  • controller 패키지를 생성합니다. 그리고 GuestbookController.java 클래스를 생성합니다.
  • GuestbookController도 마찬가지로 ComponentScan 할 때 해당 Bean을 찾을 수 있도록 @Controller 어노테이션을 붙여줍니다.
  • 이 컨트롤러에서는 서비스를 사용하므로 서비스를 선언하고 @Autowired 어노테이션 붙여줍니다. 실제 사용하게 될 서비스를 써주면 됩니다.

 

  • 그런 다음 메서드를 작성합니다. path list로 들어왔을 때 @RequestParam으로 "start"라는 값을 꺼내서 사용할 수 있습니다. 이때 만약 값이 없다면 defaultValue 0으로 start에 받아줄 겁니다.

 

  • 서비스로부터 start를 넘겨서 해당 목록을 얻어오면 되고 그다음에 서비스가 갖고 있는 getCount()를 이용해서 전체 페이지 수를 얻어옵니다. 그리고 이 전체 페이지 수를 LIMIT으로 나눠보면 pageCount를 구할 수 있습니다. 해당 pageCount를 출력합니다.

 

  • pageCount를 계산하면 이제 페이지의 수만큼 각각의 start 값을 list로 저장합니다. 그래야 숫자로 된 페이지 링크를 클릭할 때 start 값을 넘겨 줄 수 있습니다. 만약 페이지가 3이고 각각의 start 값이 0, 5, 10 이라면 list?start=0, list?strat=5 와 같이 링크처리를 해야 하고 이를 쉽게 하기 위해 리스트에 값을 저장해주는 겁니다. 여기서는 pageStartList에다가 값을 넣어주고 있습니다.

 

  • 그리고 guestbook, 전체 카운트, 페이지 시작 리스트를 JSP에서 사용해야 되기 때문에 model에다가 넣어 줍니다. 그리고 뷰 네임을 list로 지정합니다.

 

  • 두 번째 메서드는 방명록을 쓰는 메서드입니다.
  • @ModelAttributeSpring MVC가 guestbook DTO 객체를 하나 생성하고, jsp input에서 넘긴 값을 guestbook DTO와 일치하는 name 들을 찾아 이 객체 안에다가 값들을 다 넣어주는 일을 수행합니다.
    그리고 
    클라이언트의 헤더 정보를 얻기 위해 request를 파라미터로 받아옵니다.
    그다음엔 addGuestbook 메서드를 통해 내용을 insert합니다.
    넣고 나면 list.jsp redirect합니다.

 

  • 다 했으면 views에다가 list.jsp를 하나 생성해줍니다JSTL 써야 되니까 taglib을 추가합니다.
  • 실제 화면에 보여야 되는 부분에는 방명록 하고 보일 거고 방명록의 전체 수를 이렇게 화면에다가 출력하도록 합니다.

 

  • 컨트롤러에서 얻어온 방명록 리스트에 있는 Guestbook 객체를 하나 꺼낸 뒤 EL을 이용해서 id, name, 내용, regdate를 출력합니다. 그리고 이 부분을 forEach 문을 이용해서 리스트의 모든 값을 출력합니다.

 

  • 그다음에 pageStartList, 이 부분은 페이징 돼서 페이지 링크 나오는 부분입니다. 페이지의 링크 뿌려주기 위한 부분을 구현하고 있고 맨 하단에는 form 태그를 이용하여 방명록을 입력할 수 있는 부분이 있습니다.

 

  • 이제 guestbook을 한번 실행을 시켜보겠습니다.

 

  • guestbook 실행을 시켜보면 "/"로 요청했지만 index.jsp가 동작하면서 list.jsp로 redirect 하고 있는 것을 확인할 수 있습니다.
  • JSP에서 구현한 대로 방명록의 전체 수 구해서 보여지며 forEach 문이 동작하면서 리스트에서 얻어온 각각의 값들을 보여주고 있습니다. 그리고 forEach 문이 돌면서 페이지 하나가 보여질 거고요.
  • 방명록에다가 등록 해보겠습니다. 5개까지는 한 화면에 보여지는데 하나 더 등록해서 6개가 되는 순간에 페이지 링크가 하나 더 생기는 걸 볼 수 있습니다. 2번 링크 클릭하면 다음 방명록부터 나오고 있는 것을 확인할 수가 있습니다.
  • 여기까지 레이어드 아키텍처를 이용해서 방명록을 만드는 예제를 수행해보았습니다.

실습코드

GuestbookController.java

package kr.or.connect.guestbook.controller;

 

import java.util.ArrayList;

import java.util.List;

 

import javax.servlet.http.HttpServletRequest;

 

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;

import org.springframework.ui.ModelMap;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.ModelAttribute;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestParam;

 

import kr.or.connect.guestbook.dto.Guestbook;

import kr.or.connect.guestbook.service.GuestbookService;

 

@Controller

public class GuestbookController {

           @Autowired

           GuestbookService guestbookService;

          

           @GetMapping(path="/list")

           public String list(@RequestParam(name="start", required=false, defaultValue="0") int start,

                                                           ModelMap model) {

                     

                      // start로 시작하는 방명록 목록 구하기

                      List<Guestbook> list = guestbookService.getGuestbooks(start);

                     

                      // 전체 페이지수 구하기

                      int count = guestbookService.getCount();

                      int pageCount = count / GuestbookService.LIMIT;

                      if(count % GuestbookService.LIMIT > 0)

                                  pageCount++;

                     

                      // 페이지 수만큼 start의 값을 리스트로 저장

                      // 예를 들면 페이지수가 3이면

                      // 0, 5, 10 이렇게 저장된다.

                      // list?start=0 , list?start=5, list?start=10 으로 링크가 걸린다.

                      List<Integer> pageStartList = new ArrayList<>();

                      for(int i = 0; i < pageCount; i++) {

                                  pageStartList.add(i * GuestbookService.LIMIT);

                      }

                     

                      model.addAttribute("list", list);

                      model.addAttribute("count", count);

                      model.addAttribute("pageStartList", pageStartList);

                     

                      return "list";

           }

          

           @PostMapping(path="/write")

           public String write(@ModelAttribute Guestbook guestbook,

                                                                   HttpServletRequest request) {

                      String clientIp = request.getRemoteAddr();

                      System.out.println("clientIp : " + clientIp);

                      guestbookService.addGuestbook(guestbook, clientIp);

                      return "redirect:list";

           }

}

 

list.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"

           pageEncoding="UTF-8"%>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<title>방명록 목록</title>

</head>

<body>

 

           <h1>방명록</h1>

           <br> 방명록 전체 수 : ${count }

           <br>

           <br>

 

           <c:forEach items="${list}" var="guestbook">

 

${guestbook.id }<br>

${guestbook.name }<br>

${guestbook.content }<br>

${guestbook.regdate }<br>

 

           </c:forEach>

           <br>

 

           <c:forEach items="${pageStartList}" var="pageIndex" varStatus="status">

                      <a href="list?start=${pageIndex}">${status.index +1 }</a>&nbsp; &nbsp;

</c:forEach>

 

           <br>

           <br>

           <form method="post" action="write">

                      name : <input type="text" name="name"><br>

                      <textarea name="content" cols="60" rows="6"></textarea>

                      <br> <input type="submit" value="등록">

           </form>

</body>

</html>

 

생각해보기

레이어 별로 개발을 진행할 때, 각 레이어별로 잘 동작하는지 확인하는 것은 매우 중요합니다. 어떤 특정 레이어가 올바르게 동작하지 않는다면 웹 어플리케이션은 제대로 동작하지 않을 것입니다. 어느 특정 레이어가 문제가 있는지 알려면, 각 레이어별로 테스트가 필요합니다. 자바에서 테스트 코드를 좀 더 효과적으로 작성할 수 있는 방법에 대해 알아보세요.

참고 자료

[참고링크] Web on Servlet Stack
https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html

[참고링크] Serving Web Content with Spring MVC
https://spring.io/guides/gs/serving-web-content/

[참고링크] Spring MVC Tutorial              https://www.javatpoint.com/spring-3-mvc-tutorial

...더보기

cf) controller에서 service를 필드로 사용하는데 의존성 주입이 되었다. service는 인터페이스인데 되는게 이상했다. 알아보니까 service 인터페이스를 구현한 클래스가 하나라면 그 클래스를 찾아서 의존성 주입이 되는데 만약 2개 이상이면 정확히 찾지 못하고 에러가 난다고 한다. 실제로 테스트해보니 아래와 같은 에러가 났다.

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'productsApiController': Unsatisfied dependency expressed through field 'productsService'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'kr.or.connect.booking.service.ProductsService' available: expected single matching bean but found 2: productsServiceImpl,testProductsService

관련 내용은 다음 URL에 있다. https://okky.kr/article/413840

 

Comments