Web/Java+Spring

Layered Architecture 실습 - 방명록

WakaraNai 2021. 6. 9. 22:36
728x90
반응형
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<% 
	response.sendRedirect("list");
%>

이번 시간엔 방명록을 Spring 프레임워크를 이용해 만들어 보도록 하겠습니다.

이를 통해 각 레이어별로 어떤 내용들을 작성해야 하는지 알아보고, 완전히 동작하는 웹 어플리케이션을 개발해 봄으로써 Spring 웹 어플리케이션에 대한 이해를 높이는 시간이 될 수 있길 바랍니다.

 

방명록 만들기 실습

  • Spring JDBC를 이용하여 DAO 작성
  • Controller, Service Dao로 레이어드 아키텍처 구성
  • 트랜잭션 처리
  • Spring MVC에서 form 값 입력받기
  • Spring MVC에서 redirect하기
  • Controller에서 jsp에게 전달한 값을 JSTL과 EL을 이용해 출력

 

방명록 요구 사항

  • 데이터베이스 관련
    •  guestbook 테이블
      • 방명록 정보
      • id는 자동으로 입력되도록 한다
      • id, 이름, 내용, 등록일을 저장
    • log 테이블
      • 방명록에 글을 쓰거나, 방명록의 글을 삭제할 때
      • 클라이언트의 ip주소, 등록(삭제) 시간, 등록/삭제(method 칼럼) 정보를 저장
      • id는 자동으로 입력되도록 한다
  • 방명록 입력 관련
    • http://localhost:8080/guestbook/을 요청하면 자동으로 /guestbook/list로 리다이렉팅
    • 방명록이 없으면 건수는 0이 나오고 아래에 방명록을 입력하는 폼이 보여짐
    • 이름과 내용 입력하고, 등록 버튼 누르면 /guestbook/write URL로 입력한 값을 전달하여 저장
    • 값이 저장된 후에는 /guestbook/list로 리다이렉팅
  • 입력 후 출력
    • 입력한 한 건의 정보가 보임
    • 방명록 내용과 form 사이 숫자는 방명록 페이지 링크
      • 방명록 5건 당 1 페이지로 설정
      • 방명록이 6건 입력되면 아래 페이지 수가 2건 보여짐
        • 1 페이지를 누르면 /guestbook/list?start=0을 요청
        • 2 페이지를 누르면 /guestbook/list?start=5을 요청
        • /guestbook/list는 /guestbook/list?start=0와 결과가 같다

 

방명록 클래스 다이어그램

  • web.xml
    • 두 개의 자바 config 파일에 대해서 설정
      • WebMvcContextConfiguration : DispatcherServlet을 읽어들이기 위해 - URL mapping 관련 정보 정리
      • ApplicationConfig : ApplicationContextListner을 읽어들이기 위해 
        • DBConfig를 import

 

  • GuestbookController : URL 요청을 처리하는 핸들러
    • 이 컨트롤러는 비즈니스 로직을 가진 서비스 객체를 사용함
      • 해당 서비스 객체는 GuestbookService 인터페이스와 GuestbookServiceImpl 구현 클래스로 구성
        • GuestbookServiceImple에서는, 
          • LogDao와 GuestbookDao를 이용하여 비즈니스 로직을 수행
          • Dao를 하려면 Dto 필요 
            • dto:Guestbook,  dto:Log
          • LogDao 
            • 비즈니스 로직은 하나의 트랜잭션 단위로 동작하기에
            • LogDao는 저장만 하고 나머지 작업은 수행하지 않음 
            • 그렇기에 별도로 sql은 필요 없음
          • GuestbookDao
            • 여러 개의 sql 작업을 수행하기에 별도의 sql 파일이 필요
            • 그런 sql들은 GuestbookDaoSqls 파일에서 관리
  • list.jsp : 뷰의 역할을 수행
  • index.jsp : redirect한 코드만 나오게 됨

 

 

 

실습

1. 프로젝트 생성 및 라이브러리 준비

Maven Project - webapp - 이름은 guestbook

 

web.xml 

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee;http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
	version="3.1">

</web-app>

 

pom.xml

jackson : json을 이용하기 위해

 <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    
     
    <!-- jackson -->
     <jackson2.version>2.8.6</jackson2.version>
        <!-- Spring Version -->
    	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<spring.version>4.3.5.RELEASE</spring.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    
    <!-- jstl -->
	<dependency>
	    <groupId>jstl</groupId>
	    <artifactId>jstl</artifactId>
	    <version>1.2</version>
	</dependency>
	  
	  <!-- jsp -->
	<dependency>
    	<groupId>javax.servlet</groupId>
    	<artifactId>jsp-api</artifactId>
    	<version>2.0</version>
    	<scope>provided</scope>
	</dependency>
	  
    <!-- Spring -->
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-web -->
		<dependency>
		    <groupId>org.springframework</groupId>
		    <artifactId>spring-web</artifactId>
		    <version>${spring.version}</version>
		</dependency>
	  	<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
    
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${spring.version}</version>
		</dependency>
		
		
		<!--  Spring JDBC -->
		<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>
		
		<!--  mysql -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.46</version>
		</dependency>
		
		<!-- basic data source -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-dbcp2</artifactId>
			<version>2.1.1</version>
		</dependency>
		
		
		<!-- jackson -->
		<dependency>
		    <groupId>com.fasterxml.jackson.core</groupId>
		    <artifactId>jackson-core</artifactId>
		    <version>${jackson2.version}</version>
		</dependency>
		
		<dependency>
		    <groupId>com.fasterxml.jackson.datatype</groupId>
		    <artifactId>jackson-datatype-jdk8</artifactId>
		    <version>${jackson2.version}</version>
		</dependency>
				
	</dependencies>

 

 

프로젝트 우클릭 -> Project facets -> Dynamic Web Programming을 3.1로, Runtime을 톰캣으로

Update Project

 

 

2. Config 파일

@Configuration 꼭 붙이기

 

패키지 생성

경로 : guestbook/src/main/java   - java 폴더 미리 만들어두기

이름 : kr.or.connect.guestbook.config

 

1. WebMvcContextConfiguration 

org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter 상속받기

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를 사용하게 함
	// 맵핑 정보가 없는 URL 요청에 대해 
	// WAS의 default servlet이 static한 자원을 읽어서 보여주도록 함
	@Override
	public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
		configurer.enable();
	}
	
	// 특정 URL에 대한 처리에 대해 컨트롤러 클래스를 작성하지 않고 맵핑
	// 보여줄 뷰 이름만 넘김
	@Override
	public void addViewControllers(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;
    }
	
	// 이런 설정을 DispatcherServlet이 읽어낼 수 있도록 작성
}

 

2. DBConfig 

Spring JDBC가 읽어들일 수 있도록 하기 위한 설정 파일

 

org.springframework.transaction.annotation.TransactionManagementConfigurationSelector 상속받기

 

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() {
		// 트랜잭셕을 처리할 PlatformTransactionManager 객체 반환
		return transactionManger();
	}

	@Bean
	public PlatformTransactionManager transactionManger() {
		return new DataSourceTransactionManager(dataSource());
	}
}

 

2. ApplicationConfig

dao나 service에 구현되어 있는 컴포넌트들을 읽어옮

 

web.xml 수정

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee;http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
	version="3.1">

	<display-name>Spring JavaConfig Sample</display-name>
	<context-param>
		<param-name>contextClass</param-name>
		<!-- 설정 파일을 읽어들일 때 사용할 contextClass는  AnnotationConfigWebApplicationContext을 이용하겠다 -->
		<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>
	<!-- 비즈니스 로직 쪽인 DBConfig와 ApplicationConfig를 읽어들일 수 있도록 정의 -->
	<listener><!-- ContextLoaderListener는 run on server해서 context가 로딩될 때 읽기를 수행-->
		<listener-class>org.springframework.web.context.ContextLoaderListener
		</listener-class>
	</listener>

	<servlet>
		<servlet-name>mvc</servlet-name> <!-- DispatcherServlet을 Front Servlet으로 등록 -->
		<servlet-class>org.springframework.web.servlet.DispatcherServlet
		</servlet-class>
		<init-param>
			<param-name>contextClass</param-name> <!-- 이것은 이 종류의 ApplicationContext를 이용 -->
			<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext
			</param-value>
		</init-param>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			 <!-- DispatcherServlet이 실행될 때 -->
			 <!-- context-param을 참고하여 이 이름으로 등록한 파일을 가져다가 사용함 -->
			<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>
		<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>  <!-- DispatcherServlet이 모든 요청을 다 받음 -->
	</filter-mapping>
</web-app>

 

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 {

}

 

 

뷰(jsp) 생성

폴더 생성

경로 /webapp/WEB-INF/

이름 views

 

1. index.jsp

index라는 요청이 오면

"list"라는 요청으로 redirect해주기

그러므로, index는 redirect하는 로직까지만 가지고 있으면 됨

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<% 
	response.sendRedirect("list");
%>

 

 

 

중간 테스트

프로젝트 우클릭 -> Run on Server

URL를 자세히 보면 guestbook/list로 되어있음

guestbook/까지만 요청해도 저렇게 수정됨

컨트롤러 없이 "/" 라는 요청이 들어오면

"index"라는 view name으로 실행하라고 설정해둠

그래서 index.jsp가 실행된 상태

이 코드 안에서 "list"로 리다이렉트를 했기 때문에 url이 list로 변했음

 

 

 

 


3. Repository Laryer - DTO, DAO 작성

1. DB에 table 생성하기

mysql> CREATE TABLE guestbook (
    -> id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
    -> name VARCHAR(255) NOT NULL,
    -> content TEXT,
    -> regdate DATETIME,
    -> PRIMARY KEY (id)
    -> );

 

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

 

 

2. DTO

패키지 생성

경로 : src/java/main

이름 : kr.or.connect.guestbook.dto

 

Guestbook.java

    !! DATETIME은 import java.util.Date;

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

 

 

 

3. DAO - @Repository

패키지 생성

경로 : src/java/main

이름 : kr.or.connect.guestbook.dao

 

LogDao.java

  • usingGeneratedKeyColumns : id가 자동으로 입력됨
  • insertAction : insert문을 내부적으로 생성해서 실행,
  • executeAndReturnKey : insertAction에서 자동 생성된 id 값을 리턴
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();
	}
}

 

 

GuestbookDao.java

 

입력문 외에도 여러 일들을 수행하기에 query들을 관리할 GuestbookDaoSqls가 필요

LIMIT은 시작 값, 끝나는 값을 설정하여 특정 부분만 SELECT해올 때 사용

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 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.*;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;
@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");
	    }
	    
	    // "SELECT id, name, content, regdate FROM guestbook ORDER BY id DESC limit :start, :limit";
	    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();
		}
		
		
		// "DELETE FROM guestbook WHERE id = :id";
		public int deleteById(Long id) {
			Map<String, ?> params = Collections.singletonMap("id", id);  // id 값으로 접근하여 삭제
			// 해당 id에 비어있는 객체를 보내버림
			return jdbc.update(DELETE_BY_ID, params);
		}
		
		// "SELECT count(*) FROM guestbook";
		public int selectCount() {
			return jdbc.queryForObject(SELECT_COUNT, Collections.emptyMap(), Integer.class);
		}
}

 

 

중간 테스트

어떤 메소드를 하나 구현하면 꼭 제대로 동작하는 테스트해보기

 

 

실행용 패키지 생성

경로 : src/java/main

이름 : kr.or.connect.guestbook.main

 

GuestbookDaoTest.java

main 메소드에서 실행하므로 Run on Server가 아니라

Java Application으로 실행

package kr.or.connect.guestbook.main;

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.dao.GuestbookDao;
import kr.or.connect.guestbook.dto.Guestbook;

public class GuestbookDaoTest {
	public static void main(String[] args) {
		
		ApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class); 
		
		// GuestbookDao 얻어오기
		GuestbookDao guestbookDao = ac.getBean(GuestbookDao.class);
		
		
		// insert 테스트
		Guestbook guestbook = new Guestbook();
		guestbook.setName("홍길동");
		guestbook.setContent("반갑습니다. 여러분.");
		guestbook.setRegdate(new Date());
		Long id = guestbookDao.insert(guestbook);
		System.out.println("id : " + id);
	}
}

 

column을 잘못적어 null값이 들어갔다. 수정 후 해보니 정상 작동

 

 

LogDaoTest.java

package kr.or.connect.guestbook.main;

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.dao.LogDao;
import kr.or.connect.guestbook.dto.Log;

public class LogDaoTest {
	public static void main(String[] args) {
		ApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class); 
		
		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);		
	}
}

 

 


4. Service Layer

Serivce 인터페이스만 가지는 패키지 생성

경로 : src/java/main

이름 : kr.or.connect.guestbook.service

 

구현체를 가지는 패키지 생성

경로 : src/java/main

이름 : kr.or.connect.guestbook.service.impl

 

1. GuestbookService.java - Interface

service 패키지에 생성

 

필요한 메소드 선언해두기

  • 방명록 정보를 페이지 별로 읽어오기
  • 페이징 처리를 위해 전체 건 수 구하기
  • 방명록 저장하기
  • id에 해당하는 방명록 삭제하기 - ip는 LogDao를 이용해서 Log 테이블에 저장
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();

}

 

 

 

2. GuestbookServiceImpl.java  - @Service

service.impl 패키지에 생성

위의 인터페이스를 구현하는 클래스

이 부분이 바로 Service Layer

 

@Autowired -> Bean으로 자동 등록해주는 어노테이션

이를 이용해 GuestbookDao를 guestbookDao로 이용하겠다고 선언만 해두면

Spring이 알아서 그 클래스의 객체를 저 이름으로 생성해서 주입해줌

 

@Transactional -> 읽기만 하는 메소드에 트랜잭션 처리하기

내부적으로 readOnly 형태로 connection을 사용

정보가 수정되는 경우 (readOnly=false)를 적어주어야 함

메소드 실행 중 에러가 발생하면 insert가 취소됨. 그것이 바로 트랜잭션.

나눌 수 없는 하나의 작업 단위. 끝까지 다 성공해야만 성공이 됨.

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 GuestbookServiceImple implements GuestbookService {
	@Autowired
	GuestbookDao guestbookDao;
	
	@Autowired
	LogDao logDao;

	// 시작 페이지부터 방명록 몇 개씩 가져오는 메소드
	@Override
	@Transactional  // 읽기만 하는 메소드이기에, 내부적으로 readOnly로 connection을 사용하도록 함 
	public List<Guestbook> getGuestbooks(Integer start) {
		List<Guestbook> list = guestbookDao.selectAll(start, GuestbookService.LIMIT);
		return list;
	}

		// log를 남기기 위해선 readOnly로 할 수 없음
	@Override
	@Transactional(readOnly=false)
	public int deleteGuestbook(Long id, String ip) {
		int deleteCount = guestbookDao.deleteById(id);
		// 삭제 시 log 남기기
		Log log = new Log();
		log.setId(id);
		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);
		
		// log 남기기
		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

main 패키지에 생성 후 java application으로 실행

package kr.or.connect.guestbook.main;

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("gogo123");
		guestbook.setContent("Service 테스트 방명록 작성했습니다.");
		guestbook.setRegdate(new Date());
		Guestbook result = guestbookService.addGuestbook(guestbook, "127.0.0.1");
		System.out.println(result);
	}
	
}

에러 없이 잘 실행된 경우

 

에러 발생으로 입력 실패 후 다시 성공한 경우. id가 한 칸 띄워져 늘어나도 괜찮다.

 

 


5. Presentation Layer

Controller & JSP

 

Controller - @Controller

jsp 파일에서 넘어온 요청과 값을 처리하여 넘겨주는 등의 일을 처리

 

kr.or.connect.guestbook.controller 패키지를 생성하여 GuestbookController 클래스 파일 생성

 

@Autowired -> 이 컨트롤러에서 실제로 사용할 서비스를 이용할 수 있도록 선언해두기

 

@GetMapping -> 원하는 URL 쿼리문이 원하는 메소드 요청으로 왔을 때만 실행할 수 있도록 선언해두기

 

@RequestParam -> 지정한 이름으로 된 값을 꺼내서 사용할 수 있도록 함. 값이 없을 경우 defaultValue로.

 

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로 시작하는 방명록 목록 구하기
		// service로부터 start를 넣어서 해당 목록 얻어오기
		List<Guestbook> list = guestbookService.getGuestbooks(start);
		
		// 전체 페이지수 구하기
		int count = guestbookService.getCount();
		int pageCount = count / GuestbookService.LIMIT;
		if(count % GuestbookService.LIMIT > 0) 
			pageCount++; // 한 페이지 당 보여줄 수를 이용해 pageCount 구하기
		
		// 페이지 수만큼 start의 값을 리스트로 저장
		// 예를 들면 클릭한 페이지 수가 3이면
		// start 값을 저장한 리스트에 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);
		}
		
		// jsp에서 사용하도록 model에 넣어주기
		model.addAttribute("list", list);
		model.addAttribute("count", count);
		model.addAttribute("pageStartList", pageStartList);
		
		return "list";  // list.jsp 뷰로 열기
	}
	
	@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";
	}
}

 

 

JSP

views 폴더에 list.jsp 생성

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

<!-- JSTL을 쓰기 위한 선언 -->
<%@ 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>

	<!-- 리스트에 있는 Guestbook 객체를 하나 꺼내서 guestbook에 담음 -->
	<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>

 

 

프로젝트 실행

프로젝트 우클릭 -> Run on Server

728x90
반응형

'Web > Java+Spring' 카테고리의 다른 글

Spring MVC에서 Session 사용하기  (0) 2021.07.08
RestController - Rest API by Spring MVC  (0) 2021.06.10
Layered Architecture  (0) 2021.06.07
Spring MVC - Controller 작성 실습 3  (0) 2021.06.07
Spring MVC - Controller 작성 실습 2  (0) 2021.06.07