개발자입니다
[비트캠프] 81일차(17주차3일) - Spring Framework: myapp-60-1, 2 본문
[비트캠프] 81일차(17주차3일) - Spring Framework: myapp-60-1, 2
끈기JK 2023. 3. 2. 20:12
central.sonatype.com 에서 "mybatis-spring" 검색해서 아래 코드 build.gradle 에 복붙한다. $ gradle eclipse 한다.
References Libraries 에 아래 파일 추가된다.
mybatis.org 에서 Getting Started 에 있는 아래 메서드 복사해서 AppConfig 에 넣는다.
DB 커넥션 풀 준비할때 DriverManagerDataSource 객체 사용하는데 spring-jdbc 라이브러리 있어야 한다.
central.sonatype.com 에서 "spring-jdbc" 검색해서 5.3.25 (Jakarta EE는 6.x)버전 선택 후 아래 코드 build.gradle 에 복붙한다. $ gradle eclipse 한다.
References Libraries 에 아래 파일 추가된다.
docs.spring.io 에서 애노테이션 정보 확인 가능하다.
Transaction Propagation(관리) 정책
참고 : https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-propagation
① REQUIRED
Caller 가 m1() 을 call 한다. 트랜잭션 없이 메서드 호출하면 트랜잭션을 자동 생성한다.
여기서 m2() 를 call 한다. 호출자가 이미 트랜잭션 상태에 있다면 기존 트랜잭션에 속해서 실행된다.
REQUIRED 는 호출자의 트랜잭션 상태에 따라
X → tx1 (새로 생성)
tx1 → tx1 (기존 트랜잭션에 소속됨)
② REQUIRES_NEW
Caller 가 m1() 을 call 한다. 트랜잭션 없이 메서드 호출하면 트랜잭션을 자동 생성한다.
여기서 m2() 를 call 한다. 이전 메서드가 트랜잭션 상에서 실행하고 있더라도 무조건 새로 트랜잭션을 시작한다.
REQUIRES_NEW 는 호출자의 트랜잭션 상태에 따라
X → tx1 (새로 시작)
tx1 → tx2 (새로 시작)
③ MANDATORY
Caller 가 m1() 을 call 한다. 트랜잭션 없이 호출하면 예외 발생한다.
Caller 가 tx1 있는 상태에서 m1() 을 call 한다. m1() 은 tx1 사용한다.
MANDATORY 는 호출자의 트랜잭션 상태에 따라
X → 예외
tx1 → tx1
④ SUPPORT
Caller 가 m1() 을 call 한다. m1() 은 트랜잭션 없이 실행한다.
Caller 가 tx1 에서 m1() 을 call 한다. m1() 은 트랜잭션 없이 실행한다.
SUPPORT 는 호출자의 트랜잭션 상태에 따라
X → NONE
tx1 → NONE
⑤ NEVER
Caller 가 m1() 을 call 한다. m1() 은 트랜잭션 없이 실행한다.
Caller 가 tx1 에서 m1() 을 call 한다. m1() 에서 예외 발생한다.
NEVER 는 호출자의 트랜잭션 상태에 따라
X → NONE
tx1 → 예외
### 60-1. Spring WebMVC 프론트 컨트롤러 도입하기
- Spring WebMVC 프레임워크에서 제공하는 DispatcherServlet 사용법
- Spring WebMVC에서 mutipart/form-data 요청 처리를 설정하는 방법
- Mybatis와 Spring IoC 컨테이너를 연동하는 방법
- 트랜잭션을 다루는 방법(자바 코드로 직접 제어)
리스너에 IoC Container 생성한다.
Spring WebMVC 프레임워크의 DispatcherServlet 객체 생성해서 사용한다.
"/app/*" 주소 매핑한다.
서버 시작시 객체 생성하도록 LoadOnStartup 값 설정한다.
멀티파트 처리를 위해 객체 생성 및 설정한다.
package bitcamp.myapp.listener;
import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletRegistration.Dynamic;
import javax.servlet.annotation.WebListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import bitcamp.myapp.config.AppConfig;
@WebListener
public class AppInitListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
// Spring IoC 컨테이너 준비
AnnotationConfigWebApplicationContext iocContainer = new AnnotationConfigWebApplicationContext();
iocContainer.register(AppConfig.class);
// DispatcherServlet 프론트 컨트롤러 준비
DispatcherServlet dispatcherServlet = new DispatcherServlet(iocContainer);
Dynamic registration = sce.getServletContext().addServlet("app", dispatcherServlet);
registration.addMapping("/app/*");
registration.setLoadOnStartup(1);
registration.setMultipartConfig(new MultipartConfigElement(
System.getProperty("java.io.tmpdir"), // 클라이언트가 보낸 파일을 임시 보관할 폴더
1024 * 1024 * 20, // 한 파일의 최대 크기
1024 * 1024 * 20 * 10, // 한 요청 당 최대 총 파일 크기
1024 * 1024 * 1 // 클라이언트가 보낸 파일을 메모리에 임시 보관하는 최대 크기.
// 최대 크기를 초과하면 파일에 내보낸다.
));
}
}
우리가 만든 DispatcherServlet 삭제한다.
@PropertySource 로 jdbc.properties 파일 로딩한다. mybatis-config.xml 파일 삭제한다.
@MapperScan 으로 *Mapper.xml 파일과 dao 패키지 파일 이용해 구현체 자동 생성한다.
@Bean 등록하는데 transactionManager(...), sqlSessionFactory(...), multipartResolver(...) 등록한다. util 패키지 삭제한다.
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.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
//@Configuration
// Spring IoC 컨테이너가 자동 생성할 클래스를 찾을 수 있도록 패키지를 지정한다.
@ComponentScan("bitcamp.myapp")
// JDBC 설정 정보를 담고 있는 .properties 파일을 로딩한다.
@PropertySource("classpath:/bitcamp/myapp/config/jdbc.properties")
// Mybatis-Spring 라이브러리에 있는 클래스를 사용하여 DAO 인터페이스의 구현체를 자동 생성하기
@MapperScan("bitcamp.myapp.dao")
public class AppConfig {
// 시스템 property 값 가져오기
@Autowired Environment env;
// DB 커넥셕풀 객체 준비
@Bean
public DataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName(env.getProperty("jdbc.driver"));
ds.setUrl(env.getProperty("jdbc.url"));
ds.setUsername(env.getProperty("jdbc.username"));
ds.setPassword(env.getProperty("jdbc.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();
}
// Servlet3.0의 멀티파트 요청 데이터를 처리하려면
// 1) 서블릿에 멀티파트 파일 처리에 관한 설정 정보를 담은 MultipartConfigElement를 등록해야 한다.
// 2) 스프링 WebMVC에는 Servlet3.0 API를 이용해 멀티파트 데이터를 처리한 후에
// 페이지 컨트롤러에게 그 값들을 전달해주는 StandardServletMultipartResolver를 등록해야 한다.
// 주의!
// 이때 MultipartResolver 구현체 bean 이름은 "multipartResolver" 여야 한다.
@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
// Apache commons-fileupload 라이브러리로 멀티파트 요청 데이터를 처리하려면
// 1) 프로젝트에 commons-fileupload 라이브러리를 별도로 추가해야 한다.
// 2) 스프링 WebMVC에는 이 라이브러리를 이용해 멀티파트 데이터를 처리한 후에
// 페이지 컨트롤러에게 그 값들을 전달해주는 CommonsMultipartResolver를 등록해야 한다.
// 주의!
// 이때 MultipartResolver 구현체 bean 이름은 "multipartResolver" 여야 한다.
// @Bean
// public MultipartResolver multipartResolver() {
// return new CommonsMultipartResolver();
// }
}
BoardMapper.xml 의 namespace=" " 값을 인터페이스 이름과 똑같이 bitcamp.myapp.dao.BoardDao 로 수정한다.
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="bitcamp.myapp.dao.BoardDao">
<!-- 후략 -->
TeacherMapper, StudentMapper 등도 마찬가지로 적용한다.
스프링 프레임워크의 트랜잭션 사용한다.
package bitcamp.myapp.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import bitcamp.myapp.dao.BoardDao;
import bitcamp.myapp.dao.BoardFileDao;
import bitcamp.myapp.service.BoardService;
import bitcamp.myapp.vo.Board;
import bitcamp.myapp.vo.BoardFile;
@Service
public class DefaultBoardService implements BoardService {
@Autowired private PlatformTransactionManager txManager;
@Autowired private BoardDao boardDao;
@Autowired private BoardFileDao boardFileDao;
@Override
public void add(Board board) {
// 트랜잭션 동작을 설정
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setName("tx1");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 위에서 설정한 대로 동작할 트랜잭션을 준비
TransactionStatus status = txManager.getTransaction(def);
try {
boardDao.insert(board);
if (board.getAttachedFiles().size() > 0) {
for (BoardFile boardFile : board.getAttachedFiles()) {
boardFile.setBoardNo(board.getNo());
}
boardFileDao.insertList(board.getAttachedFiles());
}
txManager.commit(status); // 트랜잭션 정책에 따라 commit 수행
} catch (Exception e) {
txManager.rollback(status); // 틀랜잭션 정책에 따라 rollback 수행
throw new RuntimeException(e);
}
}
@Override
public List<Board> list(String keyword) {
return boardDao.findAll(keyword);
}
@Override
public Board get(int no) {
Board b = boardDao.findByNo(no);
if (b != null) {
boardDao.increaseViewCount(no);
}
return b;
}
@Override
public void update(Board board) {
// 트랜잭션 동작을 설정
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setName("tx1");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 위에서 설정한 대로 동작할 트랜잭션을 준비
TransactionStatus status = txManager.getTransaction(def);
try {
if (boardDao.update(board) == 0) {
throw new RuntimeException("게시글이 존재하지 않습니다!");
}
if (board.getAttachedFiles().size() > 0) {
boardFileDao.insertList(board.getAttachedFiles());
}
txManager.commit(status);
} catch (Exception e) {
txManager.rollback(status);
throw e;
}
}
@Override
public void delete(int no) {
// 트랜잭션 동작을 설정
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setName("tx1");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 위에서 설정한 대로 동작할 트랜잭션을 준비
TransactionStatus status = txManager.getTransaction(def);
try {
boardFileDao.deleteOfBoard(no);
if (boardDao.delete(no) == 0) {
throw new RuntimeException("게시글이 존재하지 않습니다!");
}
txManager.commit(status);
} catch (Exception e) {
txManager.rollback(status);
throw e;
}
}
@Override
public BoardFile getFile(int fileNo) {
return boardFileDao.findByNo(fileNo);
}
@Override
public void deleteFile(int fileNo) {
boardFileDao.delete(fileNo);
}
}
teacher, student 생략
@*Mapping 에서 매개변수를 Board 객체로 한번에 받는다. 멀티파트로 넘어오는 값을 받는 Controller 가 있으면 AppConfig.java 에서 @Bean multipartResolver() 설정해야 한다.
package bitcamp.myapp.controller;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.Part;
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 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 {
// ServletContext 는 요청 핸들러의 파라미터로 주입 받을 수 없다.
// 객체의 필드로만 주입 받을 수 있다.
@Autowired private ServletContext servletContext;
@Autowired private BoardService boardService;
@GetMapping("form")
public String form() {
return "/board/form.jsp";
}
@PostMapping("insert")
public String insert(
Board board,
// String title,
// String content,
Part[] files,
Model model, // ServletRequest 보관소에 저장할 값을 담는 임시 저장소
// 이 객체에 값을 담아 두면 프론트 컨트롤러(DispatcherServlet)가
// ServletRequest 보관소로 옮겨 담을 것이다.
HttpSession session) {
try {
Member loginUser = (Member) session.getAttribute("loginUser");
// Board board = new Board();
// board.setTitle(title);
// board.setContent(content);
Member writer = new Member();
writer.setNo(loginUser.getNo());
board.setWriter(writer);
List<BoardFile> boardFiles = new ArrayList<>();
for (Part part : files) {
if (part.getSize() == 0) {
continue;
}
String filename = UUID.randomUUID().toString();
part.write(servletContext.getRealPath("/board/upload/" + filename));
BoardFile boardFile = new BoardFile();
boardFile.setOriginalFilename(part.getSubmittedFileName());
boardFile.setFilepath(filename);
boardFile.setMimeType(part.getContentType());
boardFiles.add(boardFile);
}
board.setAttachedFiles(boardFiles);
boardService.add(board);
} catch (Exception e) {
e.printStackTrace();
model.addAttribute("error", "data");
}
return "/board/insert.jsp";
}
@GetMapping("list")
public String list(String keyword, Model model) {
model.addAttribute("boards", boardService.list(keyword));
return "/board/list.jsp";
}
@GetMapping("view")
public String view(int no, Model model) {
model.addAttribute("board", boardService.get(no));
return"/board/view.jsp";
}
@PostMapping("update")
public String update(
Board board,
// int no,
// String title,
// String content,
Part[] files,
Model model,
HttpSession session) {
try {
Member loginUser = (Member) session.getAttribute("loginUser");
// Board board = new Board();
// board.setNo(no);
// board.setTitle(title);
// board.setContent(content);
Board old = boardService.get(board.getNo());
if (old.getWriter().getNo() != loginUser.getNo()) {
return "redirect:../auth/fail";
}
List<BoardFile> boardFiles = new ArrayList<>();
for (Part part : files) {
if (part.getSize() == 0) {
continue;
}
String filename = UUID.randomUUID().toString();
part.write(servletContext.getRealPath("/board/upload/" + filename));
BoardFile boardFile = new BoardFile();
boardFile.setOriginalFilename(part.getSubmittedFileName());
boardFile.setFilepath(filename);
boardFile.setMimeType(part.getContentType());
boardFile.setBoardNo(board.getNo());
boardFiles.add(boardFile);
}
board.setAttachedFiles(boardFiles);
boardService.update(board);
} catch (Exception e) {
e.printStackTrace();
model.addAttribute("error", "data");
}
return "/board/update.jsp";
}
@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");
}
return "/board/delete.jsp";
}
@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;
}
}
}
60-2 ServletContainerInitializer 구현체
ServletContainer 를 시작하면 여기에 등록된 Web App 1, 2, 3 이 시작된다.
① ServletContainerInitializer 가 onStartup() 호출한다.
② ServletContextListener 가 contextInitialized() 호출한다. ← Spring 의 DispatcherServlet 을 등록!
이를 ① 의 onStartup() 에서 등록할 수 있다 (Spring 에서 제안하는 방법)
60-2 ServletContainerInitializer 구현체 + Spring WebMVC
ServletContainer 를 시작하고 《interface》ServletContainerInitializer (Servlet API)의 onStartup() 을 실행한다.
구현체인 《concrete》SpringServletContainerInitializer (Spring API) 에서 onStartup() 하는데 메서드 내용 중 WebApplicationInitializer 의 onStartup() 호출한다.
60-2 ServletContainerInitializer 구현체 + WebApplicationInitializer
Servlet Container 를 시작시키면 《ServletContainerInitializer》SpringServletContainerInitializer 의 onStartup() 메서드를 호출한다. 매개변수에 특정 인터페이스를 구현한 클래스들의 정보(명단)를 넘긴다. 명단 : 이 클래스에 지정된 인터페이스 (@HandlesTypes 애노테이션으로 지정된 인터페이스)
《ServletContainerInitializer》 는 spring-web.jar/META-INF/services/javax.servlet.ServletContainerInitializer 파일에 들어있는 클래스를 알아낸다.
그러면 여기서 내부적으로 WebApplicationInitializer 구현체의 onStartup() 을 호출한다.
《interface》WebApplicationInitializer 를 구현한 MyWebApplicationInitializer 에서 하는 일
① Spring IoC 컨테이너 준비
② 프론트 컨트롤러 준비
WebApplicationInitializer 를 구현한 《abstract》AbstractDispatcherServletInitializer 를 상속한 MyWebApplicationInitializer 를 만드는 방법도 있다. 여기서 하는 일
① Spring IoC 컨테이너 준비
② 프론트 컨트롤러 설정(이름, URL, 멀티파트)
《abstract》AbstractDispatcherServletInitializer 를 상속한 《abstract》AbstractAnnotationConfigDispatcherServletInitializer 를 상속한 MyWebApplicationInitializer 를 만드는 방법도 있다. 여기서 하는 일
① Spring IoC 컨테이너의 설정 클래스 준비
② 프론트 컨트롤러 설정
### 60-2. Spring WebMVC 프론트 컨트롤러 도입하기
- 트랜잭션을 다루는 방법(애노테이션으로 제어)
- @PropertySource와 @Value 사용법
- 프론트 컨트롤러를 등록하는 방법
- ServletContextListener 를 이용하여 등록
- WebApplicationInitializer 를 이용하여 등록
- AbstractDispatcherServletInitializer 를 이용하여 등록
- AbstractAnnotationConfigDispatcherServletInitializer 를 이용하여 등록
- 필터를 삽입하는 방법
- JSP 파일의 위치를 변경하는 이유와 방법: InternalResourceViewResolver 사용법
DefaultBoardService 에서 @Transactional 로 트랜잭션 제어한다.
package bitcamp.myapp.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import bitcamp.myapp.dao.BoardDao;
import bitcamp.myapp.dao.BoardFileDao;
import bitcamp.myapp.service.BoardService;
import bitcamp.myapp.vo.Board;
import bitcamp.myapp.vo.BoardFile;
@Service
public class DefaultBoardService implements BoardService {
@Autowired private BoardDao boardDao;
@Autowired private BoardFileDao boardFileDao;
@Transactional
@Override
public void add(Board board) {
boardDao.insert(board);
if (board.getAttachedFiles().size() > 0) {
for (BoardFile boardFile : board.getAttachedFiles()) {
boardFile.setBoardNo(board.getNo());
}
boardFileDao.insertList(board.getAttachedFiles());
}
}
@Override
public List<Board> list(String keyword) {
return boardDao.findAll(keyword);
}
@Override
public Board get(int no) {
Board b = boardDao.findByNo(no);
if (b != null) {
boardDao.increaseViewCount(no);
}
return b;
}
@Transactional
@Override
public void update(Board board) {
if (boardDao.update(board) == 0) {
throw new RuntimeException("게시글이 존재하지 않습니다!");
}
if (board.getAttachedFiles().size() > 0) {
boardFileDao.insertList(board.getAttachedFiles());
}
}
@Transactional
@Override
public void delete(int no) {
boardFileDao.deleteOfBoard(no);
if (boardDao.delete(no) == 0) {
throw new RuntimeException("게시글이 존재하지 않습니다!");
}
}
@Override
public BoardFile getFile(int fileNo) {
return boardFileDao.findByNo(fileNo);
}
@Override
public void deleteFile(int fileNo) {
boardFileDao.delete(fileNo);
}
}
@EnableTransactionManagement 붙여야 @Transactional 사용 가능하다.
@Value 로 jdbc.properties 정보 받는다.
뷰컴포넌트(예: JSP)의 경로를 다루는 객체인 viewResolver 를 준비한다.
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.PropertySource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
//@Configuration
// Spring IoC 컨테이너가 자동 생성할 클래스를 찾을 수 있도록 패키지를 지정한다.
@ComponentScan("bitcamp.myapp")
// JDBC 설정 정보를 담고 있는 .properties 파일을 로딩한다.
@PropertySource("classpath:/bitcamp/myapp/config/jdbc.properties")
// Mybatis-Spring 라이브러리에 있는 클래스를 사용하여 DAO 인터페이스의 구현체를 자동 생성하기
@MapperScan("bitcamp.myapp.dao")
// @Transactional 애노테이션으로 트랜잭션을 제어하려면 다음 애노테이션을 이용하여 설정해야 한다.
@EnableTransactionManagement
public class AppConfig {
// 시스템 property 값 가져오기
// @Autowired Environment env;
// DB 커넥셕풀 객체 준비
// @Bean
// public DataSource dataSource() {
// DriverManagerDataSource ds = new DriverManagerDataSource();
// ds.setDriverClassName(env.getProperty("jdbc.driver"));
// ds.setUrl(env.getProperty("jdbc.url"));
// ds.setUsername(env.getProperty("jdbc.username"));
// ds.setPassword(env.getProperty("jdbc.password"));
// return ds;
// }
// .properties 파일에 있는 값을 낱개로 주입받기
// @Value("${jdbc.driver}") String jdbcDriver;
// @Value("${jdbc.url}") String url;
// @Value("${jdbc.username}") String username;
// @Value("${jdbc.password}") String password;
//
// @Bean
// public DataSource dataSource() {
// DriverManagerDataSource ds = new DriverManagerDataSource();
// ds.setDriverClassName(jdbcDriver);
// ds.setUrl(url);
// ds.setUsername(username);
// ds.setPassword(password);
// return ds;
// }
@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();
}
// Servlet3.0의 멀티파트 요청 데이터를 처리하려면
// 1) 서블릿에 멀티파트 파일 처리에 관한 설정 정보를 담은 MultipartConfigElement를 등록해야 한다.
// 2) 스프링 WebMVC에는 Servlet3.0 API를 이용해 멀티파트 데이터를 처리한 후에
// 페이지 컨트롤러에게 그 값들을 전달해주는 StandardServletMultipartResolver를 등록해야 한다.
// 주의!
// 이때 MultipartResolver 구현체 bean 이름은 "multipartResolver" 여야 한다.
@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
// Apache commons-fileupload 라이브러리로 멀티파트 요청 데이터를 처리하려면
// 1) 프로젝트에 commons-fileupload 라이브러리를 별도로 추가해야 한다.
// 2) 스프링 WebMVC에는 이 라이브러리를 이용해 멀티파트 데이터를 처리한 후에
// 페이지 컨트롤러에게 그 값들을 전달해주는 CommonsMultipartResolver를 등록해야 한다.
// 주의!
// 이때 MultipartResolver 구현체 bean 이름은 "multipartResolver" 여야 한다.
// @Bean
// public MultipartResolver multipartResolver() {
// return new CommonsMultipartResolver();
// }
// 뷰컴포넌트(예: JSP)의 경로를 다루는 객체 준비
@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;
}
}
AbstractAnnotationConfigDispatcherServletInitializer 만 설명한다.
@WebListener 가 하던 역할을 메서드를 나눠서 한다.
CharacterEncodingFilter 를 여기에서 수행한다.
listener 패키지 삭제, CharacterEncodingfilter.java 삭제한다.
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 MyWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// DispatcherServlet 의 서블릿이름을 설정한다.
@Override
protected String getServletName() {
System.out.println("DispatcherServlet: 서블릿 이름 준비");
return "app";
}
@Override
protected Class<?>[] getServletConfigClasses() {
System.out.println("DispatcherServlet: IoC 컨테이너 설정 클래스 준비");
return new Class<?> [] {AppConfig.class};
}
@Override
protected Class<?>[] getRootConfigClasses() {
System.out.println("ServletContext: IoC 컨테이너 설정 클래스 준비");
return null;
}
@Override
protected String[] getServletMappings() {
System.out.println("DispatcherServlet: URL 설정");
return new String[] {"/app/*"};
}
@Override
protected void customizeRegistration(Dynamic registration) {
System.out.println("DispatcherServlet: 멀티파트 설정");
registration.setMultipartConfig(new MultipartConfigElement(
System.getProperty("java.io.tmpdir"), // 클라이언트가 보낸 파일을 임시 보관할 폴더
1024 * 1024 * 20, // 한 파일의 최대 크기
1024 * 1024 * 20 * 10, // 한 요청 당 최대 총 파일 크기
1024 * 1024 * 1 // 클라이언트가 보낸 파일을 메모리에 임시 보관하는 최대 크기.
// 최대 크기를 초과하면 파일에 내보낸다.
));
}
// DispatcherServlet 실행 전후에 작업을 수행할 필터 설정
@Override
protected Filter[] getServletFilters() {
return new Filter[] {new CharacterEncodingFilter("UTF-8")};
}
}
*Controller 들의 return 주소를 수정한다. 맨 앞 "/" 와 맨 뒤 ".jsp" 를 제거한다. 요청오면 알아서 붙여준다.
package bitcamp.myapp.controller;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.Part;
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 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 {
// ServletContext 는 요청 핸들러의 파라미터로 주입 받을 수 없다.
// 객체의 필드로만 주입 받을 수 있다.
@Autowired private ServletContext servletContext;
@Autowired private BoardService boardService;
@GetMapping("form")
public String form() {
return "board/form";
}
@PostMapping("insert")
public String insert(
Board board,
// String title,
// String content,
Part[] files,
Model model, // ServletRequest 보관소에 저장할 값을 담는 임시 저장소
// 이 객체에 값을 담아 두면 프론트 컨트롤러(DispatcherServlet)가
// ServletRequest 보관소로 옮겨 담을 것이다.
HttpSession session) {
try {
Member loginUser = (Member) session.getAttribute("loginUser");
// Board board = new Board();
// board.setTitle(title);
// board.setContent(content);
Member writer = new Member();
writer.setNo(loginUser.getNo());
board.setWriter(writer);
List<BoardFile> boardFiles = new ArrayList<>();
for (Part part : files) {
if (part.getSize() == 0) {
continue;
}
String filename = UUID.randomUUID().toString();
part.write(servletContext.getRealPath("/board/upload/" + filename));
BoardFile boardFile = new BoardFile();
boardFile.setOriginalFilename(part.getSubmittedFileName());
boardFile.setFilepath(filename);
boardFile.setMimeType(part.getContentType());
boardFiles.add(boardFile);
}
board.setAttachedFiles(boardFiles);
boardService.add(board);
} catch (Exception e) {
e.printStackTrace();
model.addAttribute("error", "data");
}
return "board/insert";
}
@GetMapping("list")
public String list(String keyword, Model model) {
model.addAttribute("boards", boardService.list(keyword));
return "/board/list";
}
@GetMapping("view")
public String view(int no, Model model) {
model.addAttribute("board", boardService.get(no));
return"board/view";
}
@PostMapping("update")
public String update(
Board board,
// int no,
// String title,
// String content,
Part[] files,
Model model,
HttpSession session) {
try {
Member loginUser = (Member) session.getAttribute("loginUser");
// Board board = new Board();
// board.setNo(no);
// board.setTitle(title);
// board.setContent(content);
Board old = boardService.get(board.getNo());
if (old.getWriter().getNo() != loginUser.getNo()) {
return "redirect:../auth/fail";
}
List<BoardFile> boardFiles = new ArrayList<>();
for (Part part : files) {
if (part.getSize() == 0) {
continue;
}
String filename = UUID.randomUUID().toString();
part.write(servletContext.getRealPath("/board/upload/" + filename));
BoardFile boardFile = new BoardFile();
boardFile.setOriginalFilename(part.getSubmittedFileName());
boardFile.setFilepath(filename);
boardFile.setMimeType(part.getContentType());
boardFile.setBoardNo(board.getNo());
boardFiles.add(boardFile);
}
board.setAttachedFiles(boardFiles);
boardService.update(board);
} 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");
}
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;
}
}
}
조언
*서버 렌더링은 회사에서 많이 하니까 백엔드, 프론트엔드 분리하는 방식으로 프로젝트 하는게 낫다.
과제
학습
- eomcs-java\eomcs-spring-webmvc
'네이버클라우드 AIaaS 개발자 양성과정 1기 > Spring Framework, Spring Boot' 카테고리의 다른 글
[비트캠프] 83일차(18주차1일) - Spring Framework(Thymeleaf), myapp-62 (0) | 2023.03.06 |
---|---|
[비트캠프] 82일차(17주차4일) - Spring Framework(Spring WebMVC 아키텍처, Tiles), myapp-60-3, 61, 프로젝트 절차(전체 과정) (0) | 2023.03.03 |
[Java] 예제 소스 정리 - Spring WebMVC (0) | 2023.03.03 |
[Java] 예제 소스 정리 - Spring IoC (0) | 2023.03.01 |
[비트캠프] 80일차(17주차2일) - Spring Framework(Spring IoC 컨테이너) (0) | 2023.02.28 |