-
(개발지식) - 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 값을 들어가지 않아도 한 눈에 볼 수 있다.
'개발 > 개발지식' 카테고리의 다른 글
(개발지식) - vscode 단축키 (윈도우 기준) (1) 2025.07.11 (개발지식) - ROLLUP, CUBE (0) 2025.02.17 (개발지식) 13 - 인텔리제이 단축키(윈도우 기준) (0) 2024.04.08 (개발지식) 12 - 프레임워크(Framework) 란? + Spring vs SpringBoot (0) 2024.01.24 (개발지식) 11 - REST / RESTful / RESTful API 란? (0) 2024.01.16