ETC
๋ฉ์์ง, ๊ตญ์ ํ
๋ฉ์์ง ๊ธฐ๋ฅ
: ๋ค์ํ ๋ฉ์์ง๋ฅผ ํ ๊ณณ์์ ๊ด๋ฆฌํ๋ ๊ธฐ๋ฅ
messages.properteis
item=์ํ
item.id=์ํ ID
item.itemName=์ํ๋ช
item.price=๊ฐ๊ฒฉ
item.quantity=์๋
๊ตญ์ ํ ๊ธฐ๋ฅ
: ๋ฉ์์ง ํ์ผ์ ๊ฐ ๋๋ผ๋ณ๋ก ๋ณ๋๋ก ๊ด๋ฆฌํ๋ ๊ตญ์ ํ ๊ธฐ๋ฅ
messages_en.properties ์ ๊ฐ์ด ํ์ผ๋ช
๋ง์ง๋ง์ ์ธ์ด ์ ๋ณด ์ถ๊ฐ
์ฐพ์ ์ ์๋ ๊ตญ์ ํ ํ์ผ์ด ์์ผ๋ฉด messages.properties ๋ฅผ ๊ธฐ๋ณธ์ผ๋ก ์ฌ์ฉ
messages_en.propertis
item=Item
item.id=Item ID
item.itemName=Item Name
item.price=price
item.quantity=quantity
messages_ko.propertis
item=์ํ
item.id=์ํ ID
item.itemName=์ํ๋ช
item.price=๊ฐ๊ฒฉ
item.quantity=์๋
Spring Message Source
SpringBoot ๋ MessageSource ๋ฅผ ์๋์ผ๋ก ์คํ๋ง ๋น์ผ๋ก ๋ฑ๋ก
Spring ์ฌ์ฉ ์ ๊ตฌํ์ฒด์ธ ResourceBundleMessageSource ๋ฅผ ๋น์ผ๋ก ๋ฑ๋ก
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
// messages ์ง์ ์ messages.properties ํ์ผ์ ์ฝ์ด์ ์ฌ์ฉ
messageSource.setBasenames("messages", "errors");
messageSource.setDefaultEncoding("utf-8");
return messageSource;
}
SpringBoot Message Source ์ค์
application.properties
spring.messages.basename=messages,config.i18n.messages
์คํ๋ง ๋ถํธ ๋ฉ์์ง ์์ค ๊ธฐ๋ณธ ๊ฐ: spring.messages.basename=messages
MessageSource ๋ฅผ ์คํ๋ง ๋น ๋ฑ๋กํ์ง ์๊ณ , ์คํ๋ง ๋ถํธ ๊ด๋ จ ์ค์ ์ ํ์ง ์์ผ๋ฉด messages ๋ผ๋ ์ด๋ฆ์ผ๋ก ๊ธฐ๋ณธ ๋ฑ๋ก
๋ฐ๋ผ์ messages.properties, messages_en.properties .. ํ์ผ๋ง ๋ฑ๋กํ๋ฉด ์๋์ผ๋ก ์ธ์
/resources/messages.properties
๊ฒฝ๋ก์ Message ํ์ผ ์ ์ฅ
hello=์๋
hello.name=์๋
{0}
Message Source ์ฌ์ฉ
SpringBoot ๋ MessageSource ๋ฅผ ์๋์ผ๋ก Spring Bean ์ผ๋ก ๋ฑ๋กํ๋ฏ๋ก ๋ฐ๋ก ์ฌ์ฉ ๊ฐ๋ฅ
MessageSource ๋ message.properties ํ์ผ ์ ๋ณด๋ฅผ ๊ฐ์ง๊ณ ์์
@Autowired
MessageSource ms;
@Test
void helloMessage() {
// locale ์ ๋ณด๊ฐ ์์ผ๋ฉด basename ์์ ์ค์ ํ ๊ธฐ๋ณธ ์ด๋ฆ ๋ฉ์์ง ํ์ผ(messages.properties) ์กฐํ
String result = ms.getMessage("hello", null, null);
assertThat(result).isEqualTo("์๋
");
}
@Test
void notFoundMessageCode() {
// ๋ฉ์์ง๊ฐ ์๋ ๊ฒฝ์ฐ NoSuchMessageException ๋ฐ์
assertThatThrownBy(() -> ms.getMessage("no_code", null, null))
.isInstanceOf(NoSuchMessageException.class);
}
@Test
void notFoundMessageCodeDefaultMessage() {
// ๋ฉ์์ง๊ฐ ์์ด๋ defaultMessage ๋ฅผ ์ฌ์ฉํ๋ฉด ๊ธฐ๋ณธ ๋ฉ์์ง ๋ฐํ
String result = ms.getMessage("no_code", null, "๊ธฐ๋ณธ ๋ฉ์์ง", null);
assertThat(result).isEqualTo("๊ธฐ๋ณธ ๋ฉ์์ง");
}
@Test
void argumentMessage() {
// ๋ฉ์์ง์ {0} ๋ถ๋ถ์ ๋งค๊ฐ๋ณ์๋ฅผ ์ ๋ฌํด์ ์นํ
String result = ms.getMessage("hello.name", new Object[]{"Aaron"}, null);
assertThat(result).isEqualTo("์๋
Aaron");
}
Message Source ๊ตญ์ ํ ์ฌ์ฉ
locale ์ ๋ณด ๊ธฐ๋ฐ์ผ๋ก ๊ตญ์ ํ ํ์ผ ์ ํ
Locale ์ด en_US ์ผ ๊ฒฝ์ฐ messages_en_US โ messages_en โ messages(default) ์์ ํ์
@Test
void defaultLang() {
// locale ์ ๋ณด๊ฐ ์์ผ๋ฏ๋ก messages ์ฌ์ฉ
assertThat(ms.getMessage("hello", null, null)).isEqualTo("์๋
");
// locale ์ ๋ณด๊ฐ ์์ง๋ง, message_ko ๊ฐ ์์ผ๋ฏ๋ก messages ์ฌ์ฉ
assertThat(ms.getMessage("hello", null, Locale.KOREA)).isEqualTo("์๋
");
}
@Test
void enLang() {
// locale ์ ๋ณด๊ฐ Locale.ENGLISH ์ด๋ฏ๋ก messages_en ์ฌ์ฉ
assertThat(ms.getMessage("hello", null, Locale.ENGLISH)).isEqualTo("hello");
}
Web Application Message
๋ฉ์์ง ์ ์ฉ
ํ์๋ฆฌํ์ ๋ฉ์์ง ํํ์ #{...}
๋ฅผ ์ฌ์ฉํ๋ฉด ์คํ๋ง ๋ฉ์์ง๋ฅผ ํธ๋ฆฌํ๊ฒ ์กฐํ ๊ฐ๋ฅ
messages.properties
label.item=์ํ
hello.name=์๋
{0}
Thymeleaf
<div th:text="#{label.item}"></h2>
<p th:text="#{hello.name(${item.itemName})}"></p>
๊ตญ์ ํ ์ ์ฉ
์น ๋ธ๋ผ์ฐ์ ์ ์ธ์ด ์ค์ ๊ฐ์ด ๋ณํ๋ฉด ์์ฒญ์ Accept-Language ์ ๊ฐ์ด ๋ณ๊ฒฝ๋๊ณ , ์ด ์ ๋ณด๋ฅผ Spring ์ Locale ๋ก ์ธ์ํด ์๋์ผ๋ก ๊ตญ์ ํ ์ฒ๋ฆฌ
LocaleResolver
Spring ์ Locale ์ ํ ๋ฐฉ์์ ๋ณ๊ฒฝํ ์ ์๋๋ก LocaleResolver ์ธํฐํ์ด์ค ์ ๊ณต
Spring Boot ๋ ์ธ์ด ์ ํ ์ ๊ธฐ๋ณธ์ ์ผ๋ก Accept-Language ํค๋๊ฐ์ ํ์ฉํ๋ AcceptHeaderLocaleResolver ์ฌ์ฉ
Locale ์ ํ ๋ฐฉ์์ ๋ณ๊ฒฝํ๋ ค๋ฉด LocaleResolver ๊ตฌํ์ฒด๋ฅผ ๋ณ๊ฒฝํด์ ์ฟ ํค๋ ์ธ์
๊ธฐ๋ฐ์ Locale ์ ํ ๊ธฐ๋ฅ ์ฌ์ฉ
Spring Type Converter
์คํ๋ง ํ์
๋ณํ ์ ์ฉ ์
HTTP Query String ์ผ๋ก ์ ๋ฌ๋๋ ๋ฐ์ดํฐ๋ ๋ชจ๋ String Type ์ด์ง๋ง, ์คํ๋ง์ ํ์
์ ๋ณํํด ์ ๊ณต
XML Spring Bean ์ ๋ณด ๋ณํ
// @RequestParam
@GetMapping("/hello")
public String hello(@RequestParam Integer data) {}
// @ModelAttribute
@GetMapping("/hello")
public String hello(@ModelAttribute UserData data) {}
class UserData {
Integer data;
}
// @PathVariable
@GetMapping("/users/{userId}")
public String hello(@PathVariable("data") Integer data) {}
// @Value
@Value("${api.key}")
private String key;
Type Converter
Converter Interface
์คํ๋ง์ ์ฌ์ฉ์ ์ ์ ํ์
๋ณํ์ด ํ์ํ๋ฉด ์ปจ๋ฒํฐ ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํด์ ๋ฑ๋กํด ๋ณด์.
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
ex. ์ปจ๋ฒํฐ ์ธํฐํ์ด์ค ๊ตฌํ
public class StringToIntegerConverter implements Converter<String, Integer> {
@Override
public Integer convert(String source) {
return Integer.valueOf(source);
}
}
...
@Test
void stringToInteger() {
StringToIntegerConverter converter = new StringToIntegerConverter();
Integer result = converter.convert("10");
assertThat(result).isEqualTo(10);
}
์คํ๋ง์ ์ฉ๋์ ๋ฐ๋ผ ๋ค์ํ ๋ฐฉ์์ ํ์
์ปจ๋ฒํฐ ์ ๊ณต
Converter
: ๊ธฐ๋ณธ ํ์
์ปจ๋ฒํฐ
ConverterFactory
: ์ ์ฒด ํด๋์ค ๊ณ์ธต ๊ตฌ์กฐ๊ฐ ํ์ํ ๊ฒฝ์ฐ
GenericConverter
: ์ ๊ตํ ๊ตฌํ, ๋์ ํ๋์ ์ ๋
ธํ
์ด์
์ ๋ณด ์ฌ์ฉ ๊ฐ๋ฅ
ConditionalGenericConverter
: ํน์ ์กฐ๊ฑด์ด ์ฐธ์ธ ๊ฒฝ์ฐ์๋ง ์คํ
๊ทธ๋ฐ์ ๋ฌธ์, ์ซ์, boolean, Enum ๋ฑ ์ผ๋ฐ์ ์ธ ํ์
์ ๋ํ ๋๋ถ๋ถ์ ์ปจ๋ฒํฐ๋ฅผ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณต
Spring Type Conversion
ConversionService
๊ฐ๋ณ ์ปจ๋ฒํฐ๋ฅผ ๋ชจ์๋๊ณ , ๊ทธ๊ฒ๋ค์ ๋ฌถ์ด์ ํธ๋ฆฌํ๊ฒ ์ฌ์ฉํ ์ ์๋ ๊ธฐ๋ฅ
์คํ๋ง์ @RequestParam ๊ฐ์ ๊ณณ ๋ด๋ถ์์ ConversionService ๋ฅผ ์ฌ์ฉํด์ ํ์
์ ๋ณํ
ConversionService interface
Converting ๊ฐ๋ฅ ์ฌ๋ถ์ ๊ธฐ๋ฅ ์ ๊ณต
package org.springframework.core.convert;
import org.springframework.lang.Nullable;
public interface ConversionService {
boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
@Nullable
<T> T convert(@Nullable Object source, Class<T> targetType);
@Nullable
Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
}
DefaultConversionService
ConversionService ์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด(์ปจ๋ฒํฐ๋ฅผ ๋ฑ๋กํ๋ ๊ธฐ๋ฅ๋ ์ ๊ณต)
์ฌ์ฉ ์ด์ ์ ConversionService
์ ๋ฑ๋ก ์ด์ ์ ConverterRegistry
๋ก ๋ถ๋ฆฌ๋์ด ๊ตฌํ
์ธํฐํ์ด์ค ๋ถ๋ฆฌ ์์น ์ ์ฉ(ISP
-Interface Segregation Principal)
์ธํฐํ์ด์ค ๋ถ๋ฆฌ๋ฅผ ํตํด ์ปจ๋ฒํฐ๋ฅผ ์ฌ์ฉํ๋ ํด๋ผ์ด์ธํธ์ ์ปจ๋ฒํฐ๋ฅผ ๋ฑ๋กํ๊ณ ๊ด๋ฆฌํ๋ ํด๋ผ์ด์ธํธ์ ๊ด์ฌ์ฌ๋ฅผ ๋ช
ํํ๊ฒ ๋ถ๋ฆฌ
.
ํ์
์ปจ๋ฒํฐ๋ค์ ์ปจ๋ฒ์ ์๋น์ค ๋ด๋ถ์ ์จ์ด์ ์ ๊ณต๋๋ฏ๋ก, ํด๋ผ์ด์ธํธ๋ ํ์
์ปจ๋ฒํฐ๋ฅผ ๋ชฐ๋ผ๋ ๋ฌด๊ด
ํ์
๋ณํ์ ์ํ๋ ํด๋ผ์ด์ธํธ์ ๊ฒฝ์ฐ ์ปจ๋ฒ์ ์๋น์ค ์ธํฐํ์ด์ค์๋ง ์์กด
์ปจ๋ฒ์ ์๋น์ค๋ฅผ ๋ฑ๋กํ๋ ๋ถ๋ถ๊ณผ ์ฌ์ฉํ๋ ๋ถ๋ถ์ ๋ถ๋ฆฌํ๊ณ ์์กด๊ด๊ณ ์ฃผ์
์ ์ฌ์ฉ
@Test
void conversionService() {
// Converter ๋ฑ๋ก
DefaultConversionService conversionService = new DefaultConversionService();
conversionService.addConverter(new StringToIntegerConverter());
conversionService.addConverter(new IntegerToStringConverter());
// ConverterService ์ฌ์ฉ
assertThat(conversionService.convert("10", Integer.class)).isEqualTo(10);
assertThat(conversionService.convert(10, String.class)).isEqualTo("10");
}
Apply Converter in Spring ๐
์คํ๋ง์ ๋ด๋ถ์์ ConversionService ์ ๊ณต
WebMvcConfigurer ๊ฐ ์ ๊ณตํ๋ addFormatters()
๋ฅผ ์ฌ์ฉํด์ ์ปจ๋ฒํฐ ๋ฑ๋ก
@RequestParam ์ ๊ฒฝ์ฐ RequestParamMethodArgumentResolver ์์ ConversionService ๋ฅผ ์ฌ์ฉํด์ ํ์
์ ๋ณํ
WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToIntegerConverter());
registry.addConverter(new IntegerToStringConverter());
}
}
...
@GetMapping("/hello-v2")
public String helloV2(@RequestParam Integer data) {
return "ok";
}
Apply Converter in View Template
ํ์๋ฆฌํ๋ ๋ ๋๋ง ์ ์ปจ๋ฒํฐ๋ฅผ ์ ์ฉํด์ ๋ ๋๋ง ํ๋ ๋ฐฉ๋ฒ์ ํธ๋ฆฌํ๊ฒ ์ง์
View Template
Controller.java
@GetMapping("/view")
public String converterView(Model model) {
model.addAttribute("number", 10000);
return "view";
}
view.html
๋ณ์ ํํ์ : ${...}
์ปจ๋ฒ์ ์๋น์ค ์ ์ฉ : ${{...}}
<li>${number}: <span th:text="${number}" ></span></li>
<li>${{number}}: <span th:text="${{number}}" ></span></li>
Form
Controller.java
@ModelAttribute ๋ด๋ถ์์ ConversionService ๋์
@GetMapping("/converter/edit")
public String converterForm(Model model) {
IpPort ipPort = new IpPort("127.0.0.1", 8080);
Form form = new Form(ipPort);
model.addAttribute("form", form);
return "form";
}
@PostMapping("/converter/edit")
public String converterEdit(@ModelAttribute Form form, Model model) {
IpPort ipPort = form.getIpPort();
model.addAttribute("ipPort", ipPort);
return "view";
}
form.html
th:field ๋ Converter ๊น์ง ์๋ ์ ์ฉ
th:value ๋ ๋ณด์ฌ์ฃผ๋ ์ฉ๋
<form th:object="${form}" th:method="post">
th:field <input type="text" th:field="*{ipPort}" /><br />
th:value <input type="text" th:value="*{ipPort}" /><br />
<input type="submit" />
</form>
Formatter
๊ฐ์ฒด๋ฅผ ํน์ ํ ํฌ๋ฉง์ ๋ง์ถ์ด ๋ฌธ์๋ก ์ถ๋ ฅํ๊ฑฐ๋, ๊ทธ ๋ฐ๋์ ์ญํ ์ ํ๋ ๊ฒ์ ํนํ๋ ๊ธฐ๋ฅ
Converter
: ๋ฒ์ฉ(๊ฐ์ฒด โ ๊ฐ์ฒด)์ ์ฌ์ฉ
Formatter
: ๋ฌธ์(๊ฐ์ฒด โ ๋ฌธ์, ๋ฌธ์ โ ๊ฐ์ฒด, ํ์งํ)์ ํนํ
Formatter Interface
public interface Printer<T> { // ๊ฐ์ฒด โ ๋ฌธ์
String print(T object, Locale locale);
}
public interface Parser<T> { // ๋ฌธ์ โ ๊ฐ์ฒด
T parse(String text, Locale locale) throws ParseException;
}
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
implements Formatter
public class NumberFormatter implements Formatter<Number> {
@Override
public Number parse(String text, Locale locale) throws ParseException {
NumberFormat format = NumberFormat.getInstance(locale);
return format.parse(text);
}
@Override
public String print(Number object, Locale locale) {
return NumberFormat.getInstance(locale).format(object);
}
}
...
class MyNumberFormatterTest {
MyNumberFormatter formatter = new MyNumberFormatter();
@Test
void parse() throws ParseException {
Number result = formatter.parse("1,000", Locale.KOREA);
assertThat(result).isEqualTo(1000L);
}
@Test
void print() {
String result = formatter.print(1000, Locale.KOREA);
assertThat(result).isEqualTo("1,000");
}
}
์คํ๋ง์ ์ฉ๋์ ๋ฐ๋ผ ๋ค์ํ ๋ฐฉ์์ ํฌ๋งทํฐ ์ ๊ณต
AnnotationFormatterFactory: ํ๋์ ํ์
์ด๋ ์ ๋
ธํ
์ด์
์ ๋ณด๋ฅผ ํ์ฉํ ์ ์๋ ํฌ๋งทํฐ
FormattingConversionService
ConverstionService ์๋ ์ปจ๋ฒํฐ๋ง ๋ฑ๋ก ๊ฐ๋ฅํ๊ณ , ํฌ๋งทํฐ๋ ๋ฑ๋ก ๋ถ๊ฐ
ํฌ๋งทํฐ ๋ฑ๋ก์ ์ง์ํ๋ FormattingConversionService
๋ฅผ ์ฌ์ฉํ์ฌ ํฌ๋งทํฐ๋ฅผ ์ถ๊ฐํด ๋ณด์.
๋ด๋ถ์์ ์ด๋ํฐ ํจํด์ ์ฌ์ฉํด์ Formatter ๊ฐ Converter ์ฒ๋ผ ๋์ํ๋๋ก ์ง์
DefaultFormattingConversionService
๋ FormattingConversionService ๋ฅผ ์์๋ฐ์ ๊ธฐ๋ณธ์ ์ธ ํตํ, ์ซ์ ๊ด๋ จ ๊ธฐ๋ณธ ํฌ๋งทํฐ๋ฅผ ์ถ๊ฐ ์ ๊ณต
ConversionService ๊ด๋ จ ๊ธฐ๋ฅ์ ์์๋ฐ์ผ๋ฏ๋ก Converter, Formatter ๋ชจ๋ ๋ฑ๋ก ๊ฐ๋ฅ
์คํ๋ง ๋ถํธ๋ DefaultFormattingConversionService ๋ฅผ ์์ ๋ฐ์ WebConversionService
๋ฅผ ๋ด๋ถ์์ ์ฌ์ฉ
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
// ์ปจ๋ฒํฐ ๋ฑ๋ก
conversionService.addConverter(new StringToIpPortConverter());
conversionService.addConverter(new IpPortToStringConverter());
// ํฌ๋งทํฐ ๋ฑ๋ก
conversionService.addFormatter(new MyNumberFormatter());
// ์ปจ๋ฒํฐ ์ฌ์ฉ
IpPort ipPort = conversionService.convert("127.0.0.1:8080", IpPort.class);
assertThat(ipPort).isEqualTo(new IpPort("127.0.0.1", 8080));
// ํฌ๋งทํฐ ์ฌ์ฉ
assertThat(conversionService.convert(1000, String.class)).isEqualTo("1,000");
assertThat(conversionService.convert("1,000", Long.class)).isEqualTo(1000L);
Apply Formatter in Spring ๐
๊ธฐ๋ฅ์ด ๊ฒน์น ๊ฒฝ์ฐ(Source-type, Target-type ๋์ผ) Converter ์ฐ์
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToIpPortConverter());
registry.addConverter(new IpPortToStringConverter());
registry.addFormatter(new MyNumberFormatter());
}
}
Annotation driven Formatting
์คํ๋ง์ ์๋ฐ์์ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ๋ ํ์
๋ค์ ๋ํด ์๋ง์ ํฌ๋งทํฐ๋ฅผ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณต
๊ฐ์ฒด์ ๊ฐ ํ๋๋ง๋ค ๋ค๋ฅธ ํ์์ ํฌ๋งท์ ์ง์ ํ๋ ์ด๋ ค์์ ํด๊ฒฐํ๊ธฐ ์ํด ์ ๋
ธํ
์ด์
๊ธฐ๋ฐ ํ์ ์ง์ ํฌ๋งทํฐ ์ ๊ณต
@NumberFormat
: ์ซ์ ๊ด๋ จ ํ์ ์ง์ ํฌ๋งทํฐ ์ฌ์ฉ
NumberFormatAnnotationFormatterFactory
@DateTimeFormat
: ๋ ์ง ๊ด๋ จ ํ์ ์ง์ ํฌ๋งทํฐ ์ฌ์ฉ
Jsr310DateTimeFormatAnnotationFormatterFactory
@Getter
@Setter
static class Form {
@NumberFormat(pattern = "###,###")
private Integer number;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime localDateTime;
}
Annotation-driven Formatting
์ฐธ๊ณ ,
HttpMessageConverter ์๋ Convetion Service ๊ฐ ์ ์ฉ๋์ง ์์
JSON ์ ๊ฐ์ฒด๋ก ๋ณํํ๋ HttpMessageConverter ๋ ๋ด๋ถ์์ Jackson ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉ
๋ฐ๋ผ์, JSON ๊ฒฐ๊ณผ๋ก ๋ง๋ค์ด์ง๋ ์ซ์๋ ๋ ์ง ํฌ๋งท์ ๋ณ๊ฒฝํ๊ณ ์ถ์ผ๋ฉด ํด๋น ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ ๊ณตํ๋ ์ค์ ์ ํตํด์ ํฌ๋งท์ ์ง์
File Upload
์ ์ก ๋ฐฉ์
๊ธฐ๋ณธ์ ์ธ HTML Form ์ ์ก ๋ฐฉ์
application/x-www-form-urlencoded
HTML Form
<form action="/save" method="post">
<inpout type="text" name="username" />
<inpout type="text" name="age" />
<button type="submit">์ ์ก</button>
</form>
HTTP Message
HTTP/1.1 200 OK
POST /save HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
username=kim&age=20
Form ๋ด์ฉ๊ณผ ์ฌ๋ฌ ํ์ผ์ ํจ๊ป ์ ์กํ๋ HTML Form ์ ์ก ๋ฐฉ์
HTML Form
form tag ์ enctype="multipart/form-data" ์ง์
<form action="/save" method="post" enctype="multipart/form-data">
<inpout type="text" name="username" />
<inpout type="text" name="age" />
<inpout type="file" name="file1" />
<button type="submit">์ ์ก</button>
</form>
HTTP Message
๊ฐ๊ฐ์ ์ ์ก ํญ๋ชฉ์ด ๊ตฌ๋ถ
Content-Disposition ๋ผ๋ ํญ๋ชฉ๋ณ ํค๋์ ๋ถ๊ฐ ์ ๋ณด๊ฐ ๋ถ๋ฆฌ
HTTP/1.1 200 OK
POST /save HTTP/1.1
Host: localhost:8080
Content-Type: multipart/form-data; boundary=----XXX
Content-Length: 10457
----XXX
Content-Disposition: form-data; name="username"
Kim
----XXX
Content-Disposition: form-data; name="age"
20
----XXX
Content-Disposition: form-data; name="file1"; filename="sample.jpg"
Content-Type: image/png
102941as9d86f7aa9807sd6fas987df6...
----XXX--
HTTP ๋ฉ์์ง ์ฐธ๊ณ
Servlet File Upload
Multipart ๊ด๋ จ ์ค์
# HTTP ์์ฒญ ๋ฉ์์ง ํ์ธ
logging.level.org.apache.coyote.http11=debug
# ํ์ผ ์
๋ก๋ ๊ฒฝ๋ก ์ค์
file.dir=C:/Users/Aaron/file/
# ์
๋ก๋ ์ฌ์ด์ฆ ์ ํ (์ฌ์ด์ฆ ์ด๊ณผ ์ SizeLimitExceededException ์์ธ ๋ฐ์)
# max-file-size : ํ์ผ ํ๋ ์ฌ์ด์ฆ (default > 1MB)
# max-request-size : ์ฌ๋ฌ ํ์ผ ์์ฒญ์ ๊ฒฝ์ฐ ์ ์ฒด ์ฌ์ด์ฆ (default > 10MB)
spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB
# Multipart ๋ฐ์ด์ฒ ์ฒ๋ฆฌ ์ฌ๋ถ (default > true)
spring.servlet.multipart.enabled=true
multipart.enabled ์ต์
์ด ์ผ์ ธ ์๋ค๋ฉด, Spring DispatcherServlet
์์ MultipartResolver
์คํ
multipart ์์ฒญ์ธ ๊ฒฝ์ฐ Servlet Container ๊ฐ ์ ๋ฌํ๋ HttpServletRequest
๋ฅผ MultipartHttpServletRequest
๋ก ๋ณํํด์ ๋ฐํ
Spring ์ด ์ ๊ณตํ๋ ๊ธฐ๋ณธ MultipartResolver
๋ MultipartHttpServletRequest
Interface ๋ฅผ ๊ตฌํํ StandardMultipartHttpServletRequest
๋ฅผ ๋ฐํ
ServletUploadController.java
@Slf4j
@Controller
@RequestMapping("/servlet/")
public class ServletUploadControllerV2 {
/**
* properties ์ค์ ๊ฐ ์ฃผ์
*/
@Value("${file.dir}")
private String fileDir;
@GetMapping("/upload")
public String newFile() {
return "upload-form";
}
@PostMapping("/upload")
public String saveFile(HttpServletRequest request) throws ServletException, IOException {
log.info("request={}", request);
String itemName = request.getParameter("itemName");
log.info("itemName={}", itemName);
/**
* Multipart ํ์์ ์ ์ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ Part ๋ก ๋๋์ด ์ ์ก
*/
Collection<Part> parts = request.getParts();
log.info("parts={}", parts);
for (Part part : parts) {
log.info("==== PART ====");
log.info("name={}", part.getName());
Collection<String> headerNames = part.getHeaderNames();
for (String headerName : headerNames) {
log.info("header {}: {}", headerName, part.getHeader(headerName));
}
/*
*ํธ์ ๋ฉ์๋
*/
//Content-Disposition: form-data; name="file"; filename="image.png"
//Content-Type: image/png
log.info("submittedFileName={}", part.getSubmittedFileName()); // ํด๋ผ์ด์ธํธ๊ฐ ์ ๋ฌํ ํ์ผ๋ช
log.info("size={}", part.getSize()); //part body size
//๋ฐ์ดํฐ ์ฝ๊ธฐ
InputStream inputStream = part.getInputStream(); // Part์ ์ ์ก ๋ฐ์ดํฐ ์ฝ๊ธฐ
String body = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("body={}", body);
//ํ์ผ์ ์ ์ฅํ๊ธฐ
if (StringUtils.hasText(part.getSubmittedFileName())) {
String fullPath = fileDir + part.getSubmittedFileName();
log.info("ํ์ผ ์ ์ฅ fullPath={}", fullPath);
part.write(fullPath); // Part๋ฅผ ํตํด ์ ์ก๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅ
}
}
return "upload-form";
}
}
request=org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@2b82974a
itemName=Spring
parts=[org.apache.catalina.core.ApplicationPart@367a8c9f, org.apache.catalina.core.ApplicationPart@33180a33]
==== PART ====
name=itemName
header content-disposition: form-data; name="itemName"
submittedFileName=null
size=6
body=Spring
==== PART ====
name=file
header content-disposition: form-data; name="file"; filename="image.png"
header content-type: image/png
submittedFileName=image.png
size=191492
body=๏ฟฝPNG
...
...
Spring File Upload ๐
์คํ๋ง์ MultipartFile
Interface ๋ก Multipart File ์ ๋งค์ฐ ํธ๋ฆฌํ๊ฒ ์ง์
@PostMapping("/upload")
public String saveFile(@RequestParam String itemName,
@RequestParam MultipartFile file, HttpServletRequest request) throws IOException {
if (!file.isEmpty()) {
String fullPath = fileDir + file.getOriginalFilename(); //์
๋ก๋ ํ์ผ ๋ช
log.info("ํ์ผ ์ ์ฅ fullPath={}", fullPath);
file.transferTo(new File(fullPath)); //ํ์ผ ์ ์ฅ
}
return "upload-form";
}
File Upload And Download
์์ ๋ก ๊ตฌํํ๋ ํ์ผ ์
๋ก๋, ๋ค์ด๋ก๋ (1)
์์ ๋ก ๊ตฌํํ๋ ํ์ผ ์
๋ก๋, ๋ค์ด๋ก๋ (2)