SsackTeun/naverworkplace-absence-monthly-report: naverworkplace 부재 파싱 -> 월별 휴가 사용시간 산출 (github.com)
컨트롤러 클래스는 적당한가?
클래스 이름은 적당한가?
- Think : 아닌 것 같다.
- Why? : 담고 있는 의미가 없음. 클래스는 기본적으로 네이밍에서 역할을 파악할 수 있어야함.
- How : 도메인 이름 + Controller
- Why? : 카테고리(도메인)으로 묶을 수 있으며, 특정 Request 를 추가하거나, 문제가 발생할 때 빠르게 찾을 수 있다.
GPT 에게 물어보는 "도메인 컨트롤러 나누는 방법"
-
- 기능 또는 업무 영역: 가장 일반적인 방법 중 하나는 애플리케이션의 다양한 기능 또는 업무 영역에 따라 도메인 컨트롤러를 나누는 것입니다. 예를 들어, 전자 상거래 애플리케이션의 경우 "상품(Product)Controller," "주문(Order)Controller," "결제(Payment)Controller"와 같이 기능별로 도메인 컨트롤러를 나눌 수 있습니다.
- 데이터 모델: 애플리케이션의 데이터 모델에 따라 도메인 컨트롤러를 나눌 수 있습니다. 데이터 모델이 서로 다른 엔터티 또는 도메인 객체를 포함하고 있을 때, 각 엔터티에 대한 도메인 컨트롤러를 만들어 관리합니다.
- 사용자 역할 또는 권한: 애플리케이션의 사용자 역할 또는 권한에 따라 도메인 컨트롤러를 분리할 수 있습니다. 예를 들어, 관리자와 일반 사용자 각각에 대한 별도의 도메인 컨트롤러를 생성하여 사용자 권한을 관리할 수 있습니다.
- RESTful API 디자인: RESTful API를 설계할 때는 리소스(자원)별로 도메인 컨트롤러를 나누는 것이 일반적입니다. 각 리소스에 대한 CRUD(Create, Read, Update, Delete) 작업을 처리하는 컨트롤러를 별도로 생성합니다.
- 모듈화와 재사용성: 도메인 컨트롤러를 모듈화하고 재사용 가능한 코드로 설계하려면 비슷한 기능을 하는 컨트롤러를 하나의 모듈로 묶을 수 있습니다. 이렇게 하면 유사한 동작을 하는 부분을 재사용할 수 있습니다.
- 애플리케이션의 크기와 복잡성: 애플리케이션이 크고 복잡한 경우에는 기능을 나누고 모듈화하여 관리하기 쉽도록 도메인 컨트롤러를 분리합니다. 작은 애플리케이션의 경우에는 단순한 구조를 유지할 수 있습니다.
- 세분화된 관심사 분리(Separation of Concerns): 도메인 컨트롤러를 나누는 또 다른 기준은 각 컨트롤러가 다른 관심사를 처리하도록 하는 것입니다. 예를 들어, 사용자 관리와 로깅은 서로 다른 관심사이므로 별도의 컨트롤러로 분리할 수 있습니다.
- 기능이 많이 없기 때문에 잘게 나누기에 애매
- 1 ~ 7번까지 다 살펴보았을 때, 그나마 이게 제일 적합한 것 같음.
예시를 물어보니, 아래와 같음
- 권한을 나누지 않기 때문에 x
- RESTful API 라고 하기엔, read 위주
- 재사용하기에는 기능 구현은 대부분 유틸클래스에 구현하기 때문에 제외
- 애플리케이션이 크거나 복잡하지않기 때문에 x
- 세분화된 관심사 분리
Spring AOP를 이용하면 로깅과 같은 공통 관심사를 Aspect로 정의하고, AspectJ 표현식을 사용하여 어느 메서드에서 로깅을 적용할지 결정할 수 있습니다. 이렇게 하면 핵심 비즈니스 로직에서 로깅 코드를 명시적으로 작성하지 않아도 됩니다.- 로깅 코드의 중복 제거: 핵심 로직에서 로깅 코드를 반복해서 작성하지 않아도 됩니다.
- 코드의 분리: 로깅과 같은 관심사를 별도의 모듈(Aspect)로 분리하여 코드를 더 깔끔하게 유지할 수 있습니다.
- 재사용성: 로깅 로직을 한 번 정의하면 여러 곳에서 재사용할 수 있습니다.
- 유지 보수성 향상: 로깅 레벨을 변경하거나 로깅 방식을 수정할 때, 모든 핵심 로직을 수정하지 않고도 Aspect를 수정하여 쉽게 대응할 수 있습니다.
그래서 결론은?
- 추후에 불필요한 메서드를 제거하고, 유틸클래스에 포함시킨다면 실제로 Request 가 일어나는
- "유저 리스트 파일 업/다운로드"
- "부재 일정 파일 업/다운로드"
- "엑셀 파일 생성 요청 (데이터 처리 요청)"
이렇게 2, 2, 1개 로 총 5개만 남을 것으로 생각.
FileUpDownController,ProcessController >ViewController 두개로 나누어 볼 예정.
- 데이터 포탈에 있는 API 도 따로 Controller 를 만들지는 모르겠음
소스코드
package com.example.excelparser.controller;
import com.example.excelparser.dto.MergeDTO;
import com.example.excelparser.dto.RefactorDTO;
import com.example.excelparser.dto.UserListDTO;
import com.example.excelparser.dto.original.OriginDTO;
import com.example.excelparser.service.DataRefactorService;
import com.example.excelparser.util.DataRefactoring;
import com.example.excelparser.util.excel.ExcelCreation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
@Slf4j
@RestController
public class Controller {
/* 파일 저장 위치 root */
private String multipart_location = System.getProperty("user.dir")+"/data";
/* 사용자 리스트 파일 업로드파일 저장 위치 */
private String upload_list_path = multipart_location +"/list";
/* 네이버워크플레이스 부재 엑셀파일 업로드파일 저장 위치 */
private String upload_absence_path =multipart_location +"/absence";
/* 부재 엑셀파일 원본 데이터 가공 관련 */
private DataRefactoring dataRefactor;
private DataRefactorService dataRefactorService;
Controller() throws IOException {
dataRefactor = new DataRefactoring();
dataRefactorService = new DataRefactorService();
}
/* View : absence.html
* 최근 업로드된 파일 시간 표시
*/
@GetMapping("/")
public ModelAndView main(ModelAndView mav) throws IOException {
Path list = Paths.get(upload_list_path + "/list.xlsx");
if(Files.exists(list)){
BasicFileAttributes basicFileAttributes1
= Files.readAttributes(list, BasicFileAttributes.class);
Date time1 = new Date(basicFileAttributes1.lastAccessTime().toMillis());
String current_upload_time1 = new SimpleDateFormat("yyyy년 MM월 dd일 HH시 mm분 ss초").format(time1);
mav.addObject("list_current_upload_time", current_upload_time1);
mav.addObject("list_filename", list.getFileName());
}
Path absence = Paths.get(upload_absence_path + "/absence.xlsx");
if(Files.exists(absence)){
BasicFileAttributes basicFileAttributes2
= Files.readAttributes(absence, BasicFileAttributes.class);
Date time2 = new Date(basicFileAttributes2.lastAccessTime().toMillis());
String current_upload_time2 = new SimpleDateFormat("yyyy년 MM월 dd일 HH시 mm분 ss초").format(time2);
mav.addObject("ab_current_upload_time", current_upload_time2);
mav.addObject("ab_filename", absence.getFileName());
}
mav.setViewName("absence");
return mav;
}
/* 원본 엑셀 데이터에서 추출하여, json 형태로 변환하여 반환 */
@GetMapping("/origin")
public List<OriginDTO> origin() throws IOException {
return DataRefactoring.origin();
}
/* list.xlsx 파일데이터에서 유저정보 json 으로 변환하여 반환 */
@GetMapping("/users")
public List<UserListDTO> users() throws IOException {
return DataRefactoring.getAllUsers();
}
/* list 파일 업로드한 것 다운로드하기 */
@GetMapping("/download/list")
public ResponseEntity<UrlResource> downloadList() throws MalformedURLException {
UrlResource resource = new UrlResource("file:" + upload_list_path + "/list.xlsx");
String contentDisposition = "attachment; filename=\""+ "list.xlsx" + "\"";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(resource);
}
/* 네이버워크플레이스 부재 엑셀 파일 데이터 마지막 업로드 파일 다운로드 */
@GetMapping("/download/absence")
public ResponseEntity<UrlResource> downloadAbsence() throws MalformedURLException {
UrlResource resource = new UrlResource("file:" + upload_absence_path + "/absence.xlsx");
String contentDisposition = "attachment; filename=\""+ "absence.xlsx" + "\"";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(resource);
}
/* 유저목록 파일 업로드 */
@PostMapping("/upload/user/lists")
public RedirectView upload_list(@RequestParam("file")MultipartFile file,
ModelAndView mav) throws IOException, InterruptedException {
File dir = new File(upload_list_path);
if(!dir.exists()) {
dir.mkdirs();
log.info("폴더 생성 성공 {} " + dir);
}else{
System.out.println("폴더가 이미 존재합니다.");
}
Path list = Paths.get(upload_list_path + "/list.xlsx");
if(Files.exists(list)){
Files.delete(list);
}
File savefile = new File(upload_list_path + "/list.xlsx");
file.transferTo(savefile);
return new RedirectView("/");
}
/* 네이버워크플레이스 부재 엑셀 파일 데이터 업로드 */
@PostMapping("/upload/absence")
public RedirectView upload_absence(@RequestParam("file")MultipartFile file,
ModelAndView mav) throws IOException, InterruptedException {
File dir = new File(upload_absence_path + "/absence.xlsx");
if(!dir.exists()) {
dir.mkdirs();
log.info("폴더 생성 성공 {} " + dir);
}else{
System.out.println("폴더가 이미 존재합니다.");
}
Path absence = Paths.get(upload_absence_path + "/absence.xlsx");
if(Files.exists(absence)){
Files.delete(absence);
}
File savefile = new File(upload_absence_path + "/absence.xlsx");
file.transferTo(savefile);
return new RedirectView("/");
}
/* 휴가 기간 값을 list 형태로 변환 */
@GetMapping("/refactor")
public List<RefactorDTO> refactor() throws IOException {
return DataRefactoring.refactor();
}
/* 휴가 기간 값 + 일한 일수 데이터 합치기 */
@GetMapping("/merge")
public List<MergeDTO> merge() throws IOException {
return DataRefactoring.merge();
}
/* 달이 넘어가는 경우에 대한 처리 */
@GetMapping("/dividemonth")
public List<MergeDTO> divideMonth() throws IOException {
return DataRefactoring.divideMonth();
}
/* 선택한 연월에 대해서 엑셀 다운로드 */
@GetMapping("/dateselect/{year}/{month}")
public void dateSelect(
@PathVariable("year") String year,
@PathVariable("month") String month,
HttpServletResponse response
) throws IOException {
new ExcelCreation().createFile(response, DataRefactoring.getEachMonthWithSelectMonth(year,month), year, month);
}
/* 결과 엑셀로 내려 받기 */
@GetMapping("/calculate/isholiday/{years}/{month}")
public void calculate(@PathVariable("years") String years,
@PathVariable("month") String month,
HttpServletResponse response) throws IOException {
new ExcelCreation().createFile(response, MergeDTO.convert(dataRefactorService.isHolidayCalculate(years, month)), years, month);
}
}
수정한 코드 (FileUpDownController.java)
* /files 을 root 로 명명
-> 파일을 업로드, 다운로드 기능 요청을 처리하기 때문.
* 필요없는 메소드 전부 제거
* main 을 호출하는 ViewController 에 FileUpDownController.java 의 상수가 사용되서, public static 으로 변경하였음
* 육안으로 보았을 때, upload download 메소드 구현이 파일명, 위치 빼고는 같은데 하나로 합칠 방법이 있을지는 모르겠음.
package com.example.excelparser.controller;
import com.example.excelparser.dto.MergeDTO;
import com.example.excelparser.dto.UserListDTO;
import com.example.excelparser.service.DataRefactorService;
import com.example.excelparser.util.DataRefactoring;
import com.example.excelparser.util.excel.ExcelCreation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.view.RedirectView;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
@Slf4j
@RestController
public class FileUpDownController {
/* */
private DataRefactorService dataRefactorService;
/* 저장 폴더 루트 위치 */
public final static String MULTIPART_LOCATION = System.getProperty("user.dir")+"/data";
/* 사용자 리스트 파일 업로드파일 저장 위치 */
public final static String UPLOAD_LIST_PATH = MULTIPART_LOCATION +"/list";
public final static String USERLIST_FILENAME = "list.xlsx";
/* 네이버워크플레이스 부재 엑셀파일 업로드파일 저장 위치 */
public final static String UPLOAD_ABSENCE_PATH =MULTIPART_LOCATION +"/absence";
public final static String ABSENCE_FILENAME = "absence.xlsx";
public FileUpDownController(DataRefactorService dataRefactorService) {
this.dataRefactorService = dataRefactorService;
}
/* list.xlsx 파일데이터에서 유저정보 json 으로 변환하여 반환 */
@GetMapping("/files/userlist/users")
public List<UserListDTO> users() throws IOException {
return DataRefactoring.getAllUsers();
}
/* 업로드 유저 리스트 파일 */
@PostMapping("/files/upload/userlist")
public RedirectView uploadUserListExcelFile(@RequestParam("file")MultipartFile userListFile) throws IOException {
/* /root/list 위치에 파일 생성 */
File directory = new File(UPLOAD_LIST_PATH);
/* upload_list_path 디렉토리 생성 */
if(!directory.exists()) {
directory.mkdirs();
log.info("created directory : {} " + directory);
}else{
log.info("already exist");
}
/* upload_list_path 경로에 list.xlsx 파일이 있는지 체크 */
Path list = Paths.get(UPLOAD_LIST_PATH + "/" + USERLIST_FILENAME);
/* list.xlsx 파일이 이미 존재하면, 삭제할 것 */
if(Files.exists(list)){
Files.delete(list);
}
/* MultipartFile 로 업로드하는 파일을 list.xlsx 이름으로 저장할 것 */
File saveFile = new File(UPLOAD_LIST_PATH + "/" + USERLIST_FILENAME);
userListFile.transferTo(saveFile);
return new RedirectView("/");
}
/* 다운로드 유저 리스트 파일 */
@GetMapping("/files/download/userlist")
public ResponseEntity<UrlResource> downloadList() throws MalformedURLException {
UrlResource resource = new UrlResource("file:" + UPLOAD_LIST_PATH + "/" + USERLIST_FILENAME);
String contentDisposition = "attachment; filename=\""+ USERLIST_FILENAME + "\"";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(resource);
}
/* 업로드 네이버워크플레이스 부재일정 파일 */
@PostMapping("/files/upload/absence")
public RedirectView upload_absence(@RequestParam("file")MultipartFile file) throws IOException {
File dir = new File(UPLOAD_ABSENCE_PATH + "/" + ABSENCE_FILENAME);
if(!dir.exists()) {
dir.mkdirs();
log.info("폴더 생성 성공 {} " + dir);
}else{
System.out.println("폴더가 이미 존재합니다.");
}
Path absence = Paths.get(UPLOAD_ABSENCE_PATH + "/" + ABSENCE_FILENAME);
if(Files.exists(absence)){
Files.delete(absence);
}
File saveFile = new File(UPLOAD_ABSENCE_PATH + "/" + ABSENCE_FILENAME);
file.transferTo(saveFile);
return new RedirectView("/");
}
/* 다운로드 네이버워크플레이스 부재일정 파일 */
@GetMapping("/files/download/absence")
public ResponseEntity<UrlResource> downloadAbsence() throws MalformedURLException {
UrlResource resource = new UrlResource("file:" + UPLOAD_ABSENCE_PATH + "/" + ABSENCE_FILENAME);
String contentDisposition = "attachment; filename=\""+ ABSENCE_FILENAME + "\"";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(resource);
}
/* 결과 엑셀로 내려 받기 */
@GetMapping("/files/download/result/{years}/{month}")
public void calculate(@PathVariable("years") String years,
@PathVariable("month") String month,
HttpServletResponse response) throws IOException {
new ExcelCreation().createFile(response, MergeDTO.convert(dataRefactorService.isHolidayCalculate(years, month)), years, month);
}
}
수정한코드 (ViewController.java)
* 솔직히 만들지 말지 고민하였음.
* 이 프로그램의 View 는 한페이지 뿐이고, 분리하기위해서 FileUpDownController 의 멤버변수를 public static 상수처리해야했기 때문.
* 처음에는 FileUpDownController.java 에 있는 users 를 여기로 빼려고 했는데, excel 파일에서 읽어서 리턴해주는 것이여서 옮기지 않았음.
package com.example.excelparser.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.text.SimpleDateFormat;
import java.util.Date;
@RestController
@Slf4j
public class ViewController {
private FileUpDownController fileUpDownController;
public ViewController(FileUpDownController fileUpDownController) {
this.fileUpDownController = fileUpDownController;
}
// 메인 뷰
@GetMapping("/")
public ModelAndView main(ModelAndView mav) throws IOException {
/* MAIN 로드할 때, 업로드 시간 표시할 파일 경로 */
String upload_list_path = fileUpDownController.UPLOAD_LIST_PATH;
String userListFileName = fileUpDownController.USERLIST_FILENAME;
String upload_absence_path = fileUpDownController.UPLOAD_ABSENCE_PATH;
String absenceFileName = fileUpDownController.ABSENCE_FILENAME;
log.info("{}", upload_list_path);
Path list = Paths.get(upload_list_path + "/" + userListFileName);
if(Files.exists(list)){
BasicFileAttributes basicFileAttributes1
= Files.readAttributes(list, BasicFileAttributes.class);
Date time1 = new Date(basicFileAttributes1.lastAccessTime().toMillis());
String current_upload_time1 = new SimpleDateFormat("yyyy년 MM월 dd일 HH시 mm분 ss초").format(time1);
mav.addObject("list_current_upload_time", current_upload_time1);
mav.addObject("list_filename", list.getFileName());
}
Path absence = Paths.get(upload_absence_path + "/" + absenceFileName);
if(Files.exists(absence)){
BasicFileAttributes basicFileAttributes2
= Files.readAttributes(absence, BasicFileAttributes.class);
Date time2 = new Date(basicFileAttributes2.lastAccessTime().toMillis());
String current_upload_time2 = new SimpleDateFormat("yyyy년 MM월 dd일 HH시 mm분 ss초").format(time2);
mav.addObject("ab_current_upload_time", current_upload_time2);
mav.addObject("ab_filename", absence.getFileName());
}
mav.setViewName("absence");
return mav;
}
}
진행결과
* 불필요한 메서드를 8개 가량 삭제할 수 있었음.
* 컨트롤러를 나누면서 이름을 변경하여, 역할을 유추할 수 있게 되었음
* 진행하면서 분리할 메서드 기능들을 주석으로 미리 나열을 했는데, 다음번에는 코드 작성전에 이렇게 주석으로 구조를 배치해보는 것도 좋은 방법같음.
* 해소되지 않은 부분은 코드 중복이 눈에 보이는데 이것을 어떻게 처리할지, 처리를 해야하는지? 에 대한 의문
* 컨트롤러에 여전히 처리로직이 담겨있는데 이 부분을 따로 유틸클래스로 넘길것인지?
'[Project]naverworkplace-absence > Refactoring' 카테고리의 다른 글
[마무리] - 진행하면서 느낀 점 (0) | 2023.10.12 |
---|---|
[Refactoring] - DTO 클래스 수정하기 (0) | 2023.10.11 |
[Refactoring] - 만든 프로그램을 리팩토링해보기 (2) | 2023.10.10 |