ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • (개발지식) - controller 와 service 작성 + 파일 업로드/다운로드 구현 + Swagger 사용
    개발/개발지식 2025. 1. 21. 14:29

    ■ Controller 와 서비스 

    kd 헬스케어 개발 중 controller 와 Service 에 어떤 것을 작성해야 하는지 잘 구분이 안갔다. 

     

    1. 컨틀롤러
    - 입력값에 대한 validation 체크
    - 서비스 값에 대한 비즈니스 exception 처리
    ▶ 입출력에 대한 처리 

     

    즉, 입력된 Request Body 나 Request Param 으로 받은 값이 유효한지 체크하거나,

    서비스 단에서 처리된 비즈니스에 대해서 나오는 exception 처리를 해준다. 

    2. 서비스 
    - 비즈니스 로직 
    - 값에 대한 계산, 필터링

     

    여기서 본격적으로 우리의 비즈니스에 대한 값을 처리한다. 

    나의 경우 파일 업로드/다운로드 구현했는데 파일 경로를 설정하고 

    파일 값을 세팅해주고 등등 작업을 여기서구현하는 것이다.  

     

    이를 더 잘 알기 위해서 mvc 패턴을 파악해보면 좋다. 

     

    ■ 파일 업로드/다운로드

     

    이번 프로젝트에서 처음으로 파일 업로드/다운로드 로직을 구현했다.

    db는 id, file_key(UUID 랜덤값 생성), path(오늘날짜), name, del_yn,

    created_at, created_by, updated_at, updated_by 로 구성했다. 

     

    4개 생성, 수정 관련 값은 프로젝트 공통으로 AuditInfo 로 @Embeded 설정 처리했다.

     

    * 파일 업로드

    우선 기본 로직은 클라이언트에서 param 값으로 File 을 받으면,

    absolute 경로에 오늘 날짜 기준으로 폴더를 생성해서 업로드한 파일을 저장한다.

     

    파일은 dto 로 세팅해주고 이를 entity 로 변환해주는 메서드를 사용해서,

    JpaRepository 를 상속받아서 save 메서드를 통해서  Entity 형태로 반환해준다. 

    컨트롤러에서는 해당 값을 data에 담아서 성공 응답을 준다. 

     

    ˙ Controller

    /*
    * 파일 업로드
    * */
    @Operation(summary = "업로드", description = "업로드")
    @PostMapping(value = "/file-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public DeferredResult<ResponseEntity<?>> upload(@RequestParam("file") MultipartFile file) {
        return successResponse(fileInfoService.upload(file));
    }

     

    ˙ Service

    /**
     * 파일 업로드
     * @param file : MultipartFile 파일
     * @return FileInfoEntity
     */
    @Transactional
    public FileInfoEntity upload(MultipartFile file) {
        String currentDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        Path targetDir = Paths.get(absolutePath, currentDate);
        log.info("targetDir {} ", targetDir);
    
        //오늘 날짜 업로드 폴더 없을 경우 폴더 생성
        try {
            if (!Files.exists(targetDir)) {
                Files.createDirectories(targetDir);
            }
        } catch (IOException e) {
            log.error("Failed to create directory: " + targetDir, e);
            throw new RuntimeException("Failed to create directory: " + targetDir, e);
        }
    
        log.info("upload file: {}", file.getOriginalFilename());
    
        //파일 이름, key
        String filename = file.getOriginalFilename();
        String fileKey = (UUID.randomUUID()).toString();
    
        //dto 파일 key, 경로, 이름, 삭제여부 설정
        try {
            FileInfoDto.Add requestDto = new FileInfoDto.Add();
            requestDto.setFileKey(fileKey);
            requestDto.setPath(currentDate);
            requestDto.setName(filename);
            requestDto.setDelYn(Yn.N);
    
            Path filePath = targetDir.resolve(fileKey);
            file.transferTo(filePath.toFile());
    
            log.info("Success to upload file: {}", filename);
            FileInfoEntity fileInfoEntity = fileInfoMapper.toEntity(requestDto);
            return fileInfoRepository.save(fileInfoEntity);
    
        } catch (IOException e) {
            log.error("Failed to upload file: " + filename, e);
            throw new RuntimeException("Failed to upload file: " + filename, e);
        }
    }

     

     

    * 파일 다운로드

    파일 다운로드의 경우에는 fileKey 값을 param 으로 받고,

    해당 값을 바탕으로 Repository 에서 일치하는 파일 여부를 체크,

    해당 파일 경로를 세팅 후 해당 경로에 파일이 있는지도 체크해서

    있다면 해당 파일을 dto 에 넣어서 파일과 파일 이름으로 Controller 단으로 넘겨준다.

     

    컨트롤러에서는 받은 파일과 이름을 통해서 응답값을 내보낸다.

     

    ˙ Controller

    /*
    * 파일 다운로드
    * */
    @Operation(summary = "다운로드", description = "다운로드")
    @GetMapping("/file-info")
    public DeferredResult<ResponseEntity<?>> download(@RequestParam("fileKey") String fileKey) {
        FileInfoDto.Result result = fileInfoService.download(fileKey);
        return download(result.getFile(), result.getName());
    }

     

    ˙ Service

    /**
     * 파일 다운로드
     * @param fileKey : 파일키
     * @return FileInfoDto.Result
     */
    public FileInfoDto.Result download(String fileKey) {
        log.info("Received fileKey: {}", fileKey);
    
        try {
            //파일키랑 일치하는 파일 가져오기
            FileInfoEntity fileInfo = fileInfoRepository.findByFileKey(fileKey);
            log.info("fileInfo: {}", fileInfo);
    
            if (fileInfo == null) {
                throw new FileNotFoundException("File not found: " + fileKey);
            }
    
            // 파일 경로 설정
            Path filePath = Paths.get(absolutePath, fileInfo.getPath(), fileInfo.getFileKey());
    
            // 파일이 존재하는지 확인
            if (!Files.exists(filePath)) {
                throw new FileNotFoundException("File does not exist at path: " + filePath);
            }
    
            File file = filePath.toFile();
            String fileName = fileInfo.getName();
    
            return new FileInfoDto.Result(file, fileName);
    
        } catch (IOException e) {
            // 예외 처리
            return null;
        }
    }

     

    ˙ DeferredResult download (공통 응답)

    /**
     * 파일 다운로드. 파일명 지정
     *
     * @param file : 다운로드 파일
     * @param fileName : 파일명 지정
     * @return DeferredResult
     */
    public DeferredResult<ResponseEntity<?>> download(File file, String fileName) {
        DeferredResult<ResponseEntity<?>> deferredResult;
    
        if(StringUtils.isEmpty(fileName)){
            deferredResult = download(file);
        } else {
            deferredResult = new DeferredResult<>();
            CompletableFuture.runAsync(() -> {
                try {
                    // 비동기 작업: 파일 준비
                    InputStreamResource resource = new InputStreamResource(new FileInputStream(file));
    
                    // HTTP 응답 생성
                    ResponseEntity<InputStreamResource> response = ResponseEntity.ok()
                            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
                            .contentLength(file.length())
                            .contentType(MediaType.APPLICATION_OCTET_STREAM)
                            .body(resource);
    
                    // 작업 완료 후 결과 설정
                    deferredResult.setResult(response);
                } catch (Exception e) {
                    deferredResult.setErrorResult(ResponseEntity.internalServerError().build());
                }
            });
        }
    
        return deferredResult;
    }

     

    ■ Swagger

    Swagger 로 문서 관리 시

    @Operation(summary = "업로드", description = "업로드")

     

    와 같이 달아주면 Swagger 에서 한 눈에 해당 정보를 알 수 있다. 

     

     

    서비스단은 웬만하면 주석을 달아주자

    주석을 달 때는 /** + enter 처리 하면 자동으로 형식을 만들어주는데,

    param 으로 전달되는 값과 return 값을 적어주면

     

     

     

    아래와 같이 Controller 단에서 메서드 위에 마우스만 올려도 

    해당 메서드의 파람값과 return 값을 들어가지 않아도 한 눈에 볼 수 있다. 

Designed by Tistory.