개발자입니다
[비트캠프] 82일차(17주차4일) - Spring Framework(Spring WebMVC 아키텍처, Tiles), myapp-60-3, 61, 프로젝트 절차(전체 과정) 본문
[비트캠프] 82일차(17주차4일) - Spring Framework(Spring WebMVC 아키텍처, Tiles), myapp-60-3, 61, 프로젝트 절차(전체 과정)
끈기JK 2023. 3. 3. 11:50
60-2 Spring WebMVC 아키텍처
"/app/*" 로 《Front Controller》DispatcherServlet 에게 요청한다. 여기서 Page Controller 를 call 하면 viewName (JSP URL, view 컴포넌트 이름) 예) "board/list" 을 리턴한다. 주고 받는 중간에 인터셉터 (스프링 필터)가 들어간다.
DispatcherServlet 이 viewName 예) "board/list" 을 가지고 ViewResolver 에게 요청한다. DispatcherServlet 은 요청 핸들러로부 view 이름을 리턴받지 못하면 페이지 컨트롤러의 요청 URL 을 view 이름으로 전달한다.
여기서 view 위치정보 "/WEB-INF/jsp/board/list.jsp" 를 리턴한다.
이걸 받아서 DispatcherServlet 이 list.jsp 를 실행한다. 결과를 리턴 하는데 인터셉터가 중간에 들어간다.
"spring viewresolver architecture" 검색 이미지
60-3 공용 객체와 웹관련 객체
공용 객체 : Service, DAO 등 / 웹관련 객체 : Controller, Intercepter 등
각각의 프론트 컨트롤러당 IoC 컨테이너를 보유하고 있다.
하나의 앱이 여러 프론트 컨트롤러 보유할 수 있다.
/app/* 를 처리하는 《Front Controller》DispatcherServlet 은 《IoC 컨테이너》WebApplicationContext 를 보유한다. 웹 관련 객체만 둔다 → AppConfig.class
관리 : AuthController, BoardController + 인터셉터
/admin/* 를 처리하는 《Front Controller》DispatcherServlet 은 《IoC 컨테이너》WebApplicationContext 를 보유한다. 웹 관련 객체만 둔다 → AdminConfig.class
관리 : StudentController, TeacherController + 인터셉터
《ServletContextListener》ContextLoaderListener 는 《IoC 컨테이너》WebApplicationContext 를 보유한다. 이를 Root IoC 컨테이너라 부른다 → RootConfig.class
Front Controller 들이 공유한다. 그러므로 service 객체를 여기 둬야한다.
관리 : BoardService, StudentService, TeacherService, BoardDao, BoardFileDao, StudentDao, TeacherDao, MemberDao, Mybatis 관련 객체, 트랜잭션 관련 객체(SqlSessionFactory, PlatformTransactionManager)
유지보수를 위해 이름을 길게 작성하는게 낫다
60-3 필터링 기술들 - 3가지
필터링 기술은 3가지가 있다.
- Servlet Container 에서 서블릿 사이의 서블릿 필터
- 서블릿에서 페이지 컨트롤러 사이의 인터셉터(preHandle, postHandle, afterCompletion)
- IoC 컨테이너 관리 객체 메서드 전/후의 AOP (Aspect Oriented Programming)
요청이 Servlet Container 로 오면 서블릿 필터(javax.servlet.Filter 구현체) 를 거쳐서 《서블릿》DispatcherServlet 으로 전달한다. 여기서 인터셉터나 AOP를 거쳐서 Page Controller 를 call 한다. Page Controller 에서 AOP 거쳐서 Service 를 call 한다. 여기서 AOP 거쳐서 DAO 를 call 한다. 리턴시 AOP 거쳐서 한다. JSP 에서 DispatcherServlet 으로 리턴시 인터셉터 거친다.
*AOP 동작원리
Caller 가 Proxy 의 m() 실행하면 여기서 선행작업 후 Original 의 m() 호출하고 후행작업 한다. 원래 코드를 손대지 않고 기능을 추가하는 방법이다.
60-3. Interceptor
Spring WebMVC 에서 《interface》WebMvcConfigurer 를 구현한 Java Config 를 call 한다. Java Config 설정에 따라 Spring Framework 를 준비한다.
### 60-3. Spring WebMVC 프론트 컨트롤러 도입하기
- 공용 객체와 웹 관련 객체를 구분해 관리하는 방법: Root vs Servlet WebApplicationContext
- 로그인 여부를 필터 대신 인터셉터로 처리하는 방법
AppConfig 를 공용 객체, 웹 객체(웹 안에서도 app, admin)으로 나눈다.
공용 : service 객체 및 같이 사용하는 객체, mybatis 관련 객체, 트랜잭션 관련 객체
app : auth, board
admin : student, teacher
/app/* 요청 처리하는 《프론트 컨트롤러》Dispatcher Servlet 정의한다.
package bitcamp.myapp.config;
import javax.servlet.Filter;
import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class AppWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected String getServletName() {
return "app";
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?> [] {AppConfig.class};
}
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected String[] getServletMappings() {
return new String[] {"/app/*"};
}
@Override
protected void customizeRegistration(Dynamic registration) {
registration.setMultipartConfig(new MultipartConfigElement(
System.getProperty("java.io.tmpdir"), // 클라이언트가 보낸 파일을 임시 보관할 폴더
1024 * 1024 * 20, // 한 파일의 최대 크기
1024 * 1024 * 20 * 10, // 한 요청 당 최대 총 파일 크기
1024 * 1024 * 1 // 클라이언트가 보낸 파일을 메모리에 임시 보관하는 최대 크기.
// 최대 크기를 초과하면 파일에 내보낸다.
));
}
@Override
protected Filter[] getServletFilters() {
return new Filter[] {new CharacterEncodingFilter("UTF-8")};
}
}
《IoC 컨테이너》 설정 파일인 AppConfig.java 는 다음과 같이 설정한다.
@ComponentScan 의 value 를 controller 패키지로 설정한다. excludeFilters 에 검색 제외할 클래스 지정한다.
@EnableWebMvc 붙여서 로 WebMVC 설정을 수행하라고 지시한다.
인스턴스 블록으로 객체 생성시 콘솔에 출력한다.
addInterceptors(...) 매서드로 인터셉터 등록한다.
package bitcamp.myapp.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
import bitcamp.myapp.controller.StudentController;
import bitcamp.myapp.controller.TeacherController;
import bitcamp.myapp.web.interceptor.AuthInterceptor;
//@Configuration
@ComponentScan(
value = "bitcamp.myapp.controller",
excludeFilters = {
@Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {StudentController.class, TeacherController.class})
})
// WebMVC 관련 설정을 처리하고 싶다면 다음 애노테이션을 추가하라!
// => WebMVC 관련 설정을 수행하는 클래스를 정의했으니,
// WebMvcConfigurer 구현체를 찾아
// 해당 인터페이스에 정의된대로 메서드를 호출하여
// 설정을 수행하라는 의미다!
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {
{
System.out.println("AppConfig 생성됨!");
}
@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
@Bean
public ViewResolver viewResolver() {
// 페이지 컨트롤러가 jsp 경로를 리턴하면
// viewResolver가 그 경로를 가지고 최종 jsp 경로를 계산한 다음에
// JstlView를 통해 실행한다.
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
// WebMvcConfigurer 규칙에 맞춰 인터셉터를 등록한다.
@Override
public void addInterceptors(InterceptorRegistry registry) {
System.out.println("AppConfig.addInterceptors() 호출됨!");
registry.addInterceptor(
new AuthInterceptor()).addPathPatterns("/**/*insert", "/**/*update", "/**/*delete");
}
}
인터셉터 호출은 preHandle() → BoardController → postHandle() → JSP 실행 → afterCompletion() 순으로 진행된다.
AuthIntercepter.preHandle 호출됨!
BoardController.list() 호출됨!
AuthIntercepter.postHandle() 호출됨!
board/list.jsp 실행!
AuthIntercepter.afterCompletion() 호출됨!
myapp > web > interceptor 패키지 생성해서 클래스 파일 생성한다.
preHandle(...) 메서드 등록하여 login 체크하는 코드 넣어준다. return false 하면 요청을 거부하고 처리 흐름을 중단한다.
package bitcamp.myapp.web.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import bitcamp.myapp.vo.Member;
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
Member loginUser = (Member) request.getSession().getAttribute("loginUser");
if (loginUser == null) {
response.sendRedirect(request.getContextPath() + "/app/auth/form");
return false;
}
return true;
}
}
/admin/* 요청 처리하는 《프론트 컨트롤러》Dispatcher Servlet 정의한다.
ContextLoaderListener 의 IoC 컨테이너 설정한다. 모든 서블릿이 공유하므로 어느 서블릿이든 한 곳에서만 설정한다.
package bitcamp.myapp.config;
import javax.servlet.Filter;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class AdminWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// DispatcherServlet 의 서블릿이름을 설정한다.
@Override
protected String getServletName() {
return "admin";
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] {AdminConfig.class};
}
@Override
protected Class<?>[] getRootConfigClasses() {
// ContextLoaderListener의 IoC 컨테이너 설정
// ContextLoaderListener는 모든 서블릿이 공유하기 때문에
// 여기에서 한 번 설정하면 다른 쪽에서는 설정할 필요가 없다.
return new Class<?>[] {RootConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[] {"/admin/*"};
}
@Override
protected Filter[] getServletFilters() {
return new Filter[] {new CharacterEncodingFilter("UTF-8")};
}
}
《IoC 컨테이너》 설정 파일인 AdminConfig.java 설정한다.
AuthInterceptor(), AdminCheckInterceptor() 등록한다.
package bitcamp.myapp.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
import bitcamp.myapp.controller.AuthController;
import bitcamp.myapp.controller.BoardController;
import bitcamp.myapp.controller.DownloadController;
import bitcamp.myapp.web.interceptor.AdminCheckInterceptor;
import bitcamp.myapp.web.interceptor.AuthInterceptor;
//@Configuration
@ComponentScan(
value = "bitcamp.myapp.controller",
excludeFilters = {
@Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {AuthController.class, BoardController.class, DownloadController.class})
})
@EnableWebMvc // 프론트 컨트롤러 각각에 대해 설정해야 한다.
public class AdminConfig implements WebMvcConfigurer {
{
System.out.println("AdminConfig 생성됨!");
}
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
// WebMvcConfigurer 규칙에 맞춰 인터셉터를 등록한다.
@Override
public void addInterceptors(InterceptorRegistry registry) {
System.out.println("AdminConfig.addInterceptors() 호출됨!");
registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/**");
registry.addInterceptor(new AdminCheckInterceptor()).addPathPatterns("/**");
}
}
프론트 컨트롤러에 들어온 요청 주소가 /admin/* 일 때 loginUser 가 admin 계정이 아니면 로그인 form 으로 튕겨낸다.
package bitcamp.myapp.web.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import bitcamp.myapp.vo.Member;
public class AdminCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
Member loginUser = (Member) request.getSession().getAttribute("loginUser");
if (!loginUser.getEmail().equals("admin@test.com")) {
response.sendRedirect(request.getContextPath() + "/app/auth/form");
return false;
}
return true;
}
}
RootConfig.java 는 DataSource, PlatformTransactionManager, SqlSessionFactory 객체 메서드를 둔다.
package bitcamp.myapp.config;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@ComponentScan(
value = "bitcamp.myapp",
excludeFilters = {
@Filter(
type = FilterType.REGEX,
pattern = {"bitcamp.myapp.controller.*"})
})
@PropertySource("classpath:/bitcamp/myapp/config/jdbc.properties")
@MapperScan("bitcamp.myapp.dao")
@EnableTransactionManagement
public class RootConfig {
{
System.out.println("RootConfig 생성됨!");
}
@Bean
public DataSource dataSource(
@Value("${jdbc.driver}") String jdbcDriver,
@Value("${jdbc.url}") String url,
@Value("${jdbc.username}") String username,
@Value("${jdbc.password}") String password) {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName(jdbcDriver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
return ds;
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) throws Exception {
System.out.println("PlatformTransactionManager 객체 생성! ");
return new DataSourceTransactionManager(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource, ApplicationContext appCtx) throws Exception {
System.out.println("SqlSessionFactory 객체 생성!");
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setTypeAliasesPackage("bitcamp.myapp.vo");
factoryBean.setMapperLocations(appCtx.getResources("classpath*:bitcamp/myapp/mapper/*Mapper.xml"));
return factoryBean.getObject();
}
}
Tiles 뷰 기술 적용
docs.spring.io > Web Servlet > Spring Web MVC > View Technologies
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-view
central.sonatype.com 에서 "tiles jsp" 검색한다.
Referenced Libraries 에 아래 추가된다.
61. Tiles View 사용하기
///////////////////////
/app/board/list 요청이 《Front Controller》Dispatcher Servlet 으로 오면 인터셉터를 거쳐 list() 를 BoardController 에게 지시한다. 리턴값 없다.
리턴 값이 없으므로 요청 들어온 주소를 UrlBasedViewResolver 로 보낸다. prefix 로 "app/" 붙인 후 객체 리턴한다.
이를 TilesView 객체를 이용하여 JSP 를 읽어들인다.
### 61. Tiles 뷰 기술을 적용하기
### 61. Tiles 뷰 기술을 적용하기
- JSP에 Tiles 뷰 기술을 적용하여 UI 템플릿을 다루는 방법
AppConfig.java 에 tilesViewResolver() 등록한다. prefix 설정하고 뷰리졸버의 우선 순위를 1로 한다.
기존 뷰리졸버의 우선 순위는 2로 한다.
package bitcamp.myapp.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
import org.springframework.web.servlet.view.UrlBasedViewResolver;
import org.springframework.web.servlet.view.tiles3.TilesView;
import bitcamp.myapp.controller.StudentController;
import bitcamp.myapp.controller.TeacherController;
import bitcamp.myapp.web.interceptor.AuthInterceptor;
//@Configuration
@ComponentScan(
value = "bitcamp.myapp.controller",
excludeFilters = {
@Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {StudentController.class, TeacherController.class})
})
// WebMVC 관련 설정을 처리하고 싶다면 다음 애노테이션을 추가하라!
// => WebMVC 관련 설정을 수행하는 클래스를 정의했으니,
// WebMvcConfigurer 구현체를 찾아
// 해당 인터페이스에 정의된대로 메서드를 호출하여
// 설정을 수행하라는 의미다!
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {
{
System.out.println("AppConfig 생성됨!");
}
@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
@Bean
public ViewResolver viewResolver() {
// 페이지 컨트롤러가 jsp 경로를 리턴하면
// viewResolver가 그 경로를 가지고 최종 jsp 경로를 계산한 다음에
// JstlView를 통해 실행한다.
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
viewResolver.setOrder(2);
return viewResolver;
}
@Bean
public ViewResolver tilesViewResolver() {
UrlBasedViewResolver vr = new UrlBasedViewResolver();
// Tiles 설정에 따라 템플릿을 실행할 뷰 처리기를 등록한다.
vr.setViewClass(TilesView.class);
// request handler가 리턴한 view name 앞에 일반 페이지임을 표시하기 위해 접두사를 붙인다.
vr.setPrefix("app/");
// 뷰리졸버의 우선 순위를 InternalResourceViewResolver 보다 우선하게 한다.
vr.setOrder(1);
return vr;
}
// WebMvcConfigurer 규칙에 맞춰 인터셉터를 등록한다.
@Override
public void addInterceptors(InterceptorRegistry registry) {
System.out.println("AppConfig.addInterceptors() 호출됨!");
registry.addInterceptor(
new AuthInterceptor()).addPathPatterns("/**/*insert", "/**/*update", "/**/*delete");
}
}
AdminConfig.java 에 tilesViewResolver() 등록한다.
package bitcamp.myapp.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
import org.springframework.web.servlet.view.UrlBasedViewResolver;
import org.springframework.web.servlet.view.tiles3.TilesView;
import bitcamp.myapp.controller.AuthController;
import bitcamp.myapp.controller.BoardController;
import bitcamp.myapp.controller.DownloadController;
import bitcamp.myapp.web.interceptor.AdminCheckInterceptor;
import bitcamp.myapp.web.interceptor.AuthInterceptor;
//@Configuration
@ComponentScan(
value = "bitcamp.myapp.controller",
excludeFilters = {
@Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {AuthController.class, BoardController.class, DownloadController.class})
})
@EnableWebMvc // 프론트 컨트롤러 각각에 대해 설정해야 한다.
public class AdminConfig implements WebMvcConfigurer {
{
System.out.println("AdminConfig 생성됨!");
}
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
viewResolver.setOrder(2);
return viewResolver;
}
@Bean
public ViewResolver tilesViewResolver() {
UrlBasedViewResolver vr = new UrlBasedViewResolver();
vr.setViewClass(TilesView.class);
vr.setPrefix("admin/");
vr.setOrder(1);
return vr;
}
// WebMvcConfigurer 규칙에 맞춰 인터셉터를 등록한다.
@Override
public void addInterceptors(InterceptorRegistry registry) {
System.out.println("AdminConfig.addInterceptors() 호출됨!");
registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/**");
registry.addInterceptor(new AdminCheckInterceptor()).addPathPatterns("/**");
}
}
WEB-INF 에 defs 폴더 만들고 app-tiles.xml 및 admin-tiles.xml 생성한다.
공통 레이아웃인 admin-template.jsp 정의하고 안에서 사용할 admin-header.jsp, admin-footer.jsp 정의한다.
요청 핸들러가 리턴한 경로로 TilesView 템플릿 엔진이 사용할 JSP URL 을 정의한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE tiles-definitions PUBLIC
"-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN"
"http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>
<!-- 여러 템플릿에서 공통으로 사용할 레이아웃을 정의한다. -->
<definition name="app-base" template="/WEB-INF/tiles/app-template.jsp">
<!-- template.jsp 안에서 사용할 JSP 파일의 이름을 설정한다. -->
<put-attribute name="header" value="/WEB-INF/tiles/header.jsp" />
<put-attribute name="footer" value="/WEB-INF/tiles/footer.jsp" />
</definition>
<!-- request handler가 리턴한 JSP의 경로를 가지고
TilesView 템플릿 엔진이 사용할 JSP URL을 정의한다.
예) return url ==> "board/list"
body url ==> "/WEB-INF/tiles/boad/list.jsp" -->
<definition name="app/*/*" extends="app-base">
<put-attribute name="body" value="/WEB-INF/tiles/{1}/{2}.jsp" />
</definition>
</tiles-definitions>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE tiles-definitions PUBLIC
"-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN"
"http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>
<!-- 여러 템플릿에서 공통으로 사용할 레이아웃을 정의한다. -->
<definition name="admin-base" template="/WEB-INF/tiles/admin-template.jsp">
<!-- template.jsp 안에서 사용할 JSP 파일의 이름을 설정한다. -->
<put-attribute name="header" value="/WEB-INF/tiles/admin-header.jsp" />
<put-attribute name="footer" value="/WEB-INF/tiles/admin-footer.jsp" />
</definition>
<!-- request handler가 리턴한 JSP의 경로를 가지고
TilesView 템플릿 엔진이 사용할 JSP URL을 정의한다.
예) return url ==> "board/list"
body url ==> "/WEB-INF/tiles/boad/list.jsp" -->
<definition name="admin/*/*" extends="admin-base" >
<put-attribute name="body" value="/WEB-INF/tiles/{1}/{2}.jsp" />
</definition>
</tiles-definitions>
app-template.jsp 에 디렉티브 엘리먼트로 tiles 라이브러리 불러들인다. (admin-template.jsp 도 동일함)
<tiles:*> 사용해서 header, body, footer 에 놓일 jsp 위치 지정한다.
refresh 값이 있는지 검사해서 있으면 <meta *> 태그로 "Refresh" 지정한다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles"%>
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<c:if test="${not empty refresh}">
<meta http-equiv='Refresh' content='1;url=${refresh}'>
</c:if>
<title>비트캠프 - NCP 1기</title>
<style>
header {
height: 60px;
background-color: gray;
color: black;
}
footer {
height: 60px;
background-color: navy;
color: white;
}
</style>
</head>
<body>
<tiles:insertAttribute name="header"/>
<div class='container'>
<tiles:insertAttribute name="body"/>
</div>
<tiles:insertAttribute name="footer"/>
</body>
</html>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles"%>
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<c:if test="${not empty refresh}">
<meta http-equiv='Refresh' content='1;url=${refresh}'>
</c:if>
<title>비트캠프 - NCP 1기</title>
<style>
header {
height: 80px;
background: black;
color: white;
}
footer {
height: 100px;
background: darkgray;
color: white;
}
a:visited {
color: white;
}
</style>
</head>
<body>
<tiles:insertAttribute name="header"/>
<div class='container'>
<tiles:insertAttribute name="body"/>
</div>
<tiles:insertAttribute name="footer"/>
</body>
</html>
*-template.jsp 외에는 <body> 이전부터 </body> 이후 태그 삭제한다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<header>
강의관리시스템 - 관리자
<a href="/web/admin/student/list">학생관리</a>
<a href="/web/admin/teacher/list">강사관리</a>
<c:if test="${empty loginUser}">
<a href="/web/app/auth/form">로그인</a>
</c:if>
<c:if test="${not empty loginUser}">
<a href="/web/app/auth/logout">로그아웃(${loginUser.name})</a>
</c:if>
</header>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<footer>
비트캠프 | 서울 강남구 강남대로94길 20, 삼오빌딩 6층| 사업자등록번호 : 328-85-02112
</footer>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<h1>게시판(Tiles)</h1>
<div><a href='form'>새 글</a></div>
<table border='1'>
<tr>
<th>번호</th> <th>제목</th> <th>작성자</th> <th>작성일</th> <th>조회수</th>
</tr>
<c:forEach items="${boards}" var="b">
<tr>
<td>${b.no}</td>
<td><a href='view?no=${b.no}'>${b.title}</a></td>
<td>${b.writer.name}</td>
<td>${b.createdDate}</td>
<td>${b.viewCount} </td>
</tr>
</c:forEach>
</table>
<form action='list' method='get'>
<input type='text' name='keyword' value='${param.keyword}'>
<button>검색</button>
</form>
나머지는 생략한다.
*Controller 에서 refresh 해야하는 insert, update, delete 메서드에서 해당 서비스 수행 후 model.addAttribute("refresh", "list"); 코드 넣어 값을 세팅해준다.
package bitcamp.myapp.controller;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;
import bitcamp.myapp.service.BoardService;
import bitcamp.myapp.vo.Board;
import bitcamp.myapp.vo.BoardFile;
import bitcamp.myapp.vo.Member;
@Controller
@RequestMapping("/board")
public class BoardController {
{
System.out.println("BoardController 생성됨!");
}
// ServletContext 는 요청 핸들러의 파라미터로 주입 받을 수 없다.
// 객체의 필드로만 주입 받을 수 있다.
@Autowired private ServletContext servletContext;
@Autowired private BoardService boardService;
@GetMapping("form")
public void form() {
}
@PostMapping("insert")
public void insert(
Board board,
List<MultipartFile> files,
Model model,
HttpSession session) {
try {
Member loginUser = (Member) session.getAttribute("loginUser");
Member writer = new Member();
writer.setNo(loginUser.getNo());
board.setWriter(writer);
List<BoardFile> boardFiles = new ArrayList<>();
for (MultipartFile file : files) {
if (file.isEmpty()) {
continue;
}
String filename = UUID.randomUUID().toString();
file.transferTo(new File(servletContext.getRealPath("/board/upload/" + filename)));
BoardFile boardFile = new BoardFile();
boardFile.setOriginalFilename(file.getOriginalFilename());
boardFile.setFilepath(filename);
boardFile.setMimeType(file.getContentType());
boardFiles.add(boardFile);
}
board.setAttachedFiles(boardFiles);
boardService.add(board);
model.addAttribute("refresh", "list");
} catch (Exception e) {
e.printStackTrace();
model.addAttribute("error", "data");
}
}
@GetMapping("list")
public void list(String keyword, Model model) {
System.out.println("BoardController.list() 호출됨!");
model.addAttribute("boards", boardService.list(keyword));
}
@GetMapping("view")
public void view(int no, Model model) {
model.addAttribute("board", boardService.get(no));
}
@PostMapping("update")
public String update(
Board board,
List<MultipartFile> files,
Model model,
HttpSession session) {
try {
Member loginUser = (Member) session.getAttribute("loginUser");
Board old = boardService.get(board.getNo());
if (old.getWriter().getNo() != loginUser.getNo()) {
return "redirect:../auth/fail";
}
List<BoardFile> boardFiles = new ArrayList<>();
for (MultipartFile file : files) {
if (file.isEmpty()) {
continue;
}
String filename = UUID.randomUUID().toString();
file.transferTo(new File(servletContext.getRealPath("/board/upload/" + filename)));
BoardFile boardFile = new BoardFile();
boardFile.setOriginalFilename(file.getOriginalFilename());
boardFile.setFilepath(filename);
boardFile.setMimeType(file.getContentType());
boardFile.setBoardNo(board.getNo());
boardFiles.add(boardFile);
}
board.setAttachedFiles(boardFiles);
boardService.update(board);
model.addAttribute("refresh", "list");
} catch (Exception e) {
e.printStackTrace();
model.addAttribute("error", "data");
}
return "board/update";
}
@PostMapping("delete")
public String delete(int no, Model model, HttpSession session) {
try {
Member loginUser = (Member) session.getAttribute("loginUser");
Board old = boardService.get(no);
if (old.getWriter().getNo() != loginUser.getNo()) {
return "redirect:../auth/fail";
}
boardService.delete(no);
} catch (Exception e) {
e.printStackTrace();
model.addAttribute("error", "data");
}
model.addAttribute("refresh", "list");
return "board/delete";
}
@GetMapping("filedelete")
public String filedelete(int boardNo, int fileNo, HttpSession session) {
Member loginUser = (Member) session.getAttribute("loginUser");
Board old = boardService.get(boardNo);
if (old.getWriter().getNo() != loginUser.getNo()) {
return "redirect:../auth/fail";
} else {
boardService.deleteFile(fileNo);
return "redirect:view?no=" + boardNo;
}
}
}
프로젝트 수행 절차
① 프로젝트 주제 선정
- 개요 (현황 → 문제점, 해결방안 → 이점) : 문제점이 없더라도 이야기를 꾸며내라
- UI prototype (주요 기능)
② Use Case 모델링
- Actor 식별 (시스템 사용)
- Use Case 식별 (사용자의 기능)
- Use Case Model
- Use Case 명세서
→ UI prototype 상세화
③ DB 모델링
- 테이블 식별
- 컬럼 식별
- key 식별
- 제약조건 식별
→ DB Model → ER-Diagram : eXERD (이클립스 기반 DB 모델링 구조)
④ 구현
- 기능별 full stack 작업 → 테스트. 반복
⑤ 발표
- 프로젝트 설명
- 시연
- 소감
조언
*어떤 객체가 어떤 일을 하는지 이름 파악을 빨리 해야한다. 회사 생활도 마찬가지다.
과제
/
'네이버클라우드 AIaaS 개발자 양성과정 1기 > Spring Framework, Spring Boot' 카테고리의 다른 글
[비트캠프] 84일차(18주차2일) - Spring Framework(Thymeleaf, ControllerAdvice, log4j 2), myapp-63 (0) | 2023.03.07 |
---|---|
[비트캠프] 83일차(18주차1일) - Spring Framework(Thymeleaf), myapp-62 (0) | 2023.03.06 |
[Java] 예제 소스 정리 - Spring WebMVC (0) | 2023.03.03 |
[비트캠프] 81일차(17주차3일) - Spring Framework: myapp-60-1, 2 (0) | 2023.03.02 |
[Java] 예제 소스 정리 - Spring IoC (0) | 2023.03.01 |