SsackTeun/onware-portal-renew01 (github.com)

결과물 + @ 

: 가장 만족스러웠던 부분은 파일의 갯수가 비약적으로 많이 줄었다.

: 파일이름이 의미를 가지게 되어 수정이 필요할 경우 빠르게 파악할 수 있을 것으로 기대된다

 

리팩터링 할 때는 워크플로우를 먼저 점검하기.


* 한 가지 요청에 여러 가지 서비스 메서드로 구현하지 않았는지?  - 단일책임원칙을 안 지킨 예

 

코드 문제점?

  • 서비스를 구현할 때, 서비스가 서비스를 연쇄적으로 호출하는 만행을 저질렀다.
  • 고치다보니 많이 배우게 되는데, 이것이 "단일책임원칙"이라는 걸 알게 되었다. 

 

왜 이런실수를 저질렀을까?

  • 결국 초점을 계속 내부에 두다 보니, 여태껏 이해를 못 했다는 생각이 든다.
  • 초점을 내부 구현을 무시한 채, 이 메서드는 무슨 일을 하는가?라고 물음을 던져보면 아주 쉬운 문제였다.

 

"한 가지 기능 구현" = "단일책임원칙" 

"한 가지 기능  구현을 위한 처리 구현" = "단일책임원칙"

 

이렇게 기억해야겠다.

 

다음번엔?

  • 이런 실수를 하지 않게, 무작정 코드부터 작성하지 않기.
  • 인터페이스를 활용한다음, 구현객체에 추가하고 "플로우"를 먼저 글로 작성해서 흐름을 먼저 만들기
  • 그전에 설계라는 걸 해야겠지만, 좀 더 실수를 해야된다.

 

 

 

DTO 클래스


공공데이터포탈의 공휴일정보를 얻어오는 API 의 DTO 와 내부에서 데이터 처리하는 DTO들이 섞여있다.

 

그러나 이렇게 보았을 때, DTO 의 구조파악이 안 되었다.

 

DTO 클래스를 만들 때마다 하나의 파일에 DTO를 전부 넣을 수는 없나라는 생각을 계속했다.

 

이것에 대해서 검색해 보니, Inner Class를 사용해서 간소화 하는 방법을 쓰거나 기존대로 Object 단위로 모두 분리해서 사용하는 것을 알게 되었다.

 

Inner Class 를 사용하면, 내부 클래스에 접근하는 코드가 복잡해지는 대신 눈으로 보는 코드상으로는 가독성이 좋아지는 장점이 보였다.

 

둘 중에 어떤 방법을 취할까 고민하다가 한 번도 사용해보지 않은 Inner Class를 적용해 보기로 했다.

 

스키마를 개별적인 파일로 따로 분리할 때는 디렉터리 구조에 신경을 써야 할 것 같다.

-> root 디렉터리를 만들어 관련 DTO를 최소한 분류를 해야겠다는 생각.

-> package 구조처럼 만들어 볼까 싶기도 하나 너무 번거롭거나 복잡하진 않나 싶기도 하다.

 

2023년 09월 특일정보 API 호출 결과이다

{
    "response": {
        "header": {
            "resultCode""00",
            "resultMsg""NORMAL SERVICE."
        },
        "body": {
            "items": {
                "item": [
                    {
                        "dateKind""01",
                        "dateName""추석",
                        "isHoliday""Y",
                        "locdate"20230928,
                        "seq"1
                    },
                    {
                        "dateKind""01",
                        "dateName""추석",
                        "isHoliday""Y",
                        "locdate"20230929,
                        "seq"1
                    },
                    {
                        "dateKind""01",
                        "dateName""추석",
                        "isHoliday""Y",
                        "locdate"20230930,
                        "seq"1
                    }
                ]
            },
            "numOfRows"100,
            "pageNo"1,
            "totalCount"3
        }
    }
}

Inner Class로 수정한 결과


package com.example.excelparser.dto.spcdeinfoapi;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RestDeInfoDTO{
    private Response response;
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Response {
        private Header header;
        private Body body;
    }
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Header {
        private String resultCode;
        private String resultMsg;
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Body {
        private Object items;
        private String numOfRows;
        private String pageNo;
        private String totalCount;
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Item {
        private String dateKind;
        private String dateName;
        private String isHoliday;
        private String locdate;
        private String seq;
    }
}

5~6 개 되는 파일이 파일하나로 매우 간결해졌다. 객체 간 관계를 파악하기도 편해졌다.

 

특일정보 API DTO는 정리가 되었으나, 내부데이터 가공 용도로 만든 DTO 가 남았다.

 

OriginDTO - 엑셀에서 POI API로 읽어서 해당객체에 저장하려고 만든 DTO


package com.example.excelparser.dto.original;

import lombok.Data;

@Data
public class OriginDTO {
    //문서번호
    private String doc_num;

    //이름
    private String name;

    //로그인 아이디
    private String loginId;

    //직책
    private String position;

    //부재 항목
    private String absentCase;

    //일수
    private String days;

    //기간
    private String durations;

    //작성일
    private String requestDate;
}

 

살펴보았을 때, 클래스 이름만 바꾸어 주면 될 것 같다.

 

SourceExcelDataExtractorDTO.java로 변경 (원본엑셀데이터추출)

 

나머지는 추가적으로 파악이 필요할 것 같다.

 

코드 동작 순서가 파악이 되지 않음.. -> 지금 와서 보니 메서드를 연쇄 호출해서 순서를 파악하기 어려움!!

 

고민을 해본 끝에 디버깅을 해보았다.

 

최초에 호출되는 메서드를 기준으로 줄줄이 소세지로 메서드가 왔다갔다 실행이 된다.

 

변경 계획

  1. 서비스 클래스를 "결과를 추출하는 메서드" 하나로 만든다.
  2. 임시로 서비스 클래스를 생성하여, 디버깅하면서 주석으로 워크플로우를 기록
  3. 기록한 워크플로우를 기준으로 "결과 추출 메서드" 내부에 워크플로우 기준으로 "기능단위"로 유틸리티 클래스를 손본다.

 

 

서비스 클래스 수정 -

(기존에는 isHolidayCalculate 메서드 내부에 있는 1~5 주석들 각각 메서드가 개별적으로 있었기 때문에

워크플로우가 파악이 안되는 문제가 있었음)

package com.example.excelparser.service;

import com.example.excelparser.dto.MergeDTO;
import com.example.excelparser.dto.UserListDTO;
import com.example.excelparser.dto.original.MergeOriginWithDurationDTO;
import com.example.excelparser.dto.original.SourceExcelDataExtractorDTO;
import com.example.excelparser.util.excel.ExcelCreation;
import com.example.excelparser.util.excel.ExcelParserUtil;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

public class AbsenceCalculatorService implements AbsenceCalculator {
    @Override
    public void isHolidayCalculate(
            String years,
            String month,
            HttpServletResponse response) throws IOException {
        // 1. absence.xlsx 파일 읽기 - ExcelParserUtil.java readAbsenceTimeOffList()
        List<SourceExcelDataExtractorDTO> absence = ExcelParserUtil.readAbsenceTimeOffList();

        // 2. list.xlsx 파일 읽기 - ExcelParserUtil.java getUsers()
        List<UserListDTO> userList = ExcelParserUtil.getUsers();

        // 3. 날짜 데이터 변환 &  워크플레이스 아이디 기준으로 병합
        List<MergeOriginWithDurationDTO> merged = MergeOriginWithDurationDTO.convertTo(absence, userList);
        
        // 4. 3번 결과에서 월이 넘어가는 데이터가 있다면, 월을 나누는 작업
        List<MergeOriginWithDurationDTO> reform = MergeOriginWithDurationDTO.compareMonth(merged);
        
        // 5. 4번 결과에서 다시 년도와 월을 선택
        List<MergeOriginWithDurationDTO> result = MergeOriginWithDurationDTO.compareMonth(reform, years, month);
        
        // 6. 5번 결과 데이터를 토대로 엑셀 파일을 생성
        new ExcelCreation().createFile(response, MergeDTO.convert(result), years, month);
    }
}

 

Controller 도 조금 변경되었음

[기존]

@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);
}

[변경]

@GetMapping("/files/download/result/{years}/{month}")
public void calculate(@PathVariable("years") String years,
                      @PathVariable("month") String month,
                      HttpServletResponse response) throws IOException {
    new AbsenceCalculatorService().isHolidayCalculate(years,month,response);
}

유틸클래스 메서드를 Controller 에서 호출하지 않고, Service 단으로 이동함

함수의 파라미터로 함수를 넣는 복잡함이 사라지고 코드 가독성도 높아졌음.

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개 가량 삭제할 수 있었음.

 

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

 

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

 

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

 

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

주로하는 업무가 유지보수다보니, 코드를 작성하기보단 작성된 코드가 작동상의 오류를 일으키면 찾아서 수정을 요청하거나, 테스트하거나 하는 일이 많다. 그래서 코드를 설계하고, 작성하는 일이 없다 싶히 하다.

보통 로직을 수정하는 일은 있어도, 설계에 포함되는 코드 작성은 없으니... 

 

회사에 코딩을 할 줄 아는 사람이 나밖에 없기때문에 무언가 피드백을 받거나, 설계를 하거나 할 때 참 아쉽다는 생각을 많이 했다. 

 

피드백이 없을 때 오는 가장 큰 단점은 훨씬 더 많은 시행착오를 겪어야하고, 그에 따른 시간 소모 또한 배로 들어간다.

스스로에게 피드백을 줄 수 있겠지만, 이 것은 스스로의 잘못된 생각을 고착화 시키는 것에 가까웠고, 해결을 더 어렵게 했던것 같다. 결국 더 고집스러운 형태의 프로그램이 만들어지는 결과를 가져왔다고 생각한다.

 

결과적으로 유연하지 못했으며, 변경점에 매우 취약했다. 또한 변수이름, 클래스이름 등 모두 내가 작성한것임에도 다시보면 한참을 봐야하고 딱 한마디로 읽기 싫은 코드다. 무엇인가 변경을 요청하면 한숨부터나오고, 수정을 하기 위해서는 어디를 봐야하며 그 변경점의 영향이 어디까지 미칠지 감이 1도 오지 않았다.

 

요 며칠간 내가 작성했던 프로그램을 수정하고 있다.

월별로 사원들의 휴가사용을 취합하여, 실 근무일수, 근로시간 등을 계산하여 엑셀로 만들어서 매달 회계사무실로 보낸다.

올해 3월에 처음 만들어서 보름정도를 소요한 것 같다. 

 

데이터 원본소스는 "NaverWorkPlace" 플랫폼의 부재항목을 엑셀데이터로 내려 받을 수 있는데, 이 데이터는 조회를 

해당월 1월부터 ~ 말일 까지하더라도, 해당월에 연차신청을 다음달꺼까지하게되면 다른 월까지 함께 조회되어 재가공이 필요했다. 먼저 만들기 전에 네이버에서 이 부분을 수정해준다면, 만들 필요가 없으므로 확인을 해보았는데, 앞으로도 변경할 계획은 없다고 했다.

 

목적은?

1. 네이버워크플레이스에서 얻을 수 있는 "사원의 연차데이터" 엑셀데이터를 읽음

2. 원하는 달의 데이터만 포함되어 계산 -> 엑셀로 다운로드

 

크게 고민했던 부분은?

1. 공휴일, 대체공휴일 데이터를 얻는 부분에 대해서 고민

2. 엑셀원본데이터를 정규화하여, 파싱하는 부분

3. 네이버측에서 원본데이터의 형태를 변경해버려, 수정해야 했던 부분

4. 월이 다른 데이터를 제외시키는 방법

 

리팩토링을 해보자고 결심한 이유?

1. 코드가 수정에 취약한 것은 네이버측에서 원본데이터 형태를 바꿔버렸을 때 느꼈다.

2. 결정적으로 월별로 "실 근무일수"를 추가해달라는 요청을 받고 코드를 살펴보는데 

필요한 코드와 필요없는 코드, 메서드이름, 클래스이름을 보는데 용도를 알 수 없었고, 흐름을 읽을 수 없었던게 충격이였다. 

3. DTO 들의 Data Source 를 구분할 수 없었음

4. Util 클래스와 Static 메서드가 많이 분산되어 있는데 좀 더 한 곳으로 모아야겠다는 생각을 했다

    -> 구현에 대한 생각이 앞서서였는지, DTO 클래스에도 로직을 작성해놓았다.

5. 아예 안쓰는 클래스를 작성해놓았는데, 지워도 되는지 안되는지 분간을 못하고 고민한 부분도 한 몫했다.