Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

개발자입니다

[비트캠프] 81일차(17주차3일) - Spring Framework: myapp-60-1, 2 본문

네이버클라우드 AIaaS 개발자 양성과정 1기/Spring Framework, Spring Boot

[비트캠프] 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