5.프로젝트 중 관련 이슈 및 구현 기술 - well0924/coffie_place GitHub Wiki
// Swagger
implementation 'io.springfox:springfox-boot-starter:3.0.0'
implementation 'io.springfox:springfox-swagger-ui:3.0.0'
@Configuration // 설정을 위한 어노테이션
@EnableSwagger2 // 스웨거를 사용하기 위한 어노테이션
@ComponentScan(basePackages = {"com.kr.coffie.*"}) // 해당 위치를 기반으로 스웨거 어노테이션이 있는 컨트롤러를 스캔
public class SwaggerConfig {
//
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("CoffiePlace")
.version("version 0.2")
.description("Coffieplace Api")
.license("license")
.build();
}
//swagger설정
@Bean
public Docket swaggerApi() {
return new Docket(DocumentationType.SWAGGER_2)
.useDefaultResponseMessages(false)
.select()
.apis(RequestHandlerSelectors.basePackage("com.kr.coffie"))
.paths(PathSelectors.ant("/api/**"))
.build()
.apiInfo(apiInfo());
}
}
위의 코드를 토대로 해서 설명을 하면 Docket은 Swagger의 설정을 도와주는 class를 말합니다.
useDefaultResponseMessages :
기본적으로 swagger-ui로 들어가서 api의 기본적인 응답값을 만들어줍니다.
해당 기능을 false로 해놓으면 응답값을 커스텀 할 수 있습니다.
select :
ApiSelectorBuilder 클래스의 인스턴스를 반환합니다.
해당 인스턴스를 통해 Swagger의 end-point를 제어할 수 있습니다.
apis :
api spec이 작성되어 있는 패키지를 지정합니다.
RequestHandlerSelectors.any()로 설정한다면 전체 Api에 대한 문서를 나타낼 수 있습니다.
paths :
path의 조건에 해당하는 Api를 문서화합니다.
PathSelectors.any()로 한다면 전체 Api 패턴에 대한 문서를 나타낼 수 있습니다.
PathSelectors.ant()로 특정 Api url pattern을 가진 Api만 문서를 만들 수도 있습니다.
apiInfo :
아래의 Swagger Api 문서에 대한 설정 객체를 등록해줍니다.
ApiInfo는 Swagger API 페이지에 대한 내용을 담고 있습니다.
title : API 문서의 제목
description : API 페이지에 대한 설명
version : API 문서의 버전
@Api(tags = {"자유게시판 Api"} ,value="자유게시판에 사용되는 기능 api")
@RestController
@AllArgsConstructor
@RequestMapping("/api/board/*")
public class BoardApiController {
....
@Api : Api가 어떤 역할을 하는 지 표시하는 어노테이션으로 컨트롤러 위에 추가를 합니다.
-
tags : Swagger UI에 보일 컨트롤러의 Title명칭 부여
-
value : 컨트롤러 옆에 보일 간단한 정보
@ApiResponses({
@ApiResponse(code=200, message="common ok"),
@ApiResponse(code=400, message="bad request"),
@ApiResponse(code=401, message="unauthorize"),
@ApiResponse(code=403, message="fobidden"),
@ApiResponse(code=404, message="not found"),
@ApiResponse(code=500, message="error")
})
@ApiOperation(value = "게시글 전체 조회 API",notes="자유게시판에서 글목록을 조회합니다.")
@ApiImplicitParams({
@ApiImplicitParam(name="keyword",value="검색어",example="test",dataType = "String",paramType = "query"),
@ApiImplicitParam(name="page",value="페이지",example="1",dataType = "Integer",paramType = "query"),
@ApiImplicitParam(name="perPageNum",value="페이지번호",example="5",dataType = "Integer",paramType = "query"),
@ApiImplicitParam(name="searchType",value="검색타입",example="T",dataType = "String",paramType = "query")
})
@GetMapping(value="/list")
public ResponseDto<List<BoardDto.BoardResponseDto>> articelist(Criteria cri)throws Exception{
List<BoardDto.BoardResponseDto>list = null;
int totallist =0;
list = service.boardlist(cri);
totallist = service.totalarticle(cri);
Paging paging = new Paging();
paging.setCri(cri);
paging.setTotalCount(totallist);
return new ResponseDto<>(HttpStatus.OK.value(),list);
}
@ApiOperation : method에 대한 설명과 기능을 표시하는 어노테이션
-
value : API에 대한 정보를 요약해서 설명
-
notes : API에 대한 정보를 자세히 설명
@ApiResponses : api의 응답값을 모아놓은 어노테이션
@ApiResponse : api의 응답값에 맞게 response의 설명을 작성하는 어노테이션
@ApiImplicitParam : 해당 API Method 호출에 필요한 Parameter들의 설명을 추가할 수 있다.
@ApiIgnore : Swagger ui에 표시를 하지 않게 하는 어노테이션
@ApiModel(value="게시판 요청 dto",description = "자유게시판 요청에 필요한 dto")
@Getter
@Setter
@ToString
@Builder
@AllArgsConstructor
public static class BoardRequestDto {
@ApiModelProperty(value="게시글번호",example="1")
private Integer boardId;
@ApiModelProperty(value="게시글제목",example="title",required = true)
@NotBlank(message = "제목을 입력해주세요.")
private String boardTitle;
@ApiModelProperty(value="게시글내용",example="contents",required = true)
@NotBlank(message = "내용을 입력해주세요.")
private String boardContents;
@ApiModelProperty(value="게시글저자",example="writer",required = true)
private String boardAuthor;
@ApiModelProperty(value="게시글 조회수",example="0",required = true)
private Integer readCount;
@ApiModelProperty(value="게시글비밀번호",example="1111")
private Integer passWd;
@ApiModelProperty(value="게시글 파일그룹아이디",example="free_ge3b53",required = true)
private String fileGroupId;
@ApiModelProperty(value="글 등록일",example="2022-09-21 12:34:00",required = true)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",shape = Shape.STRING)
private LocalDateTime createdAt;
}
@ApiModel :
- value: 모델에 대한 설명
- description: 모델에 대한 상세 설명
@ApiModelProperty :
- value: 속성에 대한 설명
- example: 속성의 default 값 또는 예시
- position: Swagger 문서에서 보이는 순서
- required: 속성의 필수여부 표기. 필수는 true, 아니면 false.
-
2-1.상황
-
가게 등록 페이지에서 가게 이미지를 업로드할 때 이미지의 크기가 일정하지 않다는 점
-
가게 이미지의 크기가 크면 용량도 크므로 용량을 줄일 필요가 있다고 판단
-
2-2.적용
-
이미지를 리사이징을 하는데 ImageScaling라이브러리를 활용해서 업로드를 하는 이미지를 리사이징을 하는 기능을 구현
//ImageScaling
implementation 'com.jhlabs:filters:2.0.235-1'
implementation 'com.mortennobel:java-image-scaling:0.8.6'
//이미지 리사이징
public String ResizeFile(FileDto.ImageResponseDto dto,int width,int height) {
String defaultFolder = filePath+File.separator+dto.getImgGroup()+File.separator+dto.getFileType()+File.separator;
String originFilePath = defaultFolder+dto.getStoredName();
File file = new File(originFilePath);
String thumblocalPath = "";
boolean resultCode = false;
try {
if(filePath != null && filePath.length() !=0) {
String originFileName = file.getName();
String ext = originFileName.substring(originFileName.lastIndexOf(".")+1);
String thumbFileName = originFileName.substring(0,originFileName.lastIndexOf("."))+"_thumb."+ext;
BufferedImage originImage = ImageIO.read(new FileInputStream(file));
MultiStepRescaleOp scaleImage = new MultiStepRescaleOp(width,height);
scaleImage.setUnsharpenMask(AdvancedResizeOp.UnsharpenMask.Soft);
BufferedImage resizeImage = scaleImage.filter(originImage,null);
String fullPath = defaultFolder + "thumb"+File.separator+ thumbFileName;
File out = new File(fullPath);
if(!out.getParentFile().exists()) {
out.getParentFile().mkdirs();
}
if(!out.exists()) {
resultCode = ImageIO.write(resizeImage, ext, out);
if(resultCode) {
thumblocalPath = imgPath + dto.getImgGroup()+"/"+dto.getFileType()+"/thumb/"+out.getName();
}else {
thumblocalPath = null;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return thumblocalPath;
}
서비스 단에서 이미지의 크기를 조절해서 리사이징을 하면 된다.
public int placeregister(PlaceDto.PlaceRequestDto dto,FileDto.ImageRequestDto imgvo)throws Exception{
int result = mapper.placeregister(dto);
List<FileDto.ImageResponseDto>imagelist = new ArrayList<>();
FileDto.ImageResponseDto imglist = null;
//이미지 업로드
if(result > 0) {
imagelist = utile.imagefileupload(imgvo);
}
//첨부 파일이 있는 경우
if(imagelist != null) {
String resize = "";
for(int i =0 ; i<imagelist.size();i++) {
imglist = imagelist.get(i);
//첫번째 이미지의 경우
if(i == 0) {
imglist.setIsTitle("1");
//이미지 리사이징
resize = utile.ResizeFile(imglist, 360, 360);
}else {//나머지
resize = utile.ResizeFile(imglist, 120, 120);
}
imglist.setPlaceId(dto.getPlaceId());
imgvo.setImgGroup("coffieplace");
imgvo.setFileType("place");
imglist.setThumbFileImagePath(resize);
int attachresult = filemapper.placeimageinsert(imglist);
}
}
return result;
};
-
3-1. 상황
-
Apache poi를 활용해서 가게 목록을 엑셀 파일로 다운로드를 할 수 있는 기능을 완성했지만 좀 더 유지보수를 위해서 코드 자체를 리팩토링을 하기로 했습니다.
-
3-2. 해결
컨트롤러
@Operation(summary = "가게 목록 엑셀 다운로드",description = "가게 목록을 엑셀파일로 다운로드한다.",responses = {
@ApiResponse(responseCode = "204")
})
@GetMapping("/place-download")
public DownloadResponseDto<?> getPlaceListDownload(HttpServletRequest req, HttpServletResponse res) throws Exception {
Listlist = placeRepository.findAll();
Listresult = list.stream().map(place->new PlaceResponseDto(place)).collect(Collectors.toList());
ExcelServiceexcelList = new ExcelService<>(result,PlaceResponseDto.class);
excelList.downloadExcel(res);
return new DownloadResponseDto<>();
}
@Getter
@ToString
@Builder
@RequiredArgsConstructor
@AllArgsConstructor
public class DownloadResponseDto {
private Integer status;
private HttpHeaders headers;
private Resource res;
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelColumn {
String headerName() default "";
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelFileName {
String fileName() default "";
}
@Getter
@RequiredArgsConstructor
public class ExcelRenderResource {
private final String excelFileName;
private final Map<String,String> excelHeaderNames;
private final List<String> dataFieldNames;
public String getExcelHeaderName(String dataFieldName) {
return excelHeaderNames.get(dataFieldName);
}
}
public class ExcelRenderResourceFactory {
public static ExcelRenderResource preparRenderResource(Class<?>type) {
String fileName = getFileName(type);
Map<String,String> headerNamesMap = new LinkedHashMap<>();
List<String>fieldNames = new ArrayList<>();
for(Field field : SuperClassReflectionUtil.getAllFields(type)) {
if(field.isAnnotationPresent(ExcelColumn.class)) {
ExcelColumn annotation = field.getAnnotation(ExcelColumn.class);
fieldNames.add(field.getName());
String headerName = annotation.headerName();
headerName = StringUtils.hasText(headerName) ? headerName : field.getName();
headerNamesMap.put(field.getName(), headerName);
}
}
return new ExcelRenderResource(fileName, headerNamesMap, fieldNames);
}
private static String getFileName(Class<?>type) {
String fileName = type.getSimpleName();
if(type.isAnnotationPresent(ExcelFileName.class)) {
fileName = type.getAnnotation(ExcelFileName.class).fileName();
if(!StringUtils.hasText(fileName))fileName = type.getSimpleName();
}
return fileName;
}
}
public class ExcelService {
private final Workbook workbook;
private final Sheet sheet;
private final ExcelRenderResource resource;
private final List<T> dataList;
private int rowIndex =0;
public ExcelService(List<T>dataList,Class<T>type){
this.workbook = new HSSFWorkbook();
this.sheet =workbook.createSheet();
this.resource = ExcelRenderResourceFactory.preparRenderResource(type);
this.dataList = dataList;
}
public void downloadExcel(HttpServletResponse response) throws Exception {
createHead();
createBody();
writeExcel(response);
}
private void createHead() {
Row row = sheet.createRow(rowIndex++);
int columnIndex = 0;
for(String dataFieldName : resource.getDataFieldNames()) {
Cell cell = row.createCell(columnIndex++);
String value = resource.getExcelHeaderName(dataFieldName);
cell.setCellValue(value);
}
}
private void createBody()throws Exception{
for(T data: dataList) {
Row row = sheet.createRow(rowIndex++);
int columnIndex = 0;
for(String dataFieldName : resource.getDataFieldNames()) {
Cell cell = row.createCell(columnIndex++);
Field field = SuperClassReflectionUtil.getField(data.getClass(), (dataFieldName));
field.setAccessible(true);
Object cellValue = field.get(data);
field.setAccessible(false);
setCellValue(cell,cellValue);
}
}
}
private void writeExcel(HttpServletResponse response)throws Exception{
String fileName = new String(resource.getExcelFileName().getBytes(StandardCharsets.UTF_8),StandardCharsets.ISO_8859_1);
response.setContentType("application/vnd.ms-excel");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,String.format("attachment; filename=\"%s.xls\"",fileName));
workbook.write(response.getOutputStream());
workbook.close();
}
private void setCellValue(Cell cell, Object cellValue) {
if(cellValue instanceof Number) {
Number numberValue = (Number)cellValue;
cell.setCellValue(numberValue.doubleValue());
return;
}
cell.setCellValue(ObjectUtils.isEmpty(cellValue)? "": String.valueOf(cellValue));
}
}
@NoArgsConstructor
public class SuperClassReflectionUtil {
public static List<Field> getAllFields(Class<?>clazz){
List<Field> fields = new ArrayList<>();
for(Class<?>clazzInClasses : getAllClassesIncludingSuperClasses(clazz,true)) {
fields.addAll(Arrays.asList(clazzInClasses.getDeclaredFields()));
}
return fields;
}
public static Annotation getAnnotation(Class<?>clazz,Class<? extends Annotation>targetAnnotation) {
for(Class<?>clazzInClasses : getAllClassesIncludingSuperClasses(clazz, false)) {
if(clazzInClasses.isAnnotationPresent(targetAnnotation)) {
return clazzInClasses.getAnnotation(targetAnnotation);
}
}
return null;
}
public static Field getField(Class<?>clazz,String name)throws Exception{
for(Class<?>clazzInClasses : getAllClassesIncludingSuperClasses(clazz,true)) {
for(Field field : clazzInClasses.getDeclaredFields()) {
if(field.getName().equals(name)) {
return clazzInClasses.getDeclaredField(name);
}
}
}
throw new Exception();
}
private static List<Class<?>> getAllClassesIncludingSuperClasses(Class<?>clazz,boolean fromSuper){
List<Class<?>> classes = new ArrayList<>();
while(clazz != null) {
classes.add(clazz);
clazz = clazz.getSuperclass();
}
if(fromSuper)Collections.reverse(classes);
return classes;
}
}