들어가기 전에
이번 시간엔 방명록을 Spring 프레임워크를 이용해 만들어 보도록 하겠습니다.
이를 통해 각 레이어별로 어떤 내용들을 작성해야 하는지 알아보고, 완전히 동작하는 웹 어플리케이션을 개발해 봄으로써 Spring 웹 어플리케이션에 대한 이해를 높이는 시간이 될 수 있길 바랍니다.
학습 목표
- Spring 프레임워크를 이용한 웹 어플리케이션 프로젝트를 구성할 수 있습니다.
- Spring 프레임워크를 이용한 웹 어플리케이션 개발 시 어떤 요소들을 개발해야하는지 이해합니다.
핵심 개념
- @Controller
- @Service
- @Repository
- @Transactional
학습하기
실습코드
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);
}
@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");
%>
comment
java.lang.IllegalStateException: 자식 컨테이너를 시작하는 중 오류 발생
Caused by: org.apache.catalina.LifecycleException: 구성요소 [StandardEngine[Catalina].StandardHost[localhost].StandardContext[/guestbook]]을(를) 시작하지 못했습니다.
Caused by: java.lang.IllegalArgumentException: 이름이 [spring_web]인, 둘 이상의 fragment들이 발견되었습니다.
⇒ /list로 리다이렉트 안되고 404 에러 뜨면서 이런 오류 뜨시면 web.xml에 <display-name> 태그 아래에 <absolute-ordering/> 추가하면 되네요
500에러 <jackson2.version>2.9.4</jackson2.version>로 변경하니깐 잘 되네욥
/list 로 안넘어가고 서블릿'mvc'에서 예외가 발생하며 500에러를 뱉어내는 경우에 스프링 버전을 확인해보시길 바랍니다. 최신버전의 스프링을 사용하면 호환이 잘 되지않는것같습니다...
빠른 복붙강의다.. 정신없다..
무엇보다 트랜잭션에 대한 설명과 이해과정이 인상적이었어요!
혹시 그대로 따라했는데
Error configuring application listener of class org.springframework.web.context.ContextLoaderListener
이런 에러가 뜬다면 아래 링크를 따라서 해보세요.
이거때문에 하루이상 프로젝트가 지연됬네요 ㅠㅠ
https://myblog.opendocs.co.kr/archives/1657
pom.xml을 많은 분들이 댓글을 통해서 올려주셨는데 이 댓글 양식 자체가 정렬이 안됩니다. 그래서 정렬에 어려움을 겪으시는 분들이 있으실텐데 xml 형식을 자동정렬하는 사이트를 올려드리겠습니다.
XML 자동 정렬 사이트 : http://chris.photobooks.com/xml/default.htm
XML INPUT 칸에다가 복사한 코드를 붙이신다음에(옵션을 비롯해서 다른건 안 건드리셔도 됩니다.) 옵션 밑에 Render버튼 누르시면 바로 자동정렬된 아웃풋이 나옵니다. 여러분의 시간은 소중하잖아요? ㅋㅋㅋ 포기하지 마시고 수강하시길
Exception in thread "main" org.springframework.dao.DataAccessResourceFailureException: Error retrieving database metadata;
이런 에러가 뜨고 디버깅 해보니까 Long id = guestbookDao.insert(guestbook);
여기서 오류가 발생하는거 같은데 그 이상은 잘 모르겠네요 문제가 뭘까요? 구글링해도 명확한 답이 안나오네요 ㅜㅜ
에러로그1. guestbook/list로 들어가지지 않음
pom.xml에 다음을 추가 (아래 댓글에서도 확인가능)
<!-- FOR JAVA 1.8 -->
<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>
에러로그2. database not found
: public GuestbookDao(DataSource dataSource) {
this.jdbc = new NamedParameterJdbcTemplate(dataSource);
this.insertAction = new SimpleJdbcInsert(dataSource)
.withTableName("guestbook")
.usingGeneratedKeyColumns("id");
}에서 테이블 이름을 "guestbook"이라고 안 주고 "guest"라고 줌.(긁어쓸걸..)
에러로그 3.
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type ~~
ServiceImpl의 패키지명이 ~~Service.impl로 안 돼 있고 ServiceImpl로 돼 있어서 오류가 났음.
정말 어이x;
jackson 라이브러리를 쓴적이 있었나요? 앞의 강의들 다봤었는데 쓴 기억이 안나네요
감사드립니당
Bean을 등록을 못해 에러가 발생하는데 위 소스코드 내에 componentscan이 맞는건가요?
위의 소스에서 DTO 클래스경우 @component같은 어노테이션을 지정해주지 않았는데 잘 실행이 되는 이유가 무엇인가요? 스프링 컨테이너가 알아서 등록해준건가요?
마지막 강의에서 write 처리는 강의에 나와있나요?? 갑자기 404 떠서 당황했는데 write 처리가 나온 적이 없었는데 밑에 코드에는 또 있네요...
No mapping found for HTTP request with URI [/guestbook/] in DispatcherServlet with name 'mvc'
이렇게 떠서.. 계속 헤매고 있네요. guestbook/list 로 뜨게 하는 부분 하고 있는데. 안되네요;; 같은 오류 겪으신 분 있으신가요.
GuestbookDaoTest하는 부분 잘 따라 한 것 같은데 uncategorized SQLException 에러라면서 Incorrect string value 나오는 분을 위해 글 남깁니다. mySql 초기 charset 설정이 utf8로 되어 있지 않을때 여기서는 utf8 로 보내는데 받을 때는 다른 charset으로 받아서 발생하는 에러고요 해결 방법은 간단합니다.
일단
drop table guestbook;
drop table log;
사용해서 테이블을 지우고요
set names utf8;
하셔서 데이터베이스 기본 charset 설정을 utf8;로 변경하세요.
이후 다시 테이블을 만들면, 테이블 생성시 charset을 명시해주는 경우를 제외하고 테이블의 charset은 데이터베이스의 charset을 따라 생성되기에 utf8로 테이블이 만들어져 문제가 발생하지 않을 겁니다
열공하세요~
참고 : https://bstar36.tistory.com/307
계속 404에러 떴다가 500에러 떴다가 한참 고생했네요. 아래 frontdev님 댓글이 도움이 되었습니다. spring 5.x.x라고 적으셨던 분들 frontdev님 댓글 참고하셔서 붙여넣으시고, WebMvcContextConfiguration.java 파일 잘 확인하신 뒤 해보세요.
<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.sourceEncoding>UTF-8</project.build.sourceEncoding>
<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>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Servlet JSP JSTL -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- spring jdbc & jdbc driver & connection pool -->
<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>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
<!-- basic data source -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.1.1</version>
</dependency>
<!-- Jackson Module -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson2.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>${jackson2.version}</version>
</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>
GuestbookServiceTest를 하는데 addGuestbook에서 중간에 강의와 똑같이 setIp를 빼놓고 실행했는데 guestbook rollback이 진행되지 않습니다. readonly=false를 했는데두요
@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 = new Log();
log.setId(id); // 여기서 에러
log.setMethod("insert");
log.setRegdate(new Date());
logDao.insert(log);
return guestbook;
}
rollback을 자동으로 해주어서 좋은 기능이네! 하고 써보니 안돼서 여쭤봅니다
덧붙여서 일부러 RuntimeException을 내도 안되구요, Transctional에다가 rollbackfor=runtimeexception을 해도 rollback이 안되네요ㅠㅠ
GuestbookServiceImpl 에서 getCount의 경우에는 Transactionl 을 해주지 않아도 괜찮은가요?