Spring์์ Request๋ฅผ ์ฐ์ํ๊ฒ ๋ก๊น ํ๊ธฐ
์คํ๋ง ๊ธฐ๋ฐ์ ์น ์ดํ๋ฆฌ์ผ์ด์
์ ๋ง๋ค๋ค ๋ณด๋ฉด ์์ฒญ์ ์ฒ๋ฆฌํ๋๋ฐ ๋งจ ์ฒ์์ ์์นํ๊ณ ์๋ Controller
(์ดํ ์ปจํธ๋กค๋ฌ)๋ผ๋ ๋ ์ด์ด๋ฅผ ๋ง๋ค๊ฒ ๋๋ค. ๊ทธ๋ด๋๋ฉด ์ฌ์ฉ์๊ฐ ์ด๋ค ์์ฒญ(Request)์ ํ์๋์ง์ ๋ํด ํ์ธ์ด ํ์ํ ์ ์๋ค. ๋ฌผ๋ก ํ์ธ์ ์ํด๋ ๋ฌด๋ฐฉํ์ง๋ง ๊ฐ๊ธ์ ๋ก๊น
์ ์์คํ
๋ก์ง์ ์ํฅ์ ์ฃผ์ง ์๋ ๋ฒ์์์ ์ต๋ํ ๋ค์ํ๊ฒ ๋ฏธ๋ฆฌ
ํด๋๋๊ฒ ๋์ค์ ์ ์ง๋ณด์์ ํธํ ์ ์๋ค. (์์ ์กฐ์ง์ฅ๋๊ป์ ๋ง์ํ์ ๊ฒ ์์ง๋ ๋จธ๋ฆฟ์์ ๊ฝ ์๋ฆฌ์ก๊ณ ์๋ค…)
์~์ฃผ ์ผ๋ฐ์ ์ผ๋ก, ์ปจํธ๋กค๋ฌ์์๋ ๋ค์๊ณผ ๊ฐ์ด ๋ฉ์๋ ๋จ์๋ก ํ๋ผ๋ฏธํฐ๋ฅผ ์ง์ ๋ก๊น
ํ๊ฒ ๋๋ค.
@Slf4j
@RestController
public class SampleController {
@GetMapping("/test1")
public String test1(@RequestParam String id) {
log.info("id : {}", id);
return "length : " + id.length();
}
}
์ด๋ ๊ฒ ๋๋ฉด ์ฌ์ฉ์๊ฐ GET /test1
์ด๋ผ๋ ์์ฒญ์ ๋ณด๋ผ๋ ์ด๋ค ํ๋ผ๋ฏธํฐ๋ก ํธ์ถํ์๋์ง์ ๋ํด ๋ก๊น
์ด ๋จ๊ฒ ๋๋๋ฐ ํญ์ log.info("id : {}", id);
๊ณผ ๊ฐ์ด ์๋์ผ๋ก ๋ก๊น
์ ๋จ๊ฒจ์ผ ํ๋ ๋ถํธํจ์ด ์๊ธด๋ค. ๋ฌผ๋ก ๊ผผ๊ผผํ๊ฒ ๋ฉ์๋๋ง๋ค ๋ก๊น
์ ์ ์ด์ฃผ๋ฉด ์ ํ ๋ฌธ์ ๋ ๊ฒ ์์ง๋ง ์ด๋ฌํ ์ปจํธ๋กค๋ฌ ~ ๋ฉ์๋๊ฐ ํ๋๊ฐ๊ฐ ์๋ ์์ญ ๋๋ ์๋ฐฑ๊ฐ์ผ ๊ฒฝ์ฐ์ ๊ทธ๋๋ง๋ค ๋ก๊น
์ ์ ์ด์ค์ผ ํ๋ ๋ถํธํจ์ด ์์ ์ ์๋ค. ๋ํ ์์นซ ๊น๋ฐํ๊ณ ๋ก๊น
์ ๋นผ๋จน๊ณ ๋ฐฐํฌ๋ฅผ ํ๊ฒ ๋ ๊ฒฝ์ฐ ๋ชจ๋ํฐ๋ง์ ๋ก๊น
์ ํ์ง ์์์ ๋ค์ ๋ก๊น
ํ๊ณ ๋ฐฐํฌ๋ฅผ ํ๋, ๋ณ๊ฒ๋ ์๋๋ฐ(?) “์ ๋ง ๋ถํธํ” ์ํฉ์ด ์์ ์ ์๋ค.
์ด๋ฒ ํฌ์คํ
์์๋ ์ฌ์ฉ์์ ์์ฒญ์ ๋ชจ๋ํฐ๋ง ํ๊ธฐ ์ํด ์ปจํธ๋กค๋ฌ๋ง๋ค ์ฝ๋๋ฅผ ์์ฑํด๊ฐ๋ฉฐ ๋ก๊น
์ ํ๋๊ฒ์ด ์๋๋ผ HttpServletRequestWrapper
๋ผ๋ ๊ฒ๊ณผ Filter
, AOP
๋ฅผ ์ด์ฉํ์ฌ Request์ ์ ๋ณด๋ฅผ ํ๊ณณ์์ ์ฐ์ํ๊ฒ ๋ก๊น
ํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์๋ณด๊ณ ์ ํ๋ค.
์๊ตฌ์ฌํญ

์ถ์ฒ : https://gfycat.com/ko/brightevilaoudad
ํฌ์ฐ์ฌ๊ฐ ํ๋๋ ๋นจ๊ฐ ์ฒ์ ๋ณด๋ฉฐ ๋์งํ๋ ํฉ์์ฒ๋ผ (์ฐ๊ณ ๋ณด๋ ๋๋ฌด TMI ๊ฐ๋ค….) ๋น์ฅ ์ฝ๋ฉ์ ์์ํ๋ฉฐ ๊ฐ๋ฐ์ ํ ์๋ ์์ง๋ง ์ ์ ์ํ๋ ๊ธฐ๋ฅ์ด ๋ฌด์์ธ์ง ์ฒ์ฒํ ์ ๋ฆฌํ๊ณ ๋์ด๊ฐ ํ์๊ฐ ์๋ ๊ฒ ๊ฐ๋ค. (์ด์ฉ๋ ์คํ๋ ค ํ์๊ฐ ๋ ๋น ๋ฅธ ๊ฐ๋ฐ์ ํ๊ฒ ๋๋๊ฒ ๊ฐ๋ค.)
- GET, POST ๋ฑ ๋ค์ํ http method ๋ก ๊ตฌํ๋ ๋ชจ๋ ์ปจํธ๋กค๋ฌ์ ํ๋ผ๋ฏธํฐ์ ๊ธฐํ Request ์ ๋ณด๊ฐ ๋ก๊น ์ด ๋์ผ ํ๋ค.
- ์ปจํธ๋กค๋ฌ, ๋ฉ์๋๊ฐ ๋์ด๋ ๋๋ง๋ค ๋ณ๋์ ์ฝ๋ ์ถ๊ฐ ์์ด ํ๊ณณ์์ ๊ณตํต์ ์ผ๋ก ๋ก๊น ์ด ๋์ผ ํ๋ค.
- URL ์ค ํน์ ํจํด์ผ๋ก ๋ค์ด์ค๋ ์์ฒญ์ ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ๋ก๊น ์ ํ๊ฑฐ๋, ๋ก๊น ์์ ์ ์ธํ ์ ์์ด์ผ ํ๋ค.
- ์์ ๋งํ๋ฏ ๋ค๋ฅธ ๋น์ง๋์ค ๋ก์ง์ ์ํฅ์ ์ฃผ์ง ์์์ผ ํ๋ค.
๊ตฌํํ๊ธฐ - Request ์ ํ๋ผ๋ฏธํฐ ์ ๋ฆฌ
Request ์ ๋ชจ๋ ๋ก๊น ์ ํ๊ณณ์์ ์ฒ๋ฆฌํ๊ธฐ ์ํด์ filter(ํํฐ)๋ฅผ ํ์ฉํ์๋ค. ํํฐ๋ Dispatcher servlet์ ์๋จ์ ์์นํ๊ณ ์๊ธฐ ๋๋ฌธ์ ๋ชจ๋ ์ ๋ณด๋ฅผ ํ์ธํ ์ ์๋๋ฐ ์ฉ์ดํ๋ค. ๋ฌผ๋ก ์ธํฐ์ ํฐ๋ฅผ ํ์ฉํด์๋ ๋ฐฉ๋ฒ์ด ์๊ฒ ์ง๋ง ๋ณธ ํฌ์คํ ์์๋ ํํฐ๋ฅผ ํ์ฉํด์ ๊ตฌํํ๋๊ฒ์ ๋ชฉ์ ์ผ๋ก ํ๋ค. (์ฌ์ค ์ธํฐ์ ํฐ๋ก ๋ช๋ฒ ์๋ํด๋ณด๋ค๊ฐ ์คํจํด์…์ ์ )

์ถ์ฒ : https://justforchangesake.wordpress.com/2014/05/07/spring-mvc-request-life-cycle/
Filter๋ฅผ ๋ง๋ค๊ธฐ ์ ์ Filter์์ ์ฌ์ฉํ ์ฃผ์ ํต์ฌ(?) ํด๋์ค๊ฐ ํ์ํ๋ฐ HttpServletRequest
๋ฅผ Wrapping ํด์ ์ฌ์ฉํ๊ธฐ ์ํด HttpServletRequestWrapper
๋ฅผ ์์๋ฐ๋ ํด๋์ค๋ฅผ ๋ง๋ค์. Request ์ ๋ด๊ฒจ์๋ param ๊ณผ body๋ก ์์ฒญ์ด ๋ค์ด์ฌ ๊ฒฝ์ฐ body์ ์๋ ๋ด์ฉ์ param ์ ๋ด๋ ๋ก์ง์ด๋ค. ์ฃผ์ ์ค๋ช
์ ์ฝ๋ ์์์ ์ฃผ์์ผ๋ก ์ค๋ช
ํ๊ฒ ๋ค.
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 {
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));
}
}
๊ฐ์ฅ ์ค์ํ ๋ถ๋ถ์ IOUtils.toByteArray(is)
์ ๋ถ๋ถ์ธ๋ฐ, InputStream์ ํ๋ฒ๋ฐ์ ์ฝ์ ์ ์๊ธฐ ๋๋ฌธ์ ์ด ํํฐ์์ ์คํธ๋ฆผ์ ์ฝ๋ ๋์ , ๋ํผ ๊ตฌํ์ผ๋ก ์ ์คํธ๋ฆผ ์์ฑํ๋๋ก ์์
์ ํ์๋ค. ์์นซ ์๋ชปํ๋ค๊ฐ body์ ๋ด์ฉ์ด ์ ์ค๋ ์๋ ์๊ธฐ ๋๋ฌธ์ด๋ค.
์ฐธ์กฐ :
http://stackoverflow.com/questions/10210645/http-servlet-request-lose-params-from-post-body-after-read-it-once
http://stackoverflow.com/questions/3769259/why-is-the-parameter-value-an-object-hash-code-for-request-getparametermap-ge