"스프링 파일 업로드 MultipartFile #1/2" 에선,
로컬 파일을 서버에 업로드 할 때..
로컬 파일 속성을 그대로 업로드하므로,
로컬에서의 작성일 등등의 정보가 그대로 업로드.
"스프링 파일 업로드 MultipartFile #2/2" 에선,
서버에 업로드 할 때..
경로와 파일명을 변경하면서 저장하므로,
로컬에서의 작성일 등등의 정보가 변경됨.
덧붙여서, 이미지 게시판 (테스트를 위한 간소화 버전) 관리 내용 추가.
여러 파일을 한 번에 업로드 하는 것은,
"스프링 파일 업로드 MultipartFile #1/2" 참조.
Ajax 를 이용해서 파일들을 미리 업로드 하고,
본문을 나중에 올려서 통합하는 방법도 있음.
* build.gradle
plugins { 
  id 'org.springframework.boot' version '2.7.5' 
  id 'io.spring.dependency-management' version '1.0.15.RELEASE' 
  id 'java' 
} 
group = '도메인' 
version = '0.0.1-SNAPSHOT' 
sourceCompatibility = '1.8' 
configurations { 
  compileOnly { 
    extendsFrom annotationProcessor 
  } 
} 
repositories { 
  mavenCentral() 
} 
dependencies { 
  implementation 'org.springframework.boot:spring-boot-starter-jdbc' 
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 
  implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2' 
  implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' 
  implementation 'org.springframework.boot:spring-boot-starter-validation' 
  implementation 'org.springframework.boot:spring-boot-starter-web' 
  implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect' 
  compileOnly 'org.projectlombok:lombok' 
  developmentOnly 'org.springframework.boot:spring-boot-devtools' 
  runtimeOnly 'com.oracle.database.jdbc:ojdbc8' 
  annotationProcessor 'org.projectlombok:lombok' 
  testImplementation 'org.springframework.boot:spring-boot-starter-test' 
} 
tasks.named('test') { 
  useJUnitPlatform() 
} 
* /src/main/resources/application.yml
server.port : 8086 
spring : 
  datasource : 
    driver-class-name : oracle.jdbc.driver.OracleDriver 
    url : jdbc:oracle:thin:@10.1.3.21:1521/skyin 
    username : csuser 
    password : ria72 
  jpa : 
    generate-ddl : 'true' # true 데이터베이스 고유 기능 사용 
    database : oracle 
    database-platform : org.hibernate.dialect.Oracle10gDialect 
    hibernate : 
      ddl-auto : update  # update 변경 부분만 반영, create 기존 테이블 삭제후 재생성, none 사용하지 않음, fk 와 관련 예민 
    show-sql : 'true'   # true 로그에 쿼리 인쇄 
  mybatis : 
    mapper-locations : classpath:mappers/**/*.xml 
    config-location : classpath:mybatis-config.xml 
     
logging : 
  level : 
    org :  
      hibernate : info 
       
servlet : 
  multipart : 
    max-request-size : 10MB 
    max-file-size : 10MB 
     
upload : 
  path : C:/sts/workspace/도메인/src/main/resources/upload 
* /src/main/java/도메인/domain/FileBoard.java
import javax.persistence.*; 
import org.springframework.web.multipart.*; 
import lombok.*; 
@Entity 
@Getter @Setter @ToString 
@Table(name="web_test_file") 
public class FileBoard{ 
  @Id 
  @SequenceGenerator(name="FILE_BOARD_SEQUENCE_GEN", 
      sequenceName="seq_web_test_file", initialValue=1, allocationSize=1) 
  @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="FILE_BOARD_SEQUENCE_GEN") 
  private Long fileId;              // PK 
   
  @Column(length=50)                // javax.persistence.* 
  private String subject;           // 제목 
   
  @Lob                              // javax.persistence.* 
  private String note;              // 설명 
   
  @Transient                        // javax.persistence.*, DB 연동 X 
  private MultipartFile file;       // org.springframework.web.multipart.* 
   
  private String    filePath;       // web 접근시 파일 경로. /yyyy/mm/dd 
  private String    fileName;       // 업로드 후 서버에서의 파일명 (확장자 없음) 
  private String    orgFileName;    // 업로드 전 로컬에서의 파일명 (확장자 있음) 
  private Long      fileSize;       // 단위 bytes 
  private String    contentType;    // 미디어 구분 
}
* /src/main/java/도메인/contoller/FileBoardController.java
import java.io.*; 
import java.text.*; 
import java.util.*; 
import lombok.*; 
import lombok.extern.slf4j.*; 
import 도메인.domain.*; 
import 도메인.service.*; 
import org.springframework.beans.factory.annotation.*; 
import org.springframework.http.*; 
import org.springframework.stereotype.*; 
import org.springframework.ui.*; 
import org.springframework.util.*; 
import org.springframework.web.bind.annotation.*; 
import org.springframework.web.multipart.*; 
@Controller @Slf4j 
@RequiredArgsConstructor 
@RequestMapping("/fileBoard") 
public class FileBoardController{ 
  private final FileBoardService service; 
   
  @Value("${upload.path}") // application.yml 파일 참조. org.springframework.beans.factory.annotation.* 
  private String uploadPath; 
   
   
  @GetMapping("/list") 
  public void list(Model model) throws Exception { 
    List<FileBoard> fbList = service.list(); 
    model.addAttribute("fileBoardList", fbList); 
  } 
  @GetMapping("/read") 
  public String read(Long fileId, Model model) throws Exception { // FileBoard fileBoard 보다 Long fileId 가 가벼워서 사용
    FileBoard fileBoard = service.read(fileId); 
    model.addAttribute(fileBoard); 
    return "fileBoard/read"; 
  } 
  @GetMapping("/register") 
  public String registerForm(Model model) throws Exception { 
    model.addAttribute(new FileBoard()); 
    return "fileBoard/register"; 
  } 
  @PostMapping("/register") 
  public String register(FileBoard fileBoard, Model model) throws Exception { 
    this.uploadFile(fileBoard); // 서버에 파일 업로드 
    service.regist(fileBoard);  // DB 신규 등록 
    model.addAttribute("msg","등록이 완료되었습니다."); // service.regist() 의 파라미터인 fileBoard.fileId 가 세팅되므로
                                                                                       // fileBoard.fileId 값이 있으면 성공
    return "fileBoard/read"; // read view 호출 (fileBoard.fileId 기준으로 조회) 
  } 
   
  @GetMapping("/modify") 
  public String modifyForm(Long fileId, Model model) throws Exception { 
    FileBoard fileBoard = service.read(fileId); 
    model.addAttribute(fileBoard); 
    return "fileBoard/modify"; 
  } 
  @PostMapping("/modify") 
  public String modify(FileBoard fileBoard, Model model) throws Exception { 
    this.uploadFile(fileBoard); // 서버에 파일 업로드 
    service.modify(fileBoard);  // DB 수정, 수정할 때마다 백업하는 기능은 DB 트리거 사용하는 것이 간편
    model.addAttribute("msg","수정이 완료되었습니다."); // 예외 상황이 아니면 성공
    return "fileBoard/read"; // read view 호출 (fileBoard.fileId 기준으로 조회)
  } 
  @PostMapping("/remove") 
  public String remove(FileBoard fileBoard, Model model) throws Exception { 
    service.remove(fileBoard.getFileId()); 
    model.addAttribute("msg","삭제가 완료되었습니다."); // fileBoard.fileid 가 null 이면 삭제 성공.
    return "redirect:/fileBoard/list"; // 삭제한 다음 list view 를 호출하지 않고(이곳엔 list view 에 필요한 데이타가 없음) url 호출
  } 
   
   
  private void uploadFile(FileBoard fileBoard) throws Exception { // 파일 업로드
    MultipartFile mf  = fileBoard.getFile();                // org.springframework.web.multipart.* 
    if(mf!=null && mf.getSize()>0) {                        // 수정시 파일이 변경되지 않을 수 있음.
// 이전 파일이 변경되는 경우라면 이전 파일을 삭제하기 위한 로직이 필요.
                                                                           // 아니면, 이전 내용도 백업해서 계속 참조 가능하도록 할 수는 있음 
      FileBoard     fb    = this.saveFile(mf);              // 서버에 업로드 된 스트림을 파일로 저장. 서버 디스크 상의 파일 형태.
// 이 부분을 별도 기술하지 않으면 관련 프로세스에서 원본 파일명으로 서버에 업로드 작업을 완료하여
                                                                           // 서버의 하드디스크에 저장한다 
      fileBoard.setFilePath(fb.getFilePath());              // DB 저장을 위한 세팅. 하나의 디렉토리에 너무 많은 파일이 생성되지 않도록
// /yyyy/mm/dd 형태로 배분하여 I/O 부하 감소
                                                                             // 하나의 디렉토리에 파일 갯수가 많아지면 access time 이 길어져서 퍼포먼스 떨어짐
      fileBoard.setFileName(fb.getFileName());              // DB 저장을 위한 세팅. 서버에서의 파일명 (업로드 후) 
      fileBoard.setOrgFileName(mf.getOriginalFilename());   // DB 저장을 위한 세팅. 로컬에서의 파일명 (업로드 전) 
      fileBoard.setFileSize(mf.getSize());                  // DB 저장을 위한 세팅. 서버에서의 파일 사이즈 (로컬과 약간 상이 가능) 
      fileBoard.setContentType(mf.getContentType());        // DB 저장을 위한 세팅. 파일 유형 
    } 
  } 
   
  private FileBoard saveFile(MultipartFile mFile) throws Exception { 
    SimpleDateFormat  sdf       = new SimpleDateFormat("/yyyy/MM/dd");  // uploadPath 하위에 생성될 경로, java.text.* 
    String            filePath  = sdf.format(new Date());               // URL 접속 가능한 웹 경로, java.util.* 
    String            realPath  = uploadPath+filePath;                  // 파일이 생성될 절대경로 
    File              directory = new File(realPath);                   // 디렉토리 체크용 
    if(!directory.exists()) directory.mkdirs();                         // 서버 디렉토리가 없으면 생성 
    String            fileName  = UUID.randomUUID().toString();         // 서버에서의 파일명. 확장자 필요시 원본파일명의 확장자 복사 
    File              out       = new File(realPath, fileName);         // 파일 저장용. java.io.* 
    FileCopyUtils.copy(mFile.getBytes(), out);                          // Memory 스트림에서 File 로 복사. org.springframework.util.* 
     
    FileBoard         fb        = new FileBoard(); 
    fb.setFilePath(filePath);                                           // 서버에서의 경로명. /yyyy/mm/dd 형태 
    fb.setFileName(fileName);                                           // 서버에서의 파일명. 10a99a49-2ee6-4c32-b4c7-09f36c166f1e 형태 
     
    return fb; 
  } 
   
  @ResponseBody 
  @GetMapping("/file") 
  public ResponseEntity<byte[]> getFile(Long fileId) throws Exception{ // org.springframework.http.* 
    ResponseEntity<byte[]> entity = null; 
    try { 
       
      FileBoard   fb          = service.read(fileId);             // DB 정보 추출. getFile()을호출하는쪽에서 아래 내용들을 전달해 줄 수 있다면,
                                                                             // DB 를 이렇게 호출해 줄 필요가 없다
      String      filePath    = fb.getFilePath(); 
      String      fileName    = fb.getFileName(); 
      String      orgFileName = fb.getOrgFileName(); 
      String      contentType = fb.getContentType(); // contentType 체크해서 이미지인지 일반 파일인지 체크가 필요할 수도..
       
      MediaType   mt          = MediaType.parseMediaType(contentType);    // MutipartFile.contentType 에서 따는 방법 
      HttpHeaders headers     = new HttpHeaders();                                          // org.springframework.http.* 
      File        file        = new File(uploadPath +filePath+ "/" +fileName);              // java.io.*, web 서비스 경로가 아닌 곳에 파일이 있는 경우 
      if(mt!=null) headers.setContentType(mt); 
      entity = new ResponseEntity<byte[]>(FileCopyUtils.copyToByteArray(file),              // org.springframework.http.ResponseEntity, org.springframework.util.FileCopyUtils 
        headers, 
        HttpStatus.CREATED);                                                                // org.springframework.http.* 
       
    }catch(Exception e) { 
      e.printStackTrace(); 
      entity = new ResponseEntity<byte[]>(HttpStatus.BAD_REQUEST); 
    }finally { 
    } 
     
    return entity; 
  } 
   
}
* /src/main/java/도메인/service/FileBoardService.java
import java.util.List; 
import 도메인.domain.*; 
public interface FileBoardService{ 
   
  public List list() throws Exception;
  public FileBoard read(Long fileId) throws Exception;
  public void regist(FileBoard fileBoard) throws Exception; 
  public void modify(FileBoard fileBoard) throws Exception; 
  public void remove(Long fileId) throws Exception; 
} 
* /src/main/java/도메인/service/FileBoardServiceImpl.java
import java.util.*; 
import lombok.*; 
import lombok.extern.slf4j.*; 
import 도메인.domain.*; 
import 도메인.repository.*; 
import org.springframework.data.domain.Sort.*; 
import org.springframework.data.domain.*; 
import org.springframework.stereotype.*; 
import org.springframework.transaction.annotation.*; 
@Service @Slf4j 
@RequiredArgsConstructor 
public class FileBoardServiceImpl implements FileBoardService{ 
  private final FileBoardRepository repository; 
    
  @Override
  @Transactional(readOnly=true)
  public List<FileBoard> list() throws Exception {
    return repository.findAll(Sort.by(Direction.DESC,"fileId")); // org.springframework.data.domain.*
                                                                                                                               // org.springframework.data.domain.Sort.*
  }
  @Override
  @Transactional(readOnly=true)
  public FileBoard read(Long fileId) throws Exception {
    return repository.getOne(fileId);
  }
  
  @Override 
  @Transactional // 다른 DB 와 트랜잭션이 걸릴 경우를 대비해서 세팅
  public void regist(FileBoard fileBoard) throws Exception { 
    repository.save(fileBoard); 
  } 
   
  @Override 
  @Transactional 
  public void modify(FileBoard fileBoard) throws Exception { 
    FileBoard fb = read(fileBoard.getFileId()); // 기존 DB 정보 추출
    fb.setSubject(fileBoard.getSubject()); 
    fb.setNote(fileBoard.getNote()); 
    String  fp  = fileBoard.getFilePath(); 
    if(fp!=null && fp.length()>0){ // 수정화면에서 파일을 변경하는 경우 이를 적용
      fb.setFilePath(fp); 
      fb.setFileName(fileBoard.getFileName()); 
      fb.setOrgFileName(fileBoard.getOrgFileName()); 
      fb.setFileSize(fileBoard.getFileSize()); 
      fb.setContentType(fileBoard.getContentType()); 
    } 
    //repository.save(fileBoard); // 주석처리 해도 DB 에 저장됨
  } 
   
  @Override 
  @Transactional 
  public void remove(Long fileId) throws Exception { 
    repository.deleteById(fileId); 
  }  
}
* /src/main/java/도메인/repository/FileBoardRepository.java
import 도메인.domain.*; 
import org.springframework.data.jpa.repository.*; 
public interface FileBoardRepository extends JpaRepository<FileBoard,Long>{ // org.springframework.data.jpa.repository.* 
  // extends 내용으로 충분. 꼭 필요한 경우가 아니면 JPA 가 쿼리를 자동 생성하는 것이 좋음
} 
/src/main/resources/templates/fileBoard/list.html
<html xmlns:th="http://www.thymeleaf.org" 
  xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" 
  layout:decorate="~{/layouts/default.html}"> 
<head> 
<title>[JPA] File Board, List</title> 
<script src="/js/board/fileBoard.js"></script> 
</head> 
<body> 
<div layout:fragment="content"><!--/*  default.html 의 <div layout:fragment="content"></div> 대체 */--> 
<form id="pr"> 
<input type="hidden" name="fileId" id="fileId"> 
<h2>List</h2> 
<p><a th:href="@{register}">등록</a></p> 
<table class="df" style="width:1060px"> 
  <col width="80"> 
  <col width="200"> 
  <col width="320"> 
  <col width="200"> 
  <col width="120"> 
  <col width="140"> 
  <tr> 
    <th rowspan="2">Id</th> 
    <th rowspan="2">제목</th> 
    <th rowspan="2">설명</th> 
    <th colspan="3">파일 정보</th> 
  </tr> 
  <tr> 
    <th>이름</th> 
    <th>사이즈 bytes</th> 
    <th>종류</th> 
  </tr> 
   
  <tr th:if="${#lists.isEmpty(fileBoardList)}"> 
    <td colspan="6">List is empty</td> 
  </tr> 
   
  <tr th:each="board : ${fileBoardList}"> 
    <td th:text="${board.fileId}"></td> 
    <td><a th:href="|javascript:goReadFileBoard(${board.fileId})|" th:text="${board.subject}"></a></td> 
    <td th:text="${board.note}"></td> 
    <td th:text="${board.orgFileName}"></td><!--/* 서버에서의 파일명이 다르므로 업로드시 로컬 파일명 */--> 
    <td th:text="${#numbers.formatInteger(board.fileSize, 3, 'COMMA')}"></td> 
    <td th:text="${board.contentType}"></td> 
  </tr> 
   
</table> 
</form> 
<script th:inline="javascript"> 
  var frmObj; 
  $(document).ready(function(){ 
    frmObj = $('#pr');// 여기서 사용된 form 태그 
  }); 
   
  $(window).load(function(){// 목록 화면이 표시되고 alert 되기를 바랐건만, 흰 바탕에 alert 된 다음 view 가 표시됨 
    let msg = [[${msg}]]; if(msg) alert(msg); 
  }); 
</script> 
</div> 
</body> 
</html> 
* /src/main/resources/templates/fileBoard/read.html
...
<form id="fileBoard" th:object="${fileBoard}"> 
<input type="hidden" th:field="*{fileId}"> 
<h2>Read</h2> 
<table class="df"> 
  <col width="80"> 
  <col width="320"> 
  <tr> 
    <th class="r">fileId</th> 
    <td class="l" th:text="*{fileId}"></td> 
  </tr> 
  <tr> 
    <th class="r">제목</th> 
    <td class="l" th:text="*{subject}"></td> 
  </tr> 
  <tr> 
    <th class="r">설명</th> 
    <td class="l" th:text="*{note}"></td> 
  </tr> 
  <tr> 
    <th class="r">파일</th> 
    <td class="l"> 
      <p>- 원본 파일명 <span th:text="*{orgFileName}"></span></p> 
      <p>- 파일사이즈 <span th:text="*{fileSize}"></span></p> 
      <p>- 파일종류 <span th:text="*{contentType}"></span></p> 
      <p><img th:src="@{|file?fileId=*{fileId}|}" width="200" height="300"></p><!--/* 이미지 */-->
    </td> 
  </tr> 
</table> 
<div class="btn"> 
  <a id="btnModify">수정</a> 
  <a id="btnRemove">삭제</a> 
  <a id="btnList">목록</a> 
</div> 
</form> 
<script> 
  var frmObj; 
  $(document).ready(function(){ 
    frmObj = $('#fileBoard'); 
    $('#btnModify').on('click',function(){ 
      act('get','modify');  // 수정 양식으로 이동 
    }); 
    $('#btnRemove').on('click',function(){ 
      act('post','remove'); // remove 는 post 
    }); 
    $('#btnList').on('click',function(){ 
      act('get','list') // list 는 get. 페이지나 목록 검색 조건 전달 
    }); 
  }); 
</script> 
...
* /src/main/resources/teplates/fileBoard/register.html
...
<form id="fileBoard" th:object="${fileBoard}" enctype="multipart/form-data"> 
<h2>Register</h2> 
<table> 
  <tr> 
    <th>제목</th> 
    <td> 
      <input type="text" th:field="*{subject}"> 
      <p th:if="${#fields.hasErrors('subject')}" th:errors="*{subject}"></p> 
    </td> 
  </tr> 
  <tr> 
    <th>설명</th> 
    <td> 
      <textarea th:field="*{note}"></textarea> 
      <p th:if="${#fields.hasErrors('note')}" th:errors="*{note}"></p> 
    </td> 
  </tr> 
  <tr> 
    <th>파일</th> 
    <td> 
      <input type="file" th:field="*{file}"> 
    </td> 
  </tr> 
</table> 
<div class="btn"> 
  <a id="btnRegister">저장</a> 
  <a id="btnList">목록</a> 
</div> 
</form> 
<script> 
  var frmObj; 
  $(document).ready(function(){ 
    frmObj = $('#fileBoard'); 
    $('#btnRegister').on('click',function(){ 
      if(!validForm2()) return; 
      act('post','register'); 
    }); 
    $('#btnList').on('click',function(){ 
      act('get','list') // list 는 get. 페이지나 목록 검색 조건 전달 
    }); 
  }); 
</script>
...
* /src/main/resources/templates/fileBoard/modify.html
...
<form id="fileBoard" th:object="${fileBoard}" enctype="multipart/form-data"> 
<input type="hidden" th:field="*{fileId}"> 
<h2>Modify</h2> 
<table class="df"> 
  <col width="80"> 
  <col width="320"> 
  <tr> 
    <th class="r">fileId</th> 
    <td class="l" th:text="*{fileId}"></td> 
  </tr> 
  <tr> 
    <th class="r">제목</th> 
    <td class="l"> 
      <input type="text" th:field="*{subject}"> 
    </td> 
  </tr> 
  <tr> 
    <th class="r">설명</th> 
    <td class="l"> 
      <textarea th:field="*{note}"></textarea> 
    </td> 
  </tr> 
  <tr> 
    <th class="r">파일</th> 
    <td class="l"> 
      <p>- 원본 파일명 : <span th:text="*{orgFileName}"></span></p><!--/* 기존 정보. 수정하고 나면 갱신 */-->
      <p>- 파일사이즈 : <span th:text="*{fileSize}"></span></p> 
      <p>- 파일종류 : <span th:text="*{contentType}"></span></p> 
      <p><img th:src="@{|file?fileId=*{fileId}|}" width="200" height="300"></p> 
      <input type="file" th:field="*{file}"><!--/* 파일 변경하려면 기술 */-->
    </td> 
  </tr> 
</table> 
<div class="btn"> 
  <a id="btnModify">저장</a> 
  <a id="btnList">목록</a> 
</div> 
</form> 
<script> 
  var frmObj; 
  $(document).ready(function(){ 
    $('#btnModify').on('click',function(){ 
      frmObj = $('#fileBoard'); 
      if(!validForm2()) return; 
      act('post','modify') 
    }); 
    $('#btnList').on('click',function(){ 
      act('get','list'); 
    }); 
  }); 
</script>
...
* /src/main/resources/templates/layouts/default.html
<!doctype html> 
<html xmlns:th="http://www.thymeleaf.org" 
  xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> 
<head n="0"> 
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" /> 
<link rel="stylesheet" type="text/css" href="/css/default.css"/><!--/* 전체 공통 */--> 
<script src="/js/jquery-1.10.2.min.js"></script><!--/* 전체 공통 */--> 
<script src="/js/default.js"></script><!--/* 전체 공통 */--> 
</head> 
<body> 
  <div class="header">페이지 헤더 | <a href="/" style="font-size:8pt">메인 화면 바로가기</a></div><!--/* 외부 파일로 만들 수도 있고, 여기에 기술할 수도.. */--> 
  <div layout:fragment="content" class="content"></div> 
  <div class="footer">페이지 푸터</div><!--/* 외부 파일로 만들 수도 있고, 여기에 기술할 수도.. */--> 
</body> 
</html>
* /src/main/resources/static/css/default.css
* /src/main/resources/static/js/board/fileBoard.js
function goReadFileBoard(fi){//목록에서 상세화면 오픈 
  goRead('fileId', fi); 
} 
function validForm2(){//등록/수정시 입력내용 체크 
  if($('#subject').val()==''){ alert('제목을 입력해 주세요. '); return false; } 
  if($('#note').val()==''){ alert('설명을 입력해 주세요. '); return false; } 
  //if($('#file').val()==''){ alert('파일을 선택해 주세요. '); return false; } 
  return true; 
} 
* /src/main/resources/static/js/default.js
function act(method,action){ 
frmObj // 화면용 .html 에서 정의 
.attr('method',method) 
.attr('action',action) 
.submit(); 
} 
function goRead(id, n){ $('#'+id).val(n); act('get','read'); } 
function goList(pn){ $('#page').val(pn); act('get','list'); } 
'Server Oriented > Spring' 카테고리의 다른 글
| @SpringBootTest 에서 HttpServletRequest 나 HttpSession 등을 mock 하는 방법 (0) | 2024.04.29 | 
|---|---|
| 스프링 Transaction 트랜잭션 (0) | 2023.02.10 | 
| 스프링 파일 업로드 MultipartFile #1/2 (0) | 2023.02.03 | 
| JUnit5 JPA DB 테이블 1개 목록/상세/등록/수정/삭제 (0) | 2022.12.05 | 
| 스프링 JPA, 평범한 게시판 구현 DB 테이블 1개 (2) | 2022.12.03 |