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