JiwonDev

Spring ํƒ€์ž… ์ปจ๋ฒ„ํ„ฐ (Converter, Formatter)

by JiwonDev

# ํƒ€์ž… ์ปจ๋ฒ„ํ„ฐ์˜ ํ•„์š”์„ฑ

HTTP์˜ ์š”์ฒญ ํŒŒ๋ผ๋ฉ”ํƒ€๋Š” ๋ชจ๋‘ ๋ฌธ์ž์—ด๋กœ ์ฒ˜๋ฆฌ๋œ๋‹ค. ๋งŒ์•ฝ ์ˆซ์ž๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ํƒ€์ž…์„ ๋ณ€ํ™˜ํ•ด์•ผํ•œ๋‹ค.

  @GetMapping("/hello-v1")
  public String helloV1(HttpServletRequest request) {
    String data = request.getParameter("data");//๋ฌธ์ž ํƒ€์ž… ์กฐํšŒ
    Integer intValue = Integer.valueOf(data); //์ˆซ์ž ํƒ€์ž…์œผ๋กœ ๋ณ€๊ฒฝ
    
    return "ok";
  }

 

ํ•˜์ง€๋งŒ ์Šคํ”„๋ง์—์„œ๋Š” @์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ด์„œ HTTP ํŒŒ๋ผ๋ฉ”ํƒ€๋ฅผ ์›ํ•˜๋Š” ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ๋‹ค.

  @GetMapping("/hello-v2")
  public String helloV2(@RequestParam Integer data) {
    System.out.println("data = " + data);
    return "ok";
  }
  
  
  @PostMapping("/user/add")
  public String search(@ModelAttribute User user, BindingResult result) {
    
    if (result.hasError()) {
      return "/user/add"; // add.html์— ๋ชจ๋ธ(User)๊ณผ BindingResult๊ฐ€ ์ „๋‹ฌ๋œ๋‹ค.
    }
    userService.add(user);
    return "/home";
  }

 

@RequestParm, @ModelAttribute, @PathVariable ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ, "/home"์„ ๋ทฐ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜๊ฑฐ๋‚˜ ๊ฐ์ฒด ์ •๋ณด๋ฅผ JSON์œผ๋กœ ์ „์†กํ•˜๋Š” ํ• ์ˆ˜๋„ ์žˆ๊ณ  ์ข€ ๋” ์ถ”์ƒ์ ์ธ ์ •๋ณด (IP, Port)๋ฅผ ๋ณ€ํ™˜ํ•˜๋Š” ๋“ฑ ์ •๋ง ์ˆ˜๋งŽ์€ ๊ณณ์—์„œ ํƒ€์ž… ๋ณ€ํ™˜์ด ๋ฐœ์ƒํ•œ๋‹ค.

 

 

์ด๋Š” ์Šคํ”„๋ง์—์„œ ๋‹ค์–‘ํ•œ ํƒ€์ž… ์ปจ๋ฒ„ํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ  ์ด๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๋งŒ์•ฝ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ƒˆ๋กœ์šด ํƒ€์ž…์„ ๋งŒ๋“ค์–ด์„œ Converter๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•ด์•ผ ํ• ๊นŒ?

 

์ฐธ๊ณ ๋กœ ์Šคํ”„๋ง3.0 ์ด์ „์—๋Š” ์ž๋ฐ”์˜ PropertyEditor ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•ด ํƒ€์ž… ๋ณ€ํ™˜์„ ํ–ˆ์—ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋Š” ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ์žˆ์–ด ํƒ€์ž… ๋ณ€ํ™˜ํ•  ๋•Œ ๋งˆ๋‹ค ๊ฐ์ฒด๋ฅผ ๊ณ„์†ํ•ด์„œ ์ƒ์„ฑํ•ด์ค˜์•ผ ํ•˜๋Š” ๋ฌธ์ œ์ ์ด ์žˆ์—ˆ๋‹ค.

 

 


# ์Šคํ”„๋ง์˜ Converter ์ธํ„ฐํŽ˜์ด์Šค

  • ์Šคํ”„๋ง์—์„œ๋Š” ํƒ€์ž… ์ปจ๋ฒ„ํ„ฐ๋ฅผ ๋งŒ๋“ค๊ธฐ์œ„ํ•ด ์•„๋ž˜์™€ ๊ฐ™์€ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ œ๊ณตํ•ด์ค€๋‹ค.
package org.springframework.core.convert.converter;

  public interface Converter<S, T> {

    T convert(S source);
  }

 

  • ์ด ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ด์šฉํ•ด์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ๊ฐ„๋‹จํ•˜๊ฒŒ ์ปจ๋ฒ„ํ„ฐ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.
public class IntegerToStringConverter implements Converter<Integer, String> {
  // ๋‚ด๊ฐ€ ๋งŒ๋“  Integer -> String ์ปจ๋ฒ„ํ„ฐ
  @Override
  public String convert(Integer source) {
    return String.valueOf(source);
  }
}
public class StringToIntegerConverter implements Converter<String, Integer> {
  // ๋งŒ์•ฝ ๋ฐ˜๋Œ€๋กœ ์ „ํ™˜ํ•˜๋Š” ๊ธฐ๋Šฅ๋„ ํ•„์š”ํ•˜๋‹ค๋ฉด ์ด๋ ‡๊ฒŒ ํ•˜๋‚˜ ๋” ๋งŒ๋“ค์–ด ์ฃผ๋ฉด ๋œ๋‹ค.
  @Override
  public Integer convert(String source) {
    return Integer.valueOf(source);
  }
}

 

  • ์ปจ๋ฒ„ํ„ฐ๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค๋ฉด ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€ ๋ฐ˜๋“œ์‹œ ํ…Œ์ŠคํŠธํ•ด๋ด์•ผํ•œ๋‹ค.
public class ConverterTest {
  @Test
  void stringToInteger() {
    StringToIntegerConverter converter = new StringToIntegerConverter();
    Integer result = converter.convert("10");
    assertThat(result).isEqualTo(10);
  }

  @Test
  void IntegerToString() {
    IntegerToStringConverter converter = new IntegerToStringConverter();
    String result = converter.convert(10);
    assertThat(result).isEqualTo("10");
  }
}

 


# IpPort ๊ฐ์ฒด ์ปจ๋ฒ„ํ„ฐ ๋งŒ๋“ค๊ธฐ

String <-> Integer ์ปจ๋ฒ„ํ„ฐ๋Š” ๊ตณ์ด ๋งŒ๋“ค์–ด์•ผํ•˜๋‚˜..? ์˜๋ฌธ์ด ์ƒ๊ธธ ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฒˆ์—๋Š” ์ข€ ๋” ๋ณต์žกํ•œ ์˜ˆ์ œ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.

@Getter
@EqualsAndHashCode // ๋น„๊ต๋ฅผ ์œ„ํ•œ lombok ์ฝ”๋“œ ์ƒ์„ฑ
public class IpPort {
    private String ip;
    private int port;

    public IpPort(String ip, int port) {
        this.ip = ip;
        this.port = port;
    }
}
๋”๋ณด๊ธฐ
  • ์œ„์˜ IpPort ๊ฐ์ฒด๋ฅผ String์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ปจ๋ฒ„ํ„ฐ๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค.
@Slf4j
public class IpPortToStringConverter implements Converter<IpPort, String> {
    @Override
    public String convert(IpPort source) {
        //IpPort ๊ฐ์ฒด -> "127.0.0.1:8080"
        return source.getIp() + ":" + source.getPort();
    }
}
@Slf4j
public class StringToIpPortConverter implements Converter<String, IpPort> {

    @Override
    public IpPort convert(String source) {
        //"127.0.0.1:8080" -> IpPort ๊ฐ์ฒด
        String[] split = source.split(":");
        String ip = split[0];
        int port = Integer.parseInt(split[1]);
        return new IpPort(ip, port);
    }
}

 

  • ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ด๋ณด์ž.
    public class ConverterTest {
      @Test
      void stringToIpPort() {
        IpPortToStringConverter converter = new IpPortToStringConverter();
        IpPort source = new IpPort("127.0.0.1", 8080);
        String result = converter.convert(source);
        assertThat(result).isEqualTo("127.0.0.1:8080");
      }
    
      @Test
      void ipPortToString() {
        StringToIpPortConverter converter = new StringToIpPortConverter();
        String source = "127.0.0.1:8080";
        IpPort result = converter.convert(source);
        assertThat(result).isEqualTo(new IpPort("127.0.0.1", 8080));
      }
    }

 

 


# ์ปจ๋ฒ„ํ„ฐ๋ฅผ ๋งŒ๋“ค๊ณ  ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ณผ์ •์€ ๋ณต์žกํ•˜๋‹ค.

๋งค๋ฒˆ ์ปจ๋ฒ„ํ„ฐ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ , ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ณผ์ •์€ ์ด์ฒ˜๋Ÿผ ๋ณต์žกํ•˜๋‹ค. ๋˜ํ•œ ๋งŒ๋“ค์–ด๋‘” ์ปจ๋ฒ„ํ„ฐ๋Š” ํ”„๋กœ์ ํŠธ ์ „์ฒด์—์„œ ์“ฐ์ด๊ธฐ์— ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค. ์ปจ๋ฒ„ํ„ฐ๋ฅผ ๋งŒ๋“ค๊ณ , ํ…Œ์ŠคํŠธํ•˜๊ณ  ์‚ฌ์šฉํ•˜๋Š”๊ฑธ ํ•œ๋ฒˆ์— ๊ด€๋ฆฌํ•  ์ˆ˜๋Š” ์—†์„๊นŒ?

 

โžก ์Šคํ”„๋ง์˜ ConversionService

ConverionService์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ฐ์ฒด

์ด๋ฏธ ์•Œ๊ณ ์žˆ๊ฒ ์ง€๋งŒ, ์Šคํ”„๋ง์˜ ConversionService์—์„œ๋Š” ์„œ๋น„์Šค์—์„œ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ํƒ€์ž… ์ปจ๋ฒ„ํ„ฐ๋“ค์ด ์ด๋ฏธ ๋“ฑ๋ก๋˜์–ด์žˆ๋‹ค.

"StringTo..."๋งŒ ๊ฒ€์ƒ‰ํ•˜๋”๋ผ๋„ 70๊ฐœ๊ฐ€ ๋„˜๋Š” ์ปจ๋ฒ„ํ„ฐ๊ฐ€ ์กด์žฌํ•˜๋Š” ๊ฑธ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 


# ConversionService ์ธํ„ฐํŽ˜์ด์Šค

์Šคํ”„๋ง์—์„œ๋Š” ์ปจ๋ฒ„ํ„ฐ๋ฅผ ๋“ฑ๋กํ•˜๊ณ  ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ConversionService ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ œ๊ณตํ•œ๋‹ค. ์Šคํ”„๋ง์—์„œ ๊ธฐ๋ณธ์œผ๋กœ ์ œ๊ณตํ•ด์ฃผ๋Š” ์ปจ๋ฒ„ํ„ฐ๋“ค์ด ๋งŽ์•„ ์–ด์ง€๊ฐ„ํ•˜๋ฉด ์ง์ ‘ ๋งŒ๋“ค์ผ์€ ์—†์ง€๋งŒ, ๋งŒ๋“ค๊ณ  ์‹ถ๋‹ค๋ฉด ์ด ๋งํฌ๋ฅผ ์ฐธ์กฐํ•˜์ž.

  • ConversionService๋Š” ์‚ฌ์šฉ์— ์ดˆ์ ์„ ๋งž์ถ˜ ์ธํ„ฐํŽ˜์ด์Šค์ด๋‹ค. ํด๋ผ์ด์–ธํŠธ๋Š” ์ปจ๋ฒ„ํ„ฐ ๊ตฌํ˜„์ฒด์— ์˜์กดํ•˜์ง€ ์•Š๋Š”๋‹ค.
public interface ConversionService {

  boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
  boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);

  <T> T convert(@Nullable Object source, Class<T> targetType);

  Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType,
      TypeDescriptor targetType);
}

 

  • ConverterRegistry๋Š” ๋“ฑ๋ก์— ์ดˆ์ ์„ ๋งž์ถ˜ ์ธํ„ฐํŽ˜์ด์Šค์ด๋‹ค. ์ปจ๋ฒ„ํ„ฐ ๊ตฌํ˜„์ฒด๋Š” ๊ตฌ์ฒด์ ์ธ ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.
  public interface ConverterRegistry {
    void addConverter(Converter<?, ?> converter);

    <S, T> void addConverter(Class<S> sourceType, Class<T> targetType,
        Converter<? super S, ? extends T> converter);

    void addConverter(GenericConverter converter);

    void addConverterFactory(ConverterFactory<?, ?> factory);

    void removeConvertible(Class<?> sourceType, Class<?> targetType);

  }

 

 

  • ์‚ฌ์šฉ๋ฒ•์€ ๊ฐ„๋‹จํ•˜๋‹ค. ์ธํ„ฐ์…‰ํ„ฐ์ฒ˜๋Ÿผ @Configuration๋ฅผ ํ†ตํ•ด ์Šคํ”„๋ง ConversionService์— ๋“ฑ๋กํ•˜๊ณ , ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.
     โžก ์Šคํ”„๋ง ๋‚ด์žฅ ์ปจ๋ฒ„ํ„ฐ์™€ ๋˜‘๊ฐ™์€ ๋™์ž‘(str->Integer)์„ ํ•œ๋‹ค๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด ์ง์ ‘ ์ถ”๊ฐ€ํ•œ ์ปจ๋ฒ„ํ„ฐ๋ฅผ ๋จผ์ € ์‚ฌ์šฉํ•œ๋‹ค.
@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void addFormatters(FormatterRegistry registry) {
    // ์Šคํ”„๋ง ๋‚ด๋ถ€์— ์žˆ๋Š” ConversionService์— ๋“ฑ๋ก๋œ๋‹ค.
    // ConversionService conversionService = new DefaultConversionService();
    
    // ์ง์ ‘ ์ถ”๊ฐ€ํ•œ ์ปจ๋ฒ„ํ„ฐ๋Š”, ์Šคํ”„๋ง ๊ธฐ๋ณธ ์ปจ๋ฒ„ํ„ฐ๋ณด๋‹ค ๋†’์€ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ€์ง„๋‹ค.
    registry.addConverter(new StringToIntegerConverter());
    registry.addConverter(new IntegerToStringConverter());
    registry.addConverter(new StringToIpPortConverter());
    registry.addConverter(new IpPortToStringConverter());
  }
}

 

  • ์ด์ œ ์Šคํ”„๋ง์—์„œ ์‚ฌ์šฉํ•˜๋Š” @RequestPararm๋“ฑ์—์„œ ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ์ปจ๋ฒ„ํ„ฐ๊ฐ€ ์ถ”๊ฐ€๋กœ ๋™์ž‘ํ•œ๋‹ค.
     โžก ์ •ํ™•ํžˆ๋Š” ์Šคํ”„๋ง MVC์˜ ArumentResolver๊ฐ€ ๋“ฑ๋ก๋œ ConversionService ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ•ด ํƒ€์ž…์„ ๋ณ€ํ™˜ํ•œ๋‹ค.

 

  @GetMapping("/hello-v2") // registry.addConverter(new StringToIntegerConverter());
  public String helloV2(@RequestParam Integer data) {
    System.out.println("data = " + data);
    return "ok";
  }

  @GetMapping("/ip-port") // registry.addConverter(new StringToIpPortConverter());
  public String ipPort(@RequestParam IpPort ipPort) {
    System.out.println("ipPort IP = " + ipPort.getIp());
    System.out.println("ipPort PORT = " + ipPort.getPort());
    return "ok";
  }

 


# ํƒ€์ž„๋ฆฌํ”„(View Template)์— ์ปจ๋ฒ„ํ„ฐ ์‚ฌ์šฉํ•˜๊ธฐ

  • ํƒ€์ž„๋ฆฌํ”„๋Š” ๋ณ€ํ™˜ํ•˜์ง€ ์•Š์€ ๋ชจ๋ธ ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌํ•˜๋”๋ผ๋„ ํƒ€์ž„๋ฆฌํ”„ ์ฝ”๋“œ์—์„œ ์Šคํ”„๋ง ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์ ์šฉํ• ์ˆ˜๋„ ์žˆ๋‹ค.
@Controller
public class ConverterController {

  @GetMapping("/converter-view")
  public String converterView(Model model) {
    // ๋ชจ๋ธ์— ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์ ์šฉํ•˜์ง€์•Š๊ณ  ๊ฐ์ฒด ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ
    model.addAttribute("number", 10000);
    model.addAttribute("ipPort", new IpPort("127.0.0.1", 8080));
    return "converter-view";
  }
}

 

  • ์•„๋ž˜์™€ ๊ฐ™์ด ํƒ€์ž„๋ฆฌํ”„ ๋ณ€์ˆ˜ ํ‘œํ˜„์‹์— ${{...}} ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ Œ๋”๋งํ•œ๋‹ค.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  ...
</head>

<body>
  <ul>
    <li>${number}: <span th:text="${number}"></span></li> 
    <li>${{number}}: <span th:text="${{number}}"></span></li>
    <li>${ipPort}: <span th:text="${ipPort}"></span></li><!-- ๊ทธ๋ƒฅ str(ipPort) ์ฒ˜๋ฆฌ -->
    <li>${{ipPort}}: <span th:text="${{ipPort}}"></span></li><!-- ์Šคํ”„๋ง ์ปจ๋ฒ„ํ„ฐ ์‚ฌ์šฉ -->
  </ul>
</body>
</html>

 

  • ๋˜ํ•œ ํƒ€์ž„๋ฆฌํ”„์˜ th:field๋Š” ์ž๋™์œผ๋กœ ์Šคํ”„๋ง ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์ ์šฉํ•ด์„œ HTTP ์š”์ฒญ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ธ๋‹ค.
     โžก ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์ง€์•Š๊ณ  ๋ฌธ์ž์—ด ๊ทธ๋Œ€๋กœ ์ „์†กํ•˜๋ ค๋ฉด th:value๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค. 
<form th:object="${form}" th:method="post">
    th:field <input type="text" th:field="*{ipPort}"><br/> <!-- ${{ipPort}} -->
    th:value <input type="text" th:value="*{ipPort}"><br/> <!-- ${ipPort} -->
    <input type="submit"/>
</form>

 

  • ์šฐ๋ฆฌ๋Š” ๋‹น์—ฐํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•˜๊ณ  @ModelAttribute ๋ฅผ ์ด์šฉํ•ด ๋ทฐ์—์„œ ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„์™”์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” HTTP์—์„œ ์ „์†ก๋œ ๋ฌธ์ž์—ด์„ ๊ฐ์ฒด๋กœ ์ปจ๋ฒ„ํ„ฐํ•˜๋Š” ๊ณผ์ •์ด ์žˆ๋‹ค๋Š” ๊ฑธ ์ธ์ง€ํ•˜์ž.
  @Data
  class IpPortForm {
    private IpPort ipPort;
    public IpPortForm(IpPort ipPort) {
      this.ipPort = ipPort;
    }
  }

...
  @GetMapping("/converter/edit")
  public String converterForm(Model model) {
    IpPort ipPort = new IpPort("127.0.0.1", 8080);
    IpPortForm form = new IpPortForm(ipPort); // Form ๊ฐ์ฒด๋ฅผ ๋ชจ๋ธ๋กœ ์ „๋‹ฌ
    model.addAttribute("form", form);
    return "converter-form";
  }

  @PostMapping("/converter/edit")
  public String converterEdit(@ModelAttribute IpPortForm form, Model model) {
    // @ModelAttribute ๋ฐ›์•„๋ณด๋ฉด IpPort์— ์ปจ๋ฒ„ํ„ฐ๊ฐ€ ์ ์šฉ๋˜์–ด์žˆ์Œ.
    // ๋งŒ์•ฝ th:value๋กœ ์ „์†กํ•˜๋ฉด ๋‹จ์ˆœ .toString() ๋ฌธ์ž์—ด์ด ๋“ค์–ด๊ฐ€์žˆ๋‹ค.
    IpPort ipPort = form.getIpPort();
    model.addAttribute("ipPort", ipPort);
    return "converter-view";
  }

 


# Formatter ํฌ๋งทํ„ฐ

์œ„์—์„œ ๋ฐฐ์šด Converter๋Š” ํƒ€์ž…์— ์ œํ•œ์—†์ด ๋ณ€ํ™˜๊ธฐ๋Šฅ์„ ์ œ๊ณตํ–ˆ๋‹ค. ํ•˜์ง€๋งŒ HTTP ์š”์ฒญ์˜ ๋Œ€๋ถ€๋ถ„์€ String์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋Œ€๋ถ€๋ถ„์ด๋‹ค. ๊ทธ๋ž˜์„œ ์Šคํ”„๋ง์—์„œ๋Š” ๋ฌธ์ž๋ฅผ ์ฒ˜๋ฆฌ์— ํŠนํ™”๋œ ์ปจ๋ฒ„ํ„ฐ์ธ Fomatter ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

 โžก ์ฐธ๊ณ ๋กœ ๋™์ผํ•œ ๋™์ž‘์„ ํ•˜๋Š” Converter์™€ Formatter๊ฐ€ ์žˆ๋‹ค๋ฉด, ์ปจ๋ฒ„ํ„ฐ๊ฐ€ ๋จผ์ € ์ ์šฉ๋œ๋‹ค. ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋” ๋†’๋‹ค.

 

Core Technologies

In the preceding scenario, using @Autowired works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at ServiceConfig, how do

docs.spring.io

 

Formatter๋Š” ํŠน๋ณ„ํ•œ String Converter๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค. ๋ฌธ์ž๋ฅผ ์ถœ๋ ฅํ•  ๋•Œ Locale ์ •๋ณด๋ฅผ ์ด์šฉํ•˜์—ฌ ๊ตญ์ œํ™”ํ•˜๊ฑฐ๋‚˜ ์›ํ•˜๋Š” ๋ชจ์–‘์˜ ๋ฌธ์ž์—ด๋กœ ํฌ๋งทํŒ…ํ•  ์ˆ˜ ์žˆ๋Š” ์ถ”๊ฐ€๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.

 โžก ๋ณด๋ฉด ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‹ ๊ธฐํ•˜๊ฒŒ ์ƒ์†ํ•˜๊ณ ์žˆ๋Š”๋ฐ, ์™œ ์ด๋žฌ๋Š”์ง€ ๊ถ๊ธˆํ•˜๋‹ค๋ฉด ๊ฐ์ฒด์ง€ํ–ฅ์˜ ISP ์›์น™์„ ์ฐพ์•„๋ณด์ž.

  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> {
      String print(T object, Locale locale);
      T parse(String text, Locale locale) throws ParseException;
  }
@Slf4j
public class MyNumberFormatter implements Formatter<Number> {
    @Override
    public Number parse(String text, Locale locale) throws ParseException {
        //"1,000" -> 1000
        NumberFormat format = NumberFormat.getInstance(locale);
        return format.parse(text);
    }

    @Override
    public String print(Number object, Locale locale) {
        // 1000 -> "1,000"
        return NumberFormat.getInstance(locale).format(object);
    }
}

 


# ๋‚ด๊ฐ€ ๋งŒ๋“  Formatter๋ฅผ ์Šคํ”„๋ง ConversionService์— ๋“ฑ๋กํ•˜๊ธฐ

  • ์ปจ๋ฒ„ํ„ฐ๋ž‘ ํฌ๋งทํ„ฐ์˜ ์‚ฌ์šฉ๋ฒ•์€ ๋˜‘๊ฐ™๋‹ค. ์• ์ดˆ์— ๋ฉ”์„œ๋“œ ์ด๋ฆ„๋„ addFormatters ์˜€๊ธฐ๋„ ํ•˜๋‹ค.
     โžก ์Šคํ”„๋ง๋ถ€ํŠธ์—์„œ๋Š” DefaultConversionService๋ฅผ ์ƒ์†๋ฐ›์€ WebConversionService ๊ตฌํ˜„์ฒด๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void addFormatters(FormatterRegistry registry) {
    // ์•ž์—์„œ ๋“ฑ๋กํ–ˆ๋˜ ์ปจ๋ฒ„ํ„ฐ
    // ConversionService conversionService = new WebConversionService();
    registry.addConverter(new StringToIpPortConverter());
    registry.addConverter(new IpPortToStringConverter());

    //์ถ”๊ฐ€. ์Šคํ”„๋ง์—์„œ๋Š” ์–ด๋Œ‘ํ„ฐ ํŒจํ„ด๋“ฑ์„ ์ด์šฉํ•ด Formatter๋ฅผ Converter์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๋„๋ก ๋งŒ๋“ ๋‹ค.
    registry.addFormatter(new MyNumberFormatter());
  }
}

 

  • ์ฐธ๊ณ ๋กœ ConversionService๋Š” ์Šคํ”„๋ง์— ์˜์กด์ ์ธ ๊ฐ์ฒด๊ฐ€ ์•„๋‹ˆ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์ด ๋”ฐ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
public class FormattingConversionServiceTest {

  @Test
  void formattingConversionService() {
    // ConversionService ๊ฐ์ฒด ์ƒ์„ฑ
    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);

  }
}

 


# Formatter๋ฅผ ์‹ค์ œ ์„œ๋น„์Šค์— ์‚ฌ์šฉํ•ด๋ณด๊ธฐ

์ปจ๋ฒ„ํ„ฐ๋ž‘ ๋‹ค๋ฅธ ๋˜‘๊ฐ™์ด ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค. ๋“ฑ๋ก์€ ๋‹ฌ๋ผ๋„ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ๊ฐ™์€ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

@Slf4j
public class MyNumberFormatter implements Formatter<Number> {

  @Override
  public Number parse(String text, Locale locale) throws ParseException {
    //"1,000" -> 1000
    NumberFormat format = NumberFormat.getInstance(locale);
    return format.parse(text);
  }

  @Override
  public String print(Number object, Locale locale) {
    // 1000 -> "1,000"
    return NumberFormat.getInstance(locale).format(object);
  }
}
@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void addFormatters(FormatterRegistry registry) {
    registry.addFormatter(new MyNumberFormatter());
  }
}
<!-- formatter-view.html -->
<ul> <!-- ์„œ๋ฒ„ โžก  ํด๋ผ์ด์–ธํŠธ -->
  <li>${form.number}: <span th:text="${form.number}" ></span></li>
  <li>${{form.number}}: <span th:text="${{form.number}}" ></span></li> <!-- ํฌ๋งทํ„ฐ ์‚ฌ์šฉ -->
  <li>${form.localDateTime}: <span th:text="${form.localDateTime}" ></span></li>
  <li>${{form.localDateTime}}: <span th:text="${{form.localDateTime}}" ></span></li> <!-- ํฌ๋งทํ„ฐ ์‚ฌ์šฉ -->
</ul>
<!-- formatter-form.html -->
<form th:method="post" th:object="${form}">
  <!-- ํด๋ผ์ด์–ธํŠธ โžก ์„œ๋ฒ„ -->  
  number <input th:field="*{number}" type="text"><br/> <!-- th:field ๋Š” ์ปจ๋ฒ„ํ„ฐ ์ ์šฉ -->
  localDateTime <input th:field="*{localDateTime}" type="text"><br/> 
  <input type="submit"/>
    
</form>

 


# ์Šคํ”„๋ง ์–ด๋…ธํ…Œ์ด์…˜ ํฌ๋งทํ„ฐ ํ™œ์šฉ(@NumberFormat, @DateTimeFormat)

์Šคํ”„๋ง์— ์žˆ๋Š” ๊ธฐ๋ณธ ํฌ๋งทํ„ฐ๋“ค์„ ์‚ฌ์šฉํ•˜๋‹ค๊ฐ€ ๋‚˜๋งŒ์˜ ํฌ๋งทํ„ฐ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค์–ด์„œ ๋“ฑ๋กํ•˜๋ฉด ๋œ๋‹ค. ํ•˜์ง€๋งŒ ์ƒ๊ฐํ•ด๋ณด๋ฉด ๋‚ ์งœ, ๋ฌธ์ž์— ๊ฐ„๋‹จํ•œ ํฌ๋งทํŒ…์„ ์ถ”๊ฐ€ํ• ๋•Œ๋„ ๋งค๋ฒˆ ์ด๋Ÿฐ ๊ณผ์ •์„ ๊ฑฐ์ณ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ์—๋Š” ๋„ˆ๋ฌด ๋ฒˆ๊ฑฐ๋กญ๋‹ค.

 

 โžก ์Šคํ”„๋ง์—์„œ๋Š” @XxxFormat(pattern="...")์œผ๋กœ ๊ธฐ๋ณธ ํฌ๋งทํ„ฐ๋ฅผ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๊ฒŒ ๊ตฌํ˜„ํ•ด๋†“์•˜๋‹ค.

์Šคํ”„๋ง์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋ณธ ํฌ๋งทํ„ฐ

 

Core Technologies

In the preceding scenario, using @Autowired works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at ServiceConfig, how do

docs.spring.io

@Data
class Form { // ๋ชจ๋ธ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฐ์ฒด์— ์ด๋ ‡๊ฒŒ Formatter๋ฅผ ์ง€์ • ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.
  @NumberFormat(pattern = "###,###") // "100,000" -> 100000
  private Integer number;

  @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") // "2021-08-31 ..." -> Date ๊ฐ์ฒด
  private LocalDateTime localDateTime;
}
@Controller
public class FormatterController {

  @GetMapping("/formatter/edit")
  public String formatterForm(Model model) {
    Form form = new Form();
    form.setNumber(10000);
    form.setLocalDateTime(LocalDateTime.now());
    model.addAttribute("form", form);
    return "formatter-form";
  }

  @PostMapping("/formatter/edit")
  public String formatterEdit(@ModelAttribute Form form) {
    return "formatter-view";
  }
}

 


# ์ฃผ์˜์‚ฌํ•ญ (HttpMessageConverter)

@ResponceBody์— ์‚ฌ์šฉ๋˜๋Š” ๋ฉ”์‹œ์ง€ ์ปจ๋ฒ„ํ„ฐ(HttpMessageConverter)์™€ Conversion Service์™€๋Š” ์ „ํ˜€ ์ƒ๊ด€์—†๋‹ค.

 

์ด ๋‘˜์€ ๋ณ„๊ฐœ์˜ ๊ฒƒ์œผ๋กœ ๋ฉ”์‹œ์ง€ ์ปจ๋ฒ„ํ„ฐ์˜ ๊ฒฝ์šฐ์—๋Š” ์™ธ๋ถ€๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(Jackson)๋กœ ๋™์ž‘ํ•˜๋Š”๊ฑฐ์ง€ ์Šคํ”„๋ง ์ž์ฒด์˜ ์ปจ๋ฒ„์ „ ์„œ๋น„์Šค์—์„œ Converter, Formatter๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ ์•„๋‹ˆ๋‹ค. ์ฆ‰ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์Šคํ”„๋ง @Configuration์— ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ด€๋ จ ์„ค์ •์„ ๋ฐ”๊ฟ”์•ผํ•œ๋‹ค.

 

์Šคํ”„๋ง Conversion Service๋Š” @ModelAttribute, @RequestParam, @PathVariable ๋“ฑ์— ์ ์šฉ๋œ๋‹ค.

 


# @ResponseBody๋ฅผ ์ปค์Šคํ…€ํ•˜๋Š” ๋ฐฉ๋ฒ•

๋ฌผ๋ก  @ReponseBody์— ์‚ฌ์šฉ๋˜๋Š” HttpMessageConverter์˜ ๊ตฌํ˜„์ฒด๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ๋‹ค. 

๋‹ค๋งŒ ๊ธฐ๋ณธ ๊ตฌํ˜„์ฒด(Jackson)์„ ๊ตณ์ด ๋ณ€๊ฒฝํ•ด์„œ ์‚ฌ์šฉํ•˜๋ฉด ๋ฒˆ๊ฑฐ๋กญ๊ธฐ๋„ํ•˜๊ณ , ๊ตณ์ด ๊ทธ๋ ‡๊ฒŒ๊นŒ์ง€ ํ•  ์ด์œ ๋„ ์—†๋‹ค.

 

Rest API ์„œ๋ฒ„๋ฅผ ๊ตฌํ˜„ํ•  ๋•Œ, @ResponseBody ์˜ ์ปจ๋ฒ„ํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์Šคํ”„๋ง ๊ธฐ๋ณธ ๊ตฌํ˜„์ฒด์ธ Jackson์—์„œ ์ œ๊ณตํ•ด์ฃผ๋Š” ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ํ•ด๊ฒฐํ•˜์ž.

  • ๋ฐ”์ดํŠธ ๋ฐ์ดํ„ฐ -> ByteArrayHttpMessageConverter
  • ๋ฌธ์ž์—ด ๋ฐ์ดํ„ฐ -> StringHttpMessageConverter
  • ๊ฐ์ฒด, HashMap -> MappingJackson2HttpMessageConveter (๋‚ด๋ถ€์ ์œผ๋กœ Jackson ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์ฒ˜๋ฆฌ๋ฅผ ์œ„์ž„ํ•œ๋‹ค.)
import com.fasterxml.jackson.annotation.*;

 

์˜ˆ๋ฅผ ๋“ค์–ด, Request์— ๋‹ด๊ธด Json Body์˜ ์ด๋ฆ„ ๊ทœ์น™์ด ๋‹ค๋ฅด๋‹ค๋ฉด

@JsonProperty๋ฅผ ์ด์šฉํ•˜์—ฌ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค. ๋งŒ์•ฝ ์‹œ๊ฐ„๊ณผ ๊ฐ™์ด ํฌ๋งทํŒ…๋œ ๋ฐ์ดํ„ฐ๋Š” @JsonFormat์„ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

@RestController
@RequestMapping("/api")
public class StudentController {

    public void post(@RequestBody Student student) {
        System.out.println(student);
    }
}
@Data
public class Student {

    @JsonProperty("my_name")
    private String myName;

    @JsonProperty("my_age")
    private String myAge;

    @JsonProperty("my_country")
    private String myCountry;
    
    @JsonFormat(
       shape = JsonFormat.Shape.STRING,
       pattern = "dd-MM-yyyy hh:mm:ss")
    public Date eventDate;

}

 

๋งŒ์•ฝ ํ•˜๋‚˜์”ฉ ์ง€์ •ํ•˜๋Š”๊ฒŒ ๊ท€์ฐฎ๋‹ค๋ฉด, @JsonNaming์„ ์ด์šฉํ•˜์—ฌ ํฌ๋งทํŒ…์„ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค. ( my_fisrt_age โžก MyFirstAge)

@Data
@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
public class Student {

    private String myName; // my_name ๊ฐ€ ์—ฌ๊ธฐ์— ์ €์žฅ๋จ
    private String myAge; // my_age ๊ฐ€ ์—ฌ๊ธฐ์— ์ €์žฅ๋จ
    private String myCountry; // my_country ๊ฐ€ ์—ฌ๊ธฐ์— ์ €์žฅ๋จ
}

 

ํŠน์ • ๋ฉ”์„œ๋“œ๊ฐ€ ํ•ด๋‹น ํ•„๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์„ค์ •ํ• ์ˆ˜๋„ ์žˆ๋‹ค.

public class MyBean {
    public int id;
    private String name;

    @JsonGetter("name")
    public String myGetTheName() {
        return name;
    }

    @JsonSetter("name")
    public void mySetTheName(String name) {
        this.name = name;
    }
}

 

ํ•„๋“œ๊ฐ€ ์•„๋‹Œ ์ƒ์„ฑ์ž๋กœ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๊ณ  ์‹ถ๋‹ค๋ฉด, @JsonCreator ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

public class BeanWithCreator {
    public int id;
    public String name;

    @JsonCreator
    public BeanWithCreator(
      @JsonProperty("id") int id, 
      @JsonProperty("theName") String name) {
        this.id = id;
        this.name = name;
    }
}

 

๋งŒ์•ฝ ๋งŒ๋“ค์–ด๋‘” DTO๊ฐ€ ์žฌ์‚ฌ์šฉ๋œ๋‹ค๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด ํ•œ ํ•„๋“œ์— ์—ฌ๋Ÿฌ ๊ฐ’์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

public class AliasBean {
    @JsonAlias({ "fName", "f_name" })
    private String firstName;   
    private String lastName;
}

 

๋งˆ์ง€๋ง‰์œผ๋กœ ์ง๋ ฌํ™”ํ•˜๋ฉด ์•ˆ๋˜๋Š” ํ•„๋“œ๋Š” @JsonIgnore, @JsonIgnoreProperties ๋กœ ํ•„๋“œ๊ฐ€ ์—†๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

public class BeanWithIgnore {
    @JsonIgnore
    public int id; // ์ง๋ ฌํ™”๊ฐ€ ๋˜์ง€ ์•Š์Œ. ๊ฒฐ๊ณผ์—๋Š” name ๋งŒ ๋‚จ๊ฒŒ๋จ

    public String name;
}
@JsonIgnoreProperties({ "id" })
public class BeanWithIgnore {
    public int id; // ์ง๋ ฌํ™”๊ฐ€ ๋˜์ง€์•Š์Œ. ๊ฒฐ๊ณผ ๊ฐ’์—๋Š” name๋งŒ ๋‚จ๊ฒŒ๋จ.
    public String name;
}
public class User {
    public int id;
    public Name name;

    @JsonIgnoreType // inner class๋Š” ์ด๋ ‡๊ฒŒ ignore๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
    public static class Name { 
        public String firstName;
        public String lastName;
    }
}

 

๋˜๋Š” null ์ธ ๊ฐ’๋งŒ ์ง๋ ฌํ™” ๋Œ€์ƒ์—์„œ ์ œ์™ธํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด @JsonInclude๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ๋‹ค.

@JsonInclude(Include.NON_NULL)
public class MyBean {
    public int id; // null ์ธ ๊ฒฝ์šฐ ์ œ์™ธ๋จ.
    public String name; // null ์ธ ๊ฒฝ์šฐ ์ œ์™ธ๋จ
}

 

๋งˆ์ง€๋ง‰์œผ๋กœ DTO๋ฅผ ์ƒ์†ํ•˜๊ฑฐ๋‚˜ inner class๋ฅผ ์‚ฌ์šฉํ•œ ๊ฒฝ์šฐ์—๋„ ๋ณ€ํ™˜์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

๐Ÿงจ ๋‹ค๋งŒ ์ด๋ ‡๊ฒŒ ๋ณต์žกํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜์ง€ ๋ง๊ณ , ๊ทธ๋ƒฅ ํด๋ž˜์Šค๋ฅผ 2๊ฐœ๋กœ ๋‚˜๋ˆ ์„œ ์“ฐ์ž.

inner class๋Š” ๊ทธ๋ƒฅ ์“ฐ๋ฉด ๋œ๋‹ค. ํŠน๋ณ„ํ•œ๊ฑฐ ์—†๋‹ค.

 

public class Zoo {
    public Animal animal;

    public Zoo(Animal animal) {
        this.animal = animal;
    }
    
    @JsonTypeInfo( // ์›ํ•˜๋Š” ๋ชจ์–‘ {"animal": {"type":"dog", "name":"...", "barkVolume":0.0} }
            use = JsonTypeInfo.Id.NAME,
            include = JsonTypeInfo.As.PROPERTY,
            property = "type") // "type"์— ํƒ€์ž… ์ •๋ณด๊ฐ€ ๊ธฐ๋ก๋˜๋„๋ก ๋งŒ๋“ค๊ธฐ
    @JsonSubTypes({ // ์–ด๋–ค ํƒ€์ž…์ธ์ง€ ๊ธฐ๋กํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, @JsonSubTypes๋ฅผ ์ด์šฉํ•ด์„œ ์ •์˜
            @JsonSubTypes.Type(value = Dog.class, name = "dog"),
            @JsonSubTypes.Type(value = Cat.class, name = "cat") })
    public static class Animal {
        public String name;
    }

    @JsonTypeName("dog")
    public static class Dog extends Animal {
        public double barkVolume;

        public Dog(String name) {
            super.name = name;
        }
    }

    @JsonTypeName("cat")
    public static class Cat extends Animal {
        public int lives;
        boolean likesCream;
    }
}

 

๋ธ”๋กœ๊ทธ์˜ ์ •๋ณด

JiwonDev

JiwonDev

ํ™œ๋™ํ•˜๊ธฐ