02.ETC

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 .. 파일만 λ“±λ‘ν•˜λ©΄ μžλ™μœΌλ‘œ 인식

  • μΆ”κ°€ μ˜΅μ…˜μ€ Spring-Boot Docs μ°Έκ³ 

  • /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 μ΄μ§€λ§Œ, μŠ€ν”„λ§μ€ νƒ€μž…μ„ λ³€ν™˜ν•΄ 제곡

    • @RequestParam

    • @ModelAttribute

    • @PathVariable

    • @Value

    • XML Spring Bean 정보 λ³€ν™˜

    • View Rendering

    • ...

    // @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: 문자(객체 ➜ 문자, 문자 ➜ 객체, ν˜„μ§€ν™”)에 νŠΉν™”

    • νŠΉλ³„ν•œ Converter..

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 전솑 방식

  • multipart/form-data

  • 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)

Last updated