SsackTeun/naverworkplace-absence-monthly-report: naverworkplace 부재 파싱 -> 월별 휴가 사용시간 산출 (github.com)

 

GitHub - SsackTeun/naverworkplace-absence-monthly-report: naverworkplace 부재 파싱 -> 월별 휴가 사용시간 산출

naverworkplace 부재 파싱 -> 월별 휴가 사용시간 산출. Contribute to SsackTeun/naverworkplace-absence-monthly-report development by creating an account on GitHub.

github.com

 

 

 

컨트롤러 클래스는 적당한가?


클래스 이름은 적당한가?


  • Think : 아닌 것 같다.
  • Why? : 담고 있는 의미가 없음. 클래스는 기본적으로 네이밍에서 역할을 파악할 수 있어야함.

  • How : 도메인 이름 + Controller 
  • Why? : 카테고리(도메인)으로 묶을 수 있으며, 특정 Request 를 추가하거나, 문제가 발생할 때 빠르게 찾을 수 있다.

GPT 에게 물어보는 "도메인 컨트롤러 나누는 방법"


    1. 기능 또는 업무 영역: 가장 일반적인 방법 중 하나는 애플리케이션의 다양한 기능 또는 업무 영역에 따라 도메인 컨트롤러를 나누는 것입니다. 예를 들어, 전자 상거래 애플리케이션의 경우 "상품(Product)Controller," "주문(Order)Controller," "결제(Payment)Controller"와 같이 기능별로 도메인 컨트롤러를 나눌 수 있습니다.
    2. 데이터 모델: 애플리케이션의 데이터 모델에 따라 도메인 컨트롤러를 나눌 수 있습니다. 데이터 모델이 서로 다른 엔터티 또는 도메인 객체를 포함하고 있을 때, 각 엔터티에 대한 도메인 컨트롤러를 만들어 관리합니다.
    3. 사용자 역할 또는 권한: 애플리케이션의 사용자 역할 또는 권한에 따라 도메인 컨트롤러를 분리할 수 있습니다. 예를 들어, 관리자와 일반 사용자 각각에 대한 별도의 도메인 컨트롤러를 생성하여 사용자 권한을 관리할 수 있습니다.
    4. RESTful API 디자인: RESTful API를 설계할 때는 리소스(자원)별로 도메인 컨트롤러를 나누는 것이 일반적입니다. 각 리소스에 대한 CRUD(Create, Read, Update, Delete) 작업을 처리하는 컨트롤러를 별도로 생성합니다.
    5. 모듈화와 재사용성: 도메인 컨트롤러를 모듈화하고 재사용 가능한 코드로 설계하려면 비슷한 기능을 하는 컨트롤러를 하나의 모듈로 묶을 수 있습니다. 이렇게 하면 유사한 동작을 하는 부분을 재사용할 수 있습니다.
    6. 애플리케이션의 크기와 복잡성: 애플리케이션이 크고 복잡한 경우에는 기능을 나누고 모듈화하여 관리하기 쉽도록 도메인 컨트롤러를 분리합니다. 작은 애플리케이션의 경우에는 단순한 구조를 유지할 수 있습니다.
    7. 세분화된 관심사 분리(Separation of Concerns): 도메인 컨트롤러를 나누는 또 다른 기준은 각 컨트롤러가 다른 관심사를 처리하도록 하는 것입니다. 예를 들어, 사용자 관리와 로깅은 서로 다른 관심사이므로 별도의 컨트롤러로 분리할 수 있습니다.
    1. 기능이 많이 없기 때문에 잘게 나누기에 애매
    2. 1 ~ 7번까지 다 살펴보았을 때, 그나마 이게 제일 적합한 것 같음.
      예시를 물어보니, 아래와 같음

    3. 권한을 나누지 않기 때문에 x
    4. RESTful API 라고 하기엔, read 위주
    5. 재사용하기에는 기능 구현은 대부분 유틸클래스에 구현하기 때문에 제외
    6. 애플리케이션이 크거나 복잡하지않기 때문에 x
    7. 세분화된 관심사 분리
      Spring AOP를 이용하면 로깅과 같은 공통 관심사를 Aspect로 정의하고, AspectJ 표현식을 사용하여 어느 메서드에서 로깅을 적용할지 결정할 수 있습니다. 이렇게 하면 핵심 비즈니스 로직에서 로깅 코드를 명시적으로 작성하지 않아도 됩니다.
      1. 로깅 코드의 중복 제거: 핵심 로직에서 로깅 코드를 반복해서 작성하지 않아도 됩니다.
      2. 코드의 분리: 로깅과 같은 관심사를 별도의 모듈(Aspect)로 분리하여 코드를 더 깔끔하게 유지할 수 있습니다.
      3. 재사용성: 로깅 로직을 한 번 정의하면 여러 곳에서 재사용할 수 있습니다.
      4. 유지 보수성 향상: 로깅 레벨을 변경하거나 로깅 방식을 수정할 때, 모든 핵심 로직을 수정하지 않고도 Aspect를 수정하여 쉽게 대응할 수 있습니다.
      따라서 Spring AOP를 활용하면 로깅과 같은 공통 관심사를 더 효율적으로 다룰 수 있으며, 메인 소스 코드에 로깅 코드를 포함시키지 않아도 됩니다.

그래서 결론은?


  • 추후에 불필요한 메서드를 제거하고, 유틸클래스에 포함시킨다면 실제로 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개 가량 삭제할 수 있었음.

 

* 컨트롤러를 나누면서 이름을 변경하여, 역할을 유추할 수 있게 되었음

 

* 진행하면서 분리할 메서드 기능들을 주석으로 미리 나열을 했는데, 다음번에는 코드 작성전에 이렇게 주석으로 구조를 배치해보는 것도 좋은 방법같음. 

 

* 해소되지 않은 부분은 코드 중복이 눈에 보이는데 이것을 어떻게 처리할지, 처리를 해야하는지? 에 대한 의문

 

* 컨트롤러에 여전히 처리로직이 담겨있는데 이 부분을 따로 유틸클래스로 넘길것인지?