ํธ๋ค๋ฌ ๋ฉ์๋์ @ModelAttribute ๊ฐ ์ง์ ๋๋ฉด ๋ค์๊ณผ ๊ฐ์ ์ผ์ด ์ผ์ด๋๋ค.
- ํ๋ผ๋ฏธํฐ ํ์
์ ์ค๋ธ์ ํธ๋ฅผ ๋ง๋ ๋ค.
- Ex. @ModelAttribute User user : User ๋ผ๋ ์๋ก์ด ์ค๋ธ์ ํธ๋ฅผ ๋ง๋ ๋ค.
@SessionAttributes
์ ์ํด ์ธ์ ์ ์ ์ฅ๋ ๋ชจ๋ธ ์ค๋ธ์ ํธ๊ฐ ์๋ค๋ฉด, ์ธ์ ์ ์ ์ฅ๋์ด ์๋ ์ค๋ธ์ ํธ๋ฅผ ๊ฐ์ ธ์จ๋ค.
- ์น ํ๋ผ๋ฏธํฐ๋ฅผ ์ค๋ธ์ ํธ ํ๋กํผํฐ์ ๋ฐ์ธ๋ฉ ํ๋ค.
- HTTP ๋ฅผ ํตํด ์ ๋ฌ๋๋ ํ๋ผ๋ฏธํฐ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฌธ์์ด ํ์์ด๋ผ์ String ๊ฐ์ฒด๊ฐ ์๋ ์ด์ ์ ์ ํ ๋ณํ์ด ํ์ํ๋ค.
- ์ด๋, ์คํ๋ง์์ ์ ๊ณตํ๋ ๊ธฐ๋ณธ ํ๋กํผํฐ ์๋ํฐ๋ฅผ ํตํด์
HTTP Parameter -> Object Property
๋ก ๋ณํํ๋ค. - ๋ณํ์ด ๋ถ๊ฐ๋ฅํ๋ฉด BindingResult ์์ ์ค๋ฅ๋ฅผ ์ ์ฅํด์ ์ ์ ํ ์ฒ๋ฆฌ๋ฅผ ํด์ผ ํ๋ค.
- ๋ชจ๋ธ์ ๊ฐ์ ๊ฒ์ฆํ๋ค.
- ํ์ ์ ๋ํ ๊ฒ์ฆ์ ๋๋ฌ์ง๋ง, ๊ทธ ์ธ์ ๊ฒ์ฆํ ๋ด์ฉ์ด ์๋ค๋ฉด ์ ์ ํ ๊ฒ์ฆ๊ธฐ๋ฅผ ๋ฑ๋กํด์ ๋ชจ๋ธ์ ๋ด์ฉ์ ๊ฒ์ฆํ ์ ์๋ค.
์คํ๋ง์์ ๋ฐ์ธ๋ฉ์ด๋ ์ค๋ธ์ ํธ์ ํ๋กํผํฐ์ ๊ฐ์ ๋ฃ๋ ๊ฒ์ ๋งํ๋ค.
- ์คํ๋ง์ด ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํ๋ ๋ฐ์ธ๋ฉ์ฉ ํ์ ๋ณํ API ์ด๋ค.
- PropertyEditor ๋ ์ฌ์ค ์คํ๋ง API ๋ ์๋๊ณ ์๋ฐ๋น ํ์ค์ ์ ์๋ ์ธํฐํ์ด์ค๋ค.
- XML ์ value ์ ํธ๋ฆฌ๋ทฐํธ๋, @Controller ์ ํ๋ผ๋ฏธํฐ์ ์ ์ฉ๋๋ค.
<bean id="dataSource" class="org...SimpleDriverDataSource">
<!-- com.mysql.jdbc.Driver ํด๋์ค ํ์
์ ์ค๋ธ์ ํธ๋ฅผ ๋ง๋ค์ด์ driverClass ๋ผ๋ ํ๋กํผํฐ์ ๋ฐ์ธ๋ฉํ๋ค. -->
<property name = "driverClass" value = "com.mysql.jdbc.Driver" />
</bean>
// CharsetEditor ์ฌ์ฉ
@RequestMapping("/hello")
public void hello(@RequestParam Charset charset, Model model) {
}
HTTP ์์ฒญ ํ๋ผ๋ฏธํฐ์ ๊ฐ์ ๋ฌธ์์ด์ ์คํธ๋ง ํ์ ์ผ๋ก ์๋ธ๋ฆฟ์์ ๊ฐ์ ธ์จ๋ค.
- String to Object
- setAsText() ๋ก String ํ์ ์ ๋ฌธ์์ด์ ๋ฃ๊ณ getValue() ๋ก ๋ณํ๋ ์ค๋ธ์ ํธ๋ฅผ ๊ฐ์ ธ์จ๋ค.
- Object to String
- setValue() ๋ก ์ค๋ธ์ ํธ๋ฅผ ๋ฃ๊ณ getAsText() ๋ก ๋ณํ๋ ๋ฌธ์์ด์ ๊ฐ์ ธ์จ๋ค.
๋ฐ๋ผ์, ์ปค์คํ ํ๋กํผํฐ ์๋ํฐ๋ฅผ ๋ง๋ค ๋๋, setAsText(), getAsText() ๋ถ๋ถ๋ง ์๋ณด๋ฉด ๋๋ค.
PropertyEditor ์ธํฐํ์ด์ค๋ฅผ ์ง์ ๊ตฌํํ๊ธฐ๋ณด๋ค๋ ๊ธฐ๋ณธ ๊ตฌํ์ด ๋์ด์๋ PropertyEditorSupport ํด๋์ค๋ฅผ ์์ํด์ ํ์ํ ๋ฉ์๋๋ง ์ค๋ฒ๋ผ์ด๋ฉ ํ๋๊ฒ ๋ซ๋ค.
class LevelPropertyEditor: PropertyEditorSupport() {
override fun getAsText(): String {
// this.value -> setValue ์ ์ํด ์ ์ฅ๋ Level ํ์
์ ์ค๋ธ์ ํธ๋ฅผ ๊ฐ์ ธ์์ ๊ฐ์ ๋ฌธ์๋ก ๋ณํํ๋ค.
return ((this.value as Level).intValue().toString())
}
override fun setAsText(text: String?) {
this.value = text?.trim()?.let { Level.valueOf(it.toInt()) }
}
}
๋ง๋ ์ปค์คํ
ํ๋กํผํฐ ์๋ํฐ๊ฐ ์คํ๋ง MVC ์์ ๋์ํ๊ฒ ํ๋ ค๋ฉด @InitBinder
๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.
- ์ปจํธ๋กค๋ฌ ๋ฉ์๋์์ ๋ฐ์ธ๋ฉ์ด ์ด๋ป๊ฒ ์ผ์ด๋ ๊น?
- @Controller ํธ๋ค๋ฌ ๋ฉ์๋๋ฅผ ํธ์ถํด์ค ์ฑ
์์ด ์๋
AnnotationMethodHandlerAdapter
๋ @RequestParam, @ModelAttribute ๋ฑ HTTP ์์ฒญ์ ํ๋ผ๋ฏธํฐ ๋ณ์์ ๋ฐ์ธ๋ฉ ํด์ฃผ๋ ์์ ์ด ํ์ํ ์ด๋ ธํ ์ด์ ์ ๋ง๋๋ฉด ๋จผ์ WebDataBinder
๋ผ๋ ๊ฒ์ ๋ง๋ ๋ค.
- @Controller ํธ๋ค๋ฌ ๋ฉ์๋๋ฅผ ํธ์ถํด์ค ์ฑ
์์ด ์๋
- WebDataBinder
- HTTP ์์ฒญ์ผ๋ก๋ถํฐ ๊ฐ์ ธ์จ ๋ฌธ์์ด์ ํ๋ผ๋ฏธํฐ ํ์
์ ์ค๋ธ์ ํธ๋ก ๋ณํํด์ฃผ๋ ๊ธฐ๋ฅ์ด ์๋ค. ์ด๋
PropertyEditor
๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด๋ค. - ๋ฐ๋ผ์, ์ปค์คํ ํ๋กํผํฐ ์๋ํฐ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์๋ WebDataBinder ์ ๋ฑ๋กํด์ค์ผ ํ๋ค.
- WebDataBinder ๋ ์ปค์คํ ํ๋กํผํฐ ์๋ํฐ๊ฐ ์์ผ๋ฉด ๋จผ์ ์ ์ฉํ๊ณ , ์ ์ ํ ํ๋กํผํฐ ์๋ํฐ๊ฐ ์๋ค๋ฉด ๊ทธ๋ ์คํ๋ง์์ ์ ๊ณตํ๋ ๋ํดํธ ํ๋กํผํฐ ์๋ํฐ์ค ํ๋๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋ค.
- HTTP ์์ฒญ์ผ๋ก๋ถํฐ ๊ฐ์ ธ์จ ๋ฌธ์์ด์ ํ๋ผ๋ฏธํฐ ํ์
์ ์ค๋ธ์ ํธ๋ก ๋ณํํด์ฃผ๋ ๊ธฐ๋ฅ์ด ์๋ค. ์ด๋
@RestController
class ConversionController {
@InitBinder
fun initBinder(webDataBinder: WebDataBinder) {
webDataBinder.registerCustomEditor(Level::class.java, LevelPropertyEditor())
}
@GetMapping("/level")
fun levelCustomEditor(@RequestParam level: Level): Int {
return level.intValue()
}
}
์ค๋ธ์ ํธ๋ฅผ ๋งค๋ฒ ์๋ก ๋ง๋๋ ๋์ ํ๋กํผํฐ ์๋ํฐ๋ฅผ ์ฑ๊ธํค ๋น์ผ๋ก ๋ฑ๋กํด๋๊ณ ๊ณต์ ํด์ ์ธ ์ ์์๊น? ์์ฝ์ง๋ง ํ๋กํผํฐ ์๋ํฐ๋ ์ฑ๊ธํค ๋น์ผ๋ก ๋ฑ๋ก๋ ์ ์๋ค.
ํ๋กํผํฐ ์๋ํฐ์ ์ํด ํ์
์ด ๋ณ๊ฒฝ๋๋ ์ค๋ธ์ ํธ๋ ํ ๋ฒ์ ํ๋กํผํฐ ์๋ํฐ ์ค๋ธ์ ํธ ๋ด๋ถ์ ์ ์ฅ
๋๋ค๋ ์ฌ์ค์ ์ ์ ์๋ค. ๋ฐ๋ผ์, ๋ฉํฐ ์ค๋ ๋ ํ๊ฒฝ์์ ์ฑ๊ธํค์ผ๋ก ๋ง๋ค์ด ๊ณต์ ํด์ ์ฌ์ฉํ๋ฉด ์๋๋ค.
๊ทธ๋ฐ๋ฐ ํ๋กํผํฐ ์๋ํฐ๊ฐ ๋ค๋ฅธ ์คํ๋ง ๋น์ ์ฐธ์กฐํด์ผ ํ๋ค๋ฉด ์ด๋จ๊น?
๋ค๋ฅธ ๋น์ ์ฐธ์กฐํด์ DI ๋ฐ์ผ๋ ค๋ฉด ์์ ๋ ์คํ๋ง ๋น์ผ๋ก ๋ฑ๋ก๋์ด์ผ ํ๋ค. ์ด๋ฅผ ์ํด ํ๋กํผํฐ ์๋ํฐ๊ฐ ๋ค๋ฅธ ๋น์ DI ๋ฐ์ ์ ์๋๋ก ์์ ๋ ๋น์ผ๋ก์ ๋ฑ๋ก๋๋ฉด์ ๋์์ ๋งค๋ฒ ์๋ก์ด ์ค๋ธ์ ํธ๋ฅผ ๋ง๋ค์ด์์ฌ์ฉํ ์ ์์ผ๋ ค๋ฉด ํ๋กํ ํ์
์ค์ฝํ์ ๋น
์ผ๋ก ๋ง๋ค์ด์ ธ์ผ ํ๋ค.
ํ๋กํ ํ์ ์ค์ฝํ ๋น์ ๋งค๋ฒ ๋น ์ค๋ธ์ ํธ๋ฅผ ์์ฒญํด์ ์๋ก์ด ์ค๋ธ์ ํธ๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ผ๋ฉด์ DI ๋ ๊ฐ๋ฅํ๋ค.
@Component
@Scope("prototype")
public class CodePropertyEditor extends PropertyEditorSupport{
@Atuworied codeService;
@Override
public void setAsText(String text) throws IllegalArgumentException {
Code code = codeService.getCode(Integer.valueOf(text));
this.setValue(code);
}
}
@Controller
public class UserController{
@Inject Provider<CodePropertyEditor> codePropertyEditorProvider;
@InitBinder
public void initBinder(WebDataBinder dataBinder){
dataBinder.registerCustomEditor(Code.class, codePropertyEditorProvider.get());
}
@RequestMapping("/user", method=RequestMethod.POST)
public String userAdd(@ModelAttribute User user){
// ...
}
}
- ์ด ๋ฐฉ์์ ์ฅ์ ์ ํญ์ ์์ ํ ๋๋ฉ์ธ ์ค๋ธ์ ํธ๋ฅผ ๋ฆฌํดํด์ฃผ๋ฏ๋ก, ์์ ์ ๊ธฐํ๋ ์ํ์ด ์์ด์ง๋ค.
- ๋จ์ ์ผ๋ก๋ ๋งค๋ฒ DB์์ ์กฐํ๋ฅผ ํด์ผํ๋ฏ๋ก ์ฑ๋ฅ์ ์กฐ๊ธ ๋ถ๋ด์ ์ฃผ๋ ๋จ์ ์ด ์๋ค.
- JPA ์ ๊ฐ์ด ์ํฐํฐ ๋จ์์ ์บ์ฑ ๊ธฐ๋ฒ์ด ๋ฐ๋ฌํ ๊ธฐ์ ์ ์ฌ์ฉํ ๊ฒฝ์ฐ, DB์์ ์กฐํํ๋ ๋์ ๋ฉ๋ชจ๋ฆฌ์์ ๋ฐ๋ก ์ฝ์ด์ฌ ์ ์์ผ๋ฏ๋ก DB ๋ถํ์ ๋ํ ๊ฑฑ์ ์ ํ์ง ์์๋ ๋๋ค.
PropertyEditor๋ ๊ทผ๋ณธ์ ์ธ ๋จ์ ์ด ์๋ค. ์ํ๋ฅผ ๊ฐ์ง๊ณ ์์ผ๋ฏ๋ก ์ฑ๊ธํค์ผ๋ก ๋ฑ๋กํ ์ ์๊ณ , ํญ์ ์๋ก์ด ์ค๋ธ์ ํธ๋ฅผ ๋ง๋ค์ด์ผ ํ๋ค๋ ์ ์ด๋ค. ๋ฌผ๋ก ์์ฑ๋๋ ์ค๋ธ์ ํธ ์์ฒด๊ฐ ๊ฐ๋ณ๊ธฐ ๋๋ฌธ์ ํฌ๊ฒ ๋ฌธ์ ๋ ๊ฒ์ ์์ง๋ง, ์ฑ๊ธํค ์๋น์ค ์ค๋ธ์ ํธ ์ค์ฌ์ ์คํ๋ง ๊ณผ๋ ์ ์ด์ธ๋ฆฌ์ง ์๋๋ค. ํนํ, ๋น์ผ๋ก ๋ฑ๋กํด์ ์ฌ์ฉํ ๋๋ ๋ฐ๋์ ํ๋กํ ํ์
์ค์ฝํ
๋ฅผ ์ฌ์ฉํด์ผํ๊ธฐ ๋๋ฌธ์ ๋ถํธํ๋ค.
์คํ๋ง 3.0์ดํ๋ก ์ด๋ฌํ PropertyEditor ์ ๋จ์ ์ ๋ณด์ํด์ฃผ๋ Converter ๋ผ๋ ํ์
๋ณํ API
๊ฐ ๋ฑ์ฅํ์๋ค. Converter ๋ PropertyEditor ์ ๋ฌ๋ฆฌ ๋ณํ๊ณผ์ ์์ ๋ฉ์๋๊ฐ ํ๋ฒ๋ง ํธ์ถ๋๋ค.
์ฆ, ์ํ๋ฅผ ๊ฐ์ง์ง ์๋๋ค๋ ๋ป์ด๊ณ , ์ฑ๊ธํค์ผ๋ก ๋ฑ๋กํ ์ ์๋ค๋ ๋ป์ด๋ค.
- Converter ์ธํฐํ์ด์ค
public interface Converter<S, T>{
T convert(s source);
}
class LevelToStringConverter: Converter<Level, String> {
override fun convert(source: Level): String {
return source.intValue().toString()
}
}
class StringToLevelConverter: Converter<String, Level> {
override fun convert(source: String): Level {
return Level.valueOf(source.toInt())
}
}
์ด๋ ๊ฒ ๋ ๊ฐ์ ์ปจ๋ฒํฐ๋ฅผ ์ฌ์ฉํ๋ฉด PropertyEditor ๋ฅผ ์ฌ์ฉํ์ ๋ ์ฒ๋ผ ๋์ผํ ํจ๊ณผ๋ฅผ ๋ผ ์ ์๋ค. ๋ํ Thread-safe
ํ๊ธฐ ๋๋ฌธ์ ๋ฉํฐ ์ค๋ ๋ ํ๊ฒฝ์์๋ ์์ ํ๊ฒ ์ฌ์ฉํ ์ ์๋ค.
ConversionService
๋ ์ฌ๋ฌ ์ข ๋ฅ์ ์ปจ๋ฒํฐ๋ฅผ ์ด์ฉํด์ ํ๋ ์ด์์ ํ์ ๋ณํ ์๋น์ค๋ฅผ ์ ๊ณตํด์ฃผ๋ ์ค๋ธ์ ํธ๋ฅผ ๋ง๋ค ๋ ์ฌ์ฉํ๋ ์ธํฐํ์ด์ค๋ค.- ๋ณดํต ConversionService ๋ฅผ ๊ตฌํํ
GenericConversionService
ํด๋์ค๋ฅผ ๋น์ผ๋ก ๋ฑ๋กํด์ ์ฌ์ฉํ๋ฉด ๋๋ค. GenericConversionService
๋ ์คํ๋ง์ ๋ค์ํ ํ์ ๋ณํ ๊ธฐ๋ฅ์ ๊ฐ์ง ์ค๋ธ์ ํธ๋ฅผ ๋ฑ๋กํ ์ ์๋ConverterRegistry
์ธํฐํ์ด์ค๋ ๊ตฌํํ๊ณ ์๋ค.- ์๋ก์ด ํ์
๋ณํ ์ค๋ธ์ ํธ๋
GenericConverter, ConverterFactory
๋ฅผ ์ฌ์ฉํด์ ๋ง๋ค ์๋ ์๋ค. - GenericConversionService ๋ ์ผ๋ฐ์ ์ผ๋ก ๋น์ผ๋ก ๋ฑ๋กํ๊ณ ํ์ํ ์ปจํธ๋กค๋ฌ์์ DI ๋ฐ์์ @InitBinder ๋ฉ์๋๋ฅผ ํตํด WebDataBinder ์ ์ค์ ํ๋ ๋ฐฉ์์ผ๋ก ์ฌ์ฉํ๋ค.
- ์ปจํธ๋กค๋ฌ์ ๋ฐ์ธ๋ฉ ์์ ์ ์ปจ๋ฒํฐ๋ฅผ ์ ์ฉํ๊ธฐ ์ํด์๋ ConversionService ํ์ ์ ์ค๋ธ์ ํธ๋ฅผ ํตํด WebDataBinder ์ ์ค์ ํด์ค์ผ ํ๋ค.
<bean class="org.springframework..ConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="LevelConverter" />
<!-- ์ถ๊ฐํ๊ณ ์ถ์ Converter๋ค... -->
</set>
</property>
</bean>
@Controller
public class UserController{
@Autowired ConversionService conversionService;
@InitBinder
public void initBinder(WebDataBinder dataBinder){
dataBinder.setConversionService(this.conversionService);
}
@RequestMapping("/user", method=RequestMethod.GET)
public String userSearch(@RequestParam Level level){
// ...
}
}
์คํ๋ง ๋ถํธ์์๋ ์๋์ฒ๋ผ ํธ๋ฆฌํ๊ฒ ์ฌ์ฉํ ์ ์๋ค. @InitBinder
๋ฅผ ์ปจํธ๋กค๋ฌ์์ ์ฌ์ฉํ์ง ์์๋ ์ปจ๋ฒํฐ๊ฐ ๋์ํ๋ค.
@Configuration
class WebMvcConfig: WebMvcConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
registry.addConverter(StringToLevelConverter())
registry.addConverter(LevelToStringConverter())
}
}
- ์น ์ ํ๋ฆฌ์ผ์ด์
์์ ๊ฐ์ฒด๋ฅผ ๋ฌธ์๋ก, ๋ฌธ์๋ฅผ ๊ฐ์ฒด๋ก ๋ณํํ๋ ์
- ํ๋ฉด์ ์ซ์๋ฅผ ์ถ๋ ฅํด์ผ ํ๋๋ฐ, Integer String ์ถ๋ ฅ ์์ ์ ์ซ์ 1000 ๋ฌธ์ "1,000" ์ด๋ ๊ฒ 1000 ๋จ์์ ์ผํ๋ฅผ ๋ฃ์ด์ ์ถ๋ ฅํ๊ฑฐ๋, ๋๋ "1,000" ๋ผ๋ ๋ฌธ์๋ฅผ 1000 ์ด๋ผ๋ ์ซ์๋ก ๋ณ๊ฒฝํด์ผ ํ๋ค.
- ๋ ์ง ๊ฐ์ฒด๋ฅผ ๋ฌธ์์ธ "2021-01-01 10:50:11" ์ ๊ฐ์ด ์ถ๋ ฅํ๊ฑฐ๋ ๋๋ ๊ทธ ๋ฐ๋์ ์ํฉ
- ์ฌ๊ธฐ์ ์ถ๊ฐ๋ก ๋ ์ง ์ซ์์ ํํ ๋ฐฉ๋ฒ์ Locale ํ์งํ ์ ๋ณด๊ฐ ์ฌ์ฉ๋ ์ ์๋ค.
์ด๋ ๊ฒ ๊ฐ์ฒด๋ฅผ ํน์ ํ ํฌ๋งท์ ๋ง์ถ์ด ๋ฌธ์๋ก ์ถ๋ ฅํ๊ฑฐ๋ ๋๋ ๊ทธ ๋ฐ๋์ ์ญํ ์ ํ๋ ๊ฒ์ ํนํ๋ ๊ธฐ๋ฅ์ด ๋ฐ๋ก ํฌ๋งทํฐ(Formatter)์ด๋ค. ํฌ๋งทํฐ๋ ์ปจ๋ฒ์ ์ ํน๋ณํ ๋ฒ์ ์ผ๋ก ์ดํดํ๋ฉด ๋๋ค.
- Converter vs Formatter
- Converter ๋ ๋ฒ์ฉ(๊ฐ์ฒด -> ๊ฐ์ฒด)
- Formatter ๋ ๋ฌธ์์ ํนํ(๊ฐ์ฒด -> ๋ฌธ์, ๋ฌธ์ -> ๊ฐ์ฒด) + ํ์งํ(Locale)
- Converter ์ ํน๋ณํ ๋ฒ์
ํฌ๋งทํฐ(Formatter)๋ ๊ฐ์ฒด๋ฅผ ๋ฌธ์๋ก ๋ณ๊ฒฝํ๊ณ , ๋ฌธ์๋ฅผ ๊ฐ์ฒด๋ก ๋ณ๊ฒฝํ๋ ๋ ๊ฐ์ง ๊ธฐ๋ฅ์ ๋ชจ๋ ์ํํ๋ค.
- String print(T object, Locale locale) : ๊ฐ์ฒด๋ฅผ ๋ฌธ์๋ก ๋ณ๊ฒฝํ๋ค.
- T parse(String text, Locale locale) : ๋ฌธ์๋ฅผ ๊ฐ์ฒด๋ก ๋ณ๊ฒฝํ๋ค.
/**
* Formats objects of type T.
* A Formatter is both a Printer <i>and</i> a Parser for an object type.
*
* @author Keith Donald
* @since 3.0
* @param <T> the type of object this Formatter formats
*/
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
public interface Printer<T> {
String print(T object, Locale locale);
}
public interface Parser<T> {
T parse(String text, Locale locale) throws ParseException;
}
์ซ์ 1000 ์ ๋ฌธ์ "1,000" ์ผ๋ก ๊ทธ๋ฌ๋๊น, 1000 ๋จ์๋ก ์ผํ๊ฐ ๋ค์ด๊ฐ๋ ํฌ๋งท์ ์ ์ฉํด๋ณด์. ๊ทธ๋ฆฌ๊ณ ๊ทธ ๋ฐ๋๋ ์ฒ๋ฆฌํด์ฃผ๋ ํฌ๋งทํฐ๋ฅผ ๋ง๋ค์ด๋ณด์.
@Slf4j
public class MyNumberFormatter implements Formatter<Number> {
// ๋ฌธ์๋ฅผ ์ซ์๋ก
@Override
public Number parse(String text, Locale locale) throws ParseException {
log.info("text={}, locale={}", text, locale);
// "1,000" -> 1000
NumberFormat format = NumberFormat.getInstance(locale);
return format.parse(text);
}
// ์ซ์๋ฅผ ๋ฌธ์๋ก
@Override
public String print(Number object, Locale locale) {
log.info("object={}, locale={}", object, locale);
return NumberFormat.getInstance(locale).format(object);
}
}
"1,000" ์ฒ๋ผ ์ซ์ ์ค๊ฐ์ ์ผํ๋ฅผ ์ ์ฉํ๋ ค๋ฉด ์๋ฐ๊ฐ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ๋ NumberFormat
๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๋ฉด
๋๋ค. ์ด ๊ฐ์ฒด๋ Locale ์ ๋ณด๋ฅผ ํ์ฉํด์ ๋๋ผ๋ณ๋ก ๋ค๋ฅธ ์ซ์ ํฌ๋งท์ ๋ง๋ค์ด์ค๋ค.
- ํ ์คํธ ์ฝ๋
class MyNumberFormatterTest {
MyNumberFormatter formatter = new MyNumberFormatter();
@Test
void parse() throws ParseException {
Number result = formatter.parse("1,000", Locale.KOREA);
assertThat(result).isEqualTo(1000L); // Long ํ์
์ฃผ์
}
@Test
void print() {
String result = formatter.print(1000, Locale.KOREA);
assertThat(result).isEqualTo("1,000");
}
}
parse() ์ ๊ฒฐ๊ณผ๊ฐ Long ์ด๊ธฐ ๋๋ฌธ์ isEqualTo(1000L) ์ ํตํด ๋น๊ตํ ๋ ๋ง์ง๋ง์ L ์ ๋ฃ์ด์ฃผ์ด์ผ ํ๋ค.
- ์คํ ๊ฒฐ๊ณผ
MyNumberFormatter - text=1,000, locale=ko_KR
MyNumberFormatter - object=1000, locale=ko_KR
์คํ๋ง์ ์ฉ๋์ ๋ฐ๋ผ ๋ค์ํ ๋ฐฉ์์ ํฌ๋งทํฐ๋ฅผ ์ ๊ณตํ๋ค.
- Formatter ํฌ๋งทํฐ
- AnnotationFormatterFactory ํ๋์ ํ์ ์ด๋ ์ ๋ ธํ ์ด์ ์ ๋ณด๋ฅผ ํ์ฉํ ์ ์๋ ํฌ๋งทํฐ
์์ธํ ๋ด์ฉ์ ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์.
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#format
์ปจ๋ฒ์ ์๋น์ค์๋ ์ปจ๋ฒํฐ๋ง ๋ฑ๋กํ ์ ์๊ณ , ํฌ๋งทํฐ๋ฅผ ๋ฑ๋กํ ์ ๋ ์๋ค. ๊ทธ๋ฐ๋ฐ ์๊ฐํด๋ณด๋ฉด ํฌ๋งทํฐ๋ ๊ฐ์ฒด -> ๋ฌธ์, ๋ฌธ์ -> ๊ฐ์ฒด๋ก ๋ณํํ๋ ํน๋ณํ ์ปจ๋ฒํฐ์ผ ๋ฟ์ด๋ค.
ํฌ๋งทํฐ๋ฅผ ์ง์ํ๋ ์ปจ๋ฒ์ ์๋น์ค๋ฅผ ์ฌ์ฉํ๋ฉด ์ปจ๋ฒ์ ์๋น์ค์ ํฌ๋งทํฐ๋ฅผ ์ถ๊ฐํ ์ ์๋ค. ๋ด๋ถ์์ ์ด๋ํฐ ํจํด
์ ์ฌ์ฉํด์ Formatter ๊ฐ Converter ์ฒ๋ผ ๋์ํ๋๋ก ์ง์ํ๋ค.
FormattingConversionService
๋ ํฌ๋งทํฐ๋ฅผ ์ง์ํ๋ ์ปจ๋ฒ์ ์๋น์ค์ด๋ค.
DefaultFormattingConversionService
๋ FormattingConversionService ์ ๊ธฐ๋ณธ์ ์ธ ํตํ, ์ซ์ ๊ด๋ จ ๋ช๊ฐ์ง ๊ธฐ๋ณธ ํฌ๋งทํฐ๋ฅผ ์ถ๊ฐํด์ ์ ๊ณตํ๋ค.
- ํ ์คํธ ์ฝ๋
public class FormattingConversionServiceTest {
@Test
void formattingConversionService() {
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);
}
}
FormattingConversionService ๋ ConversionService ๊ด๋ จ ๊ธฐ๋ฅ์ ์์๋ฐ๊ธฐ ๋๋ฌธ์ ๊ฒฐ๊ณผ์ ์ผ๋ก ์ปจ๋ฒํฐ๋ ํฌ๋งทํฐ๋ ๋ชจ๋ ๋ฑ๋กํ ์ ์๋ค. ๊ทธ๋ฆฌ๊ณ ์ฌ์ฉํ ๋๋ ConversionService ๊ฐ ์ ๊ณตํ๋ convert ๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.
์ถ๊ฐ๋ก ์คํ๋ง ๋ถํธ๋ DefaultFormattingConversionService
๋ฅผ ์์ ๋ฐ์ WebConversionService
๋ฅผ ๋ด๋ถ์์ ์ฌ์ฉํ๋ค.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// ์ฃผ์์ฒ๋ฆฌ ์ฐ์ ์์
// registry.addConverter(new StringToIntegerConverter());
// registry.addConverter(new IntegerToStringConverter());
registry.addConverter(new StringToIpPortConverter());
registry.addConverter(new IpPortToStringConverter());
// ์ถ๊ฐ
registry.addFormatter(new MyNumberFormatter());
}
}
MyNumberFormatter ๋ ์ซ์ -> ๋ฌธ์, ๋ฌธ์ -> ์ซ์๋ก ๋ณ๊ฒฝํ๊ธฐ ๋๋ฌธ์ ๋์ ๊ธฐ๋ฅ์ด ๊ฒน์น๋ค. ์ฐ์ ์์๋ ์ปจ๋ฒํฐ๊ฐ ์ฐ์ ํ๋ฏ๋ก ํฌ๋งทํฐ๊ฐ ์ ์ฉ๋์ง ์๊ณ , ์ปจ๋ฒํฐ๊ฐ ์ ์ฉ๋๋ค.
์คํ๋ง์ ์๋ฐ์์ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ๋ ํ์ ๋ค์ ๋ํด ์ ๋ง์ ํฌ๋งทํฐ๋ฅผ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ๋ค.
๊ทธ๋ฐ๋ฐ ํฌ๋งทํฐ๋ ๊ธฐ๋ณธ ํ์์ด ์ง์ ๋์ด ์๊ธฐ ๋๋ฌธ์, ๊ฐ์ฒด์ ๊ฐ ํ๋๋ง๋ค ๋ค๋ฅธ ํ์์ผ๋ก ํฌ๋งท์ ์ง์ ํ๊ธฐ๋ ์ด๋ ต๋ค.
์คํ๋ง์ ์ด๋ฐ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์ ๋ ธํ ์ด์ ๊ธฐ๋ฐ์ผ๋ก ์ํ๋ ํ์์ ์ง์ ํด์ ์ฌ์ฉํ ์ ์๋ ๋งค์ฐ ์ ์ฉํ ํฌ๋งทํฐ ๋ ๊ฐ์ง๋ฅผ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ๋ค.
-
@NumberFormat
: ์ซ์ ๊ด๋ จ ํ์ ์ง์ ํฌ๋งทํฐ ์ฌ์ฉ,NumberFormatAnnotationFormatterFactory
-
@DateTimeFormat
: ๋ ์ง ๊ด๋ จ ํ์ ์ง์ ํฌ๋งทํฐ ์ฌ์ฉ, Jsr310DateTimeFormatAnnotationFormatterFactory -
FormatterController
@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";
}
@Data
static class Form {
@NumberFormat(pattern = "###,###")
private Integer number;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime localDateTime;
}
}
- formatter-from.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form th:object="${form}" th:method="post">
number <input type="text" th:field="*{number}"><br/>
localDateTime <input type="text" th:field="*{localDateTime}"><br/>
<input type="submit"/>
</form>
</body>
</html>
- formatter-view.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<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>
</body>
</html>
- ๊ฒฐ๊ณผ
${form.number}: 10000
${{form.number}}: 10,000
${form.localDateTime}: 2021-01-01T00:00:00
${{form.localDateTime}}: 2021-01-01 00:00:00
๋ฉ์์ง ์ปจ๋ฒํฐ(HttpMessageConverter)
์๋ ์ปจ๋ฒ์ ์๋น์ค๊ฐ ์ ์ฉ๋์ง ์๋๋ค.
ํนํ ๊ฐ์ฒด๋ฅผ JSON์ผ๋ก ๋ณํํ ๋ ๋ฉ์์ง ์ปจ๋ฒํฐ๋ฅผ ์ฌ์ฉํ๋ฉด์ ์ด ๋ถ๋ถ์ ๋ง์ด ์คํดํ๋๋ฐ, HttpMessageConverter ์ ์ญํ ์ HTTP ๋ฉ์์ง ๋ฐ๋์ ๋ด์ฉ์ ๊ฐ์ฒด๋ก ๋ณํํ๊ฑฐ๋ ๊ฐ์ฒด๋ฅผ HTTP ๋ฉ์์ง ๋ฐ๋์ ์ ๋ ฅํ๋ ๊ฒ์ด๋ค.
์๋ฅผ ๋ค์ด์ JSON์ ๊ฐ์ฒด๋ก ๋ณํํ๋ ๋ฉ์์ง ์ปจ๋ฒํฐ๋ ๋ด๋ถ์์ Jackson ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ค. ๊ฐ์ฒด๋ฅผ JSON์ผ๋ก ๋ณํํ๋ค๋ฉด ๊ทธ ๊ฒฐ๊ณผ๋ ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋ฌ๋ฆฐ ๊ฒ์ด๋ค. ๋ฐ๋ผ์ JSON ๊ฒฐ๊ณผ๋ก ๋ง๋ค์ด์ง๋ ์ซ์๋ ๋ ์ง ํฌ๋งท์ ๋ณ๊ฒฝํ๊ณ ์ถ์ผ๋ฉด ํด๋น ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ ๊ณตํ๋ ์ค์ ์ ํตํด์ ํฌ๋งท์ ์ง์ ํด์ผ ํ๋ค.
๊ฒฐ๊ณผ์ ์ผ๋ก ์ด๊ฒ์ ์ปจ๋ฒ์ ์๋น์ค์ ์ ํ ๊ด๊ณ๊ฐ ์๋ค. ์ปจ๋ฒ์ ์๋น์ค๋ @RequestParam , @ModelAttribute , @PathVariable , ๋ทฐ ํ ํ๋ฆฟ ๋ฑ์์ ์ฌ์ฉํ ์ ์๋ค.
- ํ ๋น์ ์คํ๋ง3