관리 메뉴

나만의공간

Spring Boot Project Initializr (Request Get/POST 출력) #5 본문

IT/Spring

Spring Boot Project Initializr (Request Get/POST 출력) #5

밥알이 2022. 1. 6. 15:10

연재순서

1. Spring Boot Project Initializr (스프링 부트 프로젝트 생성법)
2. Spring Boot Project Initializr (RestFul API 연결)
3. Spring Boot Project Initialzr (Swagger 연결)  
4. Spring Boot Project Initializr (Log4j 연결)
5. Spring Boot Project Initializr (Requet Get/Post 출력)
6. 번외 : Spring Boot War 파일 생성, 로컬 Tomcat 뛰우기
7.  Srping Boot Project Initializr (GitHub 위치) 

Spring Boot를 이용하여 Get/Post값을 전달 받을경우 Spring에 의해 Domain으로 자동 매핑이 됩니다.
자동 매핑중 Spring내부에서 매핑이 안되고 오류가 발생할 경우가 있습니다. 이때 어떤 Param 정보들이 넘어 왔는지 알수가 없어 답답한 경우가 있었습니다.

이에 Spring에 매핑 되기전에 Filter를 이용하여 넘어온 Param값을 로그로 출력하는 기능을 추가 하고자 합니다.

Domain 로그출력 소스는 아래 블로그에서 가져왔습니다.
https://taetaetae.github.io/2019/06/30/controller-common-logging/

 

Spring에서 Request를 우아하게 로깅하기

스프링 기반의 웹 어플리케이션을 만들다 보면 요청을 처리하는데 맨 처음에 위치하고 있는 Controller(이하 컨트롤러)라는 레이어를 만들게 된다. 그럴때면 사용자가 어떤 요청(Request)을 하였는지

taetaetae.github.io

Dependency추가 내용

Build.gradle 내용

plugins {
//	id 'org.springframework.boot' version '2.6.2'
	id 'org.springframework.boot' version '2.5.2'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
	id 'war'
}

group = 'com.study'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

//wrapper {
//	gradleVersion = "6.8.3"
//	distributionUrl = "https://services.gradle.org/distributions/gradle-$gradleVersion-all.zip"
//}

war {
	enabled = true
	webInf { // webInf에 processeResources의 결과 디렉토리를 포함하도록 추가
		with {
			from("${buildDir}/resources/main")
		}
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	implementation group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
	implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'
	implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.26'
	testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3'

//	param 정보 출력을 위한 Dependency 추가 내용
	implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
	implementation group: 'commons-lang', name: 'commons-lang', version: '2.6'
	implementation group: 'commons-io', name: 'commons-io', version: '2.11.0'
	implementation group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1.1'
	implementation group: 'org.apache.httpcomponents', name: 'httpcore', version: '4.4.15'
	implementation group: 'org.aspectj', name: 'aspectjweaver', version: '1.9.7'

}

test {
	useJUnitPlatform()
}

Filter Class 추가

package com.study.studyTwo.core;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ReadListener;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.entity.ContentType;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;

@Slf4j
@WebFilter(urlPatterns = "/*")
public class ReadableRequestWrapperFilter implements Filter {
	@Override
	public void init(FilterConfig filterConfig) {
		// Do nothing
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
		throws IOException, ServletException {
		ReadableRequestWrapper wrapper = new ReadableRequestWrapper((HttpServletRequest)request);
		chain.doFilter(wrapper, response);
	}

	@Override
	public void destroy() {
		// Do nothing
	}

	public class ReadableRequestWrapper extends HttpServletRequestWrapper {
		private final Charset encoding;
		private byte[] rawData;
		private Map<String, String[]> params = new HashMap<>();

		public ReadableRequestWrapper(HttpServletRequest request) {
			super(request);
			this.params.putAll(request.getParameterMap()); // 원래의 파라미터를 저장

			String charEncoding = request.getCharacterEncoding(); // 인코딩 설정
			this.encoding = StringUtils.isBlank(charEncoding) ? StandardCharsets.UTF_8 : Charset.forName(charEncoding);

			try {
				InputStream is = request.getInputStream();
				this.rawData = IOUtils.toByteArray(is); // InputStream 을 별도로 저장한 다음 getReader() 에서 새 스트림으로 생성

				// body 파싱
				String collect = this.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
				if (StringUtils.isEmpty(collect)) { // body 가 없을경우 로깅 제외
					return;
				}
				if (request.getContentType() != null && request.getContentType().contains(
					ContentType.MULTIPART_FORM_DATA.getMimeType())) { // 파일 업로드시 로깅제외
					return;
				}
				JSONParser jsonParser = new JSONParser();
				Object parse = jsonParser.parse(collect);
				if (parse instanceof JSONArray) {
					JSONArray jsonArray = (JSONArray)jsonParser.parse(collect);
					setParameter("requestBody", jsonArray.toJSONString());
				} else {
					if (parse instanceof String) {
						setParameter("string", parse.toString().replace("\"", "\\\""));
					} else {
						JSONObject jsonObject = (JSONObject)jsonParser.parse(collect);
						Iterator iterator = jsonObject.keySet().iterator();
						while (iterator.hasNext()) {
							String key = (String)iterator.next();
							setParameter(key, jsonObject.get(key).toString().replace("\"", "\\\""));
						}
					}
				}
			} catch (Exception e) {
				log.error("ReadableRequestWrapper init error", e);
			}
		}

		@Override
		public String getParameter(String name) {
			String[] paramArray = getParameterValues(name);
			if (paramArray != null && paramArray.length > 0) {
				return paramArray[0];
			} else {
				return null;
			}
		}

		@Override
		public Map<String, String[]> getParameterMap() {
			return Collections.unmodifiableMap(params);
		}

		@Override
		public Enumeration<String> getParameterNames() {
			return Collections.enumeration(params.keySet());
		}

		@Override
		public String[] getParameterValues(String name) {
			String[] result = null;
			String[] dummyParamValue = params.get(name);

			if (dummyParamValue != null) {
				result = new String[dummyParamValue.length];
				System.arraycopy(dummyParamValue, 0, result, 0, dummyParamValue.length);
			}
			return result;
		}

		public void setParameter(String name, String value) {
			String[] param = {value};
			setParameter(name, param);
		}

		public void setParameter(String name, String[] values) {
			params.put(name, values);
		}

		@Override
		public ServletInputStream getInputStream() {
			final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.rawData);

			return new ServletInputStream() {
				@Override
				public boolean isFinished() {
					return false;
				}

				@Override
				public boolean isReady() {
					return false;
				}

				@Override
				public void setReadListener(ReadListener readListener) {
					// Do nothing
				}

				public int read() {
					return byteArrayInputStream.read();
				}
			};
		}

		@Override
		public BufferedReader getReader() {
			return new BufferedReader(new InputStreamReader(this.getInputStream(), this.encoding));
		}
	}
}

Param 로그 출력할 Inspet Class추가

package com.study.studyTwo.core;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.json.simple.JSONObject;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Component
@Aspect
@Slf4j
public class LogInspect {
	@Pointcut("execution(* com.study..*Controller.*(..))") // 이런 패턴이 실행될 경우 수행
	public void loggerPointCut() {
	}

	@Around("loggerPointCut()")
	public Object methodLog(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
		try {
			Object result = proceedingJoinPoint.proceed();
			HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest(); // request 정보를 가져온다.

			String controllerName = proceedingJoinPoint.getSignature().getDeclaringType().getSimpleName();
			String methodName = proceedingJoinPoint.getSignature().getName();

			Map<String, Object> params = new HashMap<>();

			try {
				params.put("controller", controllerName);
				params.put("method", methodName);
				params.put("params", getParams(request));
//				params.put("log_time", LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
				params.put("log_time", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
				params.put("request_uri", request.getRequestURI());
				params.put("http_method", request.getMethod());
				params.put("request_ip",request.getRemoteAddr());
			} catch (Exception e) {
				log.error("LoggerAspect error", e);
			}
			log.info("params : {}", params); // param에 담긴 정보들을 한번에 로깅한다.

			ObjectMapper om = new ObjectMapper();
			log.info("objectMapper : {}",om.writeValueAsString(params));


			return result;

		} catch (Throwable throwable) {
			throw throwable;
		}
	}

	/**
	 * request 에 담긴 정보를 JSONObject 형태로 반환한다.
	 * @param request
	 * @return
	 */
	private static JSONObject getParams(HttpServletRequest request) {
		JSONObject jsonObject = new JSONObject();
		Enumeration<String> params = request.getParameterNames();
		while (params.hasMoreElements()) {
			String param = params.nextElement();
			String replaceParam = param.replaceAll("\\.", "-");
			jsonObject.put(replaceParam, request.getParameter(param));
		}
		return jsonObject;
	}
}

테스트용 메소드 추가내용

package com.study.studyTwo.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Api(tags={"메인화면 API"})
@Slf4j
public class StudyController {

    @GetMapping("/study")
    @ApiOperation(value="Study 하자")
    public String helloWorld() {
        log.info("로그 테스트중입니다.");
        return "Hello World Study";
    }

    @GetMapping("/param/get/test1")
    @ApiOperation(value="이름")
    public String getTest1(@RequestParam String name) {
        log.info("name : {}",name);
        return "name is " + name;
    }

    @PostMapping("/param/post/test1")
    @ApiOperation(value="이름")
    public String postTest1(@RequestBody String name) {
        log.info("name : {}",name);
        return "name is " + name;
    }

    @PostMapping("/param/post/testList")
    @ApiOperation(value="이름")
    public String postTest1List(@RequestBody List<String> listName) {
        log.info("사이즈 크기 : {}",listName.size());
        return "사이즈 크기 " + listName.size();
    }

}

서버 실행 후 메소드 접근시 노출 되는 로그 정보

swagger 접근 화면

포스트 배열 테스트용 요청시 로그 출력 화면

15:09:41 [INFO ] (StudyController.java:42) 사이즈 크기 : 3
15:09:41 [INFO ] (LogInspect.java:51) params : {request_ip=0:0:0:0:0:0:0:1, controller=StudyController, http_method=POST, method=postTest1List, params={"requestBody":"[\"가나다라\",\"1234\",\"잘나와여 좋아여\"]"}, log_time=2022-01-06 15:09:41, request_uri=/param/post/testList}
15:09:41 [INFO ] (LogInspect.java:54) objectMapper : {"request_ip":"0:0:0:0:0:0:0:1","controller":"StudyController","http_method":"POST","method":"postTest1List","params":{"requestBody":"[\"가나다라\",\"1234\",\"잘나와여 좋아여\"]"},"log_time":"2022-01-06 15:09:41","request_uri":"/param/post/testList"}

신규로 추가된 메소드에 대한 설명은 참고 사이트를 참고 하셔도 됩니다.
추가적인 설명도 기재 예정입니다.

Comments