5.프로젝트 중 관련 이슈 및 구현 기술 - well0924/coffie_place GitHub Wiki

1. Swagger를 활용해서 Rest Api 문서 자동화

1-1.Swagger를 사용하려면 먼저 gradle에 라이브러리를 주입

// Swagger implementation 'io.springfox:springfox-boot-starter:3.0.0' implementation 'io.springfox:springfox-swagger-ui:3.0.0'

1-2.Swagger에 관련된 설정 Class를 작성

@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 문서의 버전

1-3. 스웨거 어노테이션 사용

@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.ImageScaling 을 활용해서 이미지 업로드시 리사이징으로 서버용량 최적화

  • 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. Apache Poi를 활용해서 엑셀 파일 다운로드 기능

  • 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;
}

}

⚠️ **GitHub.com Fallback** ⚠️