Parameter 데이터 바인딩

YH·2023년 3월 26일
0

✅ Parameter Data Binding

클라이언트로부터 요청이 들어왔을 때 요청 데이터를 바인딩해주는 요소들

  • @PathVariable
  • @RequestParam
  • @RequestBody
  • @ModelAttribute & Model Object
  • @ResponseBody -> 요청에 대한 처리 후 응답 데이터를 바인딩

PropertyEditor, Converter, Formatter 추가

◾ @PathVariable

  • URL 경로의 일부를 파라미터로 사용할 때 쓰는 어노테이션
  • 템플릿 변수의 값을 추출하고 그 값을 메소드 변수에 할당하는데 사용 됨
  • @PathVariable은 오직 하나만 받을 수 있음
@RequestMapping("/board/{id}", method=RequestMethod.GET)
public String getBoard(@PathVariable(value="id") String id) {
	...
}

주의

  • null이나 공백이 들어가는 파라미터는 사용하지 말 것
  • @PathVariable로 파라미터를 전달 받을 때 '.' 가 포함되어 있을 경우, . 뒤에 문자가 잘림

◾ @RequestParam

  • 쿼리 스트링을 파라미터로 받아올 때 사용하는 어노테이션
  • 보통 Get Method일 때 사용되지만, HTML Form 태그에서 POST를 사용할 때도 @RequestParam으로 값을 받을 수 있다.
@GetMapping("/board)
public String gerBoard(@RequestParam(name="id",required=true,defaultValue="") String id,
					   @RequestParam(name="name",required=false) String name) {
	...
}
  • HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능
  • String, int 등의 단순 타입이면 @RequestParam 도 생략 가능 -> 헷갈릴 수 있기 때문에 권장하지 않음
  • @RequestParam 애노테이션을 생략하면 스프링 MVC는 내부에서 required=false 를 적용
  • required가 true인 값이 넘어오지 않으면 Bad Request(400 Error) 발생

◾ @RequestBody

  • Json 형식의 데이터를 원하는 타입의 객체로 변환하여 사용할 수 있도록 하는 어노테이션
  • HTTP request Body를 자바 객체로 받을 수 있음
  • @RequestBody는 생략 불가능, 생략하게 되면 @ModelAttribute로 적용됨
  • HTTP 요청 본문은 HttpMessageConverter에 의해 처리되는데, 요청 시 전달받은 HTTP의 Content-Type 헤더에 선언된 콘텐츠 타입을 기준으로 메소드 인자값을 처리한다.
@PostMapping(value="/board")
public BaseResponse addPost(@RequestBody BoardInput input) {
        ...
    }

◾ @ModelAttribute & Model Object

  • Model 객체는 서버에서 응답받은 데이터를 View로 전달할 때 사용되는 객체
  • @ModelAttribute는 파라미터에 정의된 타입의 객체를 자동으로 생성해준다.
  • 요청 파라미터 객체의 Property를 찾아 해당 Property의 값을 바인딩 해준다.
  • @ModelAttribute를 사용하면, Model 객체에 자동으로 추가 해주므로 model.addAttribute()를 생략 가능
@ResponseBody
@RequestMapping("/board")
public String modelAttributeV1(@ModelAttribute BoardData boardData) {
  log.info("username={}, age={}", boardData.getUsername(), boardData.getAge());
  ...
}

◾ @ResponseBody

  • @ResponseBody 를 사용하면 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달 가능
  • View를 반환하지 않고 Http 바디 정보를 직접 반환한다.
  • HttpMessageConverter를 통해 요청 HTTP Content-Type 헤더에 선언된 데이터 형식에 맞게 메소드 리턴값을 반환
  • @RequestBody에서 produces 또는 consumes 속성을 통해 리턴 값을 명시할 수 있음
    • produces: Accecpt 헤더를 참고하여 Accecpt 헤더와 produces 속성에 명시된 데이터 타입이 일치해야만 메소드 리턴 값을 반환한다.
    • consumes: Content-Type 헤더와 consumes 속성에 명시된 데이터 타입이 일치해야만 메소드 리턴값을 반환한다.
@ResponseBody
@PostMapping("/board")
public HelloData getData(@RequestBody HelloData data) {
	log.info("username={}, age={}", data.getUsername(), data.getAge());
	...
}

✅ 스프링과 타입 변환

✔️ 개발을 하다보면 타입을 변환해야 하는 경우가 상당히 많은데, Spring에서는 이렇게 필요한 타입으로 데이터 바인딩 해주는 여러 인터페이스를 제공함
✔️ 인터페이스 종류에는 PropertyEditor, Converter, Formatter가 있음

◾ PropertyEditor

✔️ Spring이 제공하는 DataBinder 인터페이스를 통해 사용됨
✔️ Spring 3 이전까지 DataBinder가 변환 작업에 사용한 인터페이스
✔️ 값(상태 정보)을 저장하고 있어 동시성 문제가 있어서 타입을 변환할 때 마다 객체를 계속 생성해야 하는 단점이 있음
✔️ 위의 단점으로 인해 서로 다른 쓰레드끼리 값이 공유되므로 싱글톤 Scope 빈으로 등록하여 사용할 수 없음
✔️ Object - String 간의 변환만 가능
✔️ PropertyEditor 인터페이스를 상속해도 되지만, 구현해야할 메소드가 많아 PropertyEditorSupport 상속으로 필요한 메소드만 구현하는 것이 나음

예제

import java.beans.PropertyEditorSupport;
 
// implements PropertyEditor를 해도 되지만 구현해야할 메소드가 굉장히 많다.
// 따라서 extends PropertyEditorSupport로 필요한 메소드만 선택해서 구현한다.
// PropertyEditorSupport를 상속하면 보통 getAsText(), setAsText()를 override함
public class EventEditor extends PropertyEditorSupport {
    // setAsText() : String -> Object
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        // 사용자가 입력한 문자열을 int로 변환하여 Event 객체를 생성한 뒤 setValue() 호출
        setValue(new Event(Integer.parseInt(text)));
    }
 
    // getAsText() : Object -> String
    @Override
    public String getAsText() {
        Event event = (Event) getValue();
        return event.getId().toString();
    }
}

◾ Converter

✔️ S(Source) 타입을 T(Target) 타입으로 변환할 수 있는 Generic한 변환기
✔️ Spring 3 부터 도입됨
✔️ Spring이 제공하는 ConversionService 인터페이스를 통해 사용 (ConversionService는 개별 컨버터를 모아두고 컨버터들을 묶어서 편리하게 사용할 수 있는 기능을 제공하는 인터페이스)
✔️ 값(상태 정보)를 저장하지 않으므로 동시성 문제가 발생하지 않음 (Thread-Safe)
✔️ Bean으로 등록하여 사용 가능

Convert 인터페이스

package org.springframework.core.convert.converter;

public interface Converter<S, T> {
	T convert(S source);
}

Converter 구현 예시

public class BoardConverter {
      public static class StringToBoardInputConverter implements Converter<String, BoardInput> {
      @Override
      public BoardInput convert(String source) {
          return new BoardInput(Integer.parseInt(source));
      }
  }

  public static class BoardInputToStringConverter implements Converter<BoardInput, String> {
      @Override
      public String convert(BoardInput source) {
          return source.getId() + ";" + source.getName());
      }
  }
}

✔️ ConversionService

  • DefaultConversionService는 스프링이 제공하는 ConversionService 인터페이스의 구현체인데, 이 것은 컨버터를 등록하는 기능과 사용하는 기능을 따로 제공한다.
  • 컨버터 등록 부분(ConverterRegistry)과 사용 부분(ConverterService)이 분리되어, 사용자는 ConversionService(컨버터 사용)만 의존하면 되기 때문에 ISP 원칙이 성립한다.
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);
    
	<T> T convert(@Nullable Object source, Class<T> targetType);
	Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
}

✅ 스프링에 Converter 적용 방법

✔️ 컨버터를 실제로 스프링에 등록해서 사용하는 방법을 알아본다.

1. Web Config에 등록하는 방법

  • WebMvcConfigurer 인터페이스를 상속받아 아래와 같이 사용
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
@Configuration
public class WebConfig implements WebMvcConfigurer {
 
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new BoardConverter.StringToBoardInputConverter());
        registry.addConverter(new BoardConverter.BoardInputToStringConverter());
    }
}

- 참고로, 스프링에는 이미 여러 기본 컨버터들을 제공하는데, 이렇게 직접 컨버터를 추가하면
기본 컨버터보다 높은 우선 순위를 가진다.

처리 과정

  • @RequestParam을 사용했을 때의 예시 : @RequeustParam 은 @RequestParam을 처리하는 ArgumentResolver인 RequestParamMethodArgumentResolver에서 ConversionService를 사용해서 타입을 변환한다.

2. Converter를 빈으로 등록하는 방법 - Spring Boot 사용 시

  • 아래와 같이 @Component를 통해 Bean으로 등록하여 사용
 @Component
 public static class StringToBoardInputConverter implements Converter<String, BoardInput> {
      @Override
      public BoardInput convert(String source) {
          return new BoardInput(Integer.parseInt(source));
      }
  }

⬆️ Convert를 적용하면, 스프링이 내부에서 사용하는 ConversionService에 해당 Converter들을 추가해줌

✔️ 참고 - 용도에 따라 다양한 방식의 컨버터를 사용 가능 (Spring에서 제공)

  • Converter -> 기본 타입 컨버터
  • ConverterFactory -> 전체 클래스 계층 구조가 필요할 때
  • GenericConverter -> 정교한 구현, 대상 필드의 어노테이션 정보 사용 가능
  • ConditionalGenericConverter -> 특정 조건이 참인 경우에만 발생

✅ 뷰 템플릿에 컨버터 적용하기

✔️ Thymeleaf(뷰 템플릿)은 뷰를 렌더링 시에 컨버터를 적용해서 렌더링 하는 방법을 지원한다.

@Controller
public class ConverterController {
    @GetMapping("/converter-view")
    public String converterView(Model model) {
        model.addAttribute("member", 10000);
        model.addAttribute("ipPort", new IpPort("127.0.0.1", 8000));

        return "converter-view";
    }
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</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>
  <li>${{ipPort}}: <span th:text="${{ipPort}}" ></span></li>
</ul>
</body>
</html>

◾ Formatter

✔️ 객체를 특정한 포맷에 맞추어 문자로 출력하거나 또는 그 반대의 역할을 하는 것에 특화된 기능이 바로 Fomatter이다. Converter의 특별한 버전이라고 보면 된다.
✔️ Spring 3부터 도입됨
✔️ Object - String 간 변환을 담당하는 Web에 특화된 인터페이스 -> 웹에서는 문자를 다른 타입으로 변환하거나, 다른 타입을 문자로 변환하는 상황이 대부분임
✔️ Spring이 제공하는 ConversionService 인터페이스를 통해 사용
✔️ 값(상태 정보)를 저장하지 않으므로 동시성 문제가 발생하지 않음 (Thread-Safe)
✔️ Bean으로 등록하여 사용 가능
✔️ 문자열을 Locale에 따라 다국화 처리 기능 제공 (Optional)

  • 웹 애플리케이션의 변환 예시
    • 화면에 숫자 출력 시에, 숫자에 쉼표를 추가해서 1,000 처럼 출력해야 할 때나 혹은 그 반대의 상황
    • 날짜 객체를 문자인 "2021-01-01 10:50:11"와 같이 출력하거나 그 반대의 상황

Converter vs Formatter

  • Convert는 범용(객체 <-> 객체)
  • Formatter는 문자에 특화 (객체 <-> 문자) + 현지화(Locale)

Formatter 인터페이스

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> {
}

Formatter 사용 예시

public class SampleFormatter implements Formatter<Number> {

  @Override
  //Number는 Interger나 Long과 같은 숫자 타입의 부모 클래스
  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 SampleFormatterTest {

SampleFormatter formatter = new SampleFormatter();

  @Test
  void parse() throws ParseException {
    Number result = formatter.parse("1,000", Locale.KOREA);
    assertThat(result).isEqualTo(1000L); //parse()의 결과가 Long 이므로 비교 시 L 접미사를 넣어주어야 함
  }
  
  @Test
  void print() {
    String result = formatter.print(1000, Locale.KOREA);
    assertThat(result).isEqualTo("1,000");
  }
}

✔️ 참고 - 용도에 따라 다양한 Formatter 사용 가능 (Spring에서 제공)


Formatter를 지원하는 ConversionService

✔️ FormattingConversionService는 Formatter를 지원하는 Conversion Service임
해당 서비스를 사용하면 내부에서 어댑터 패턴을 사용하여 Formatter가 Converter처럼 동작하도록 지원한다.
✔️ DefaultFormattingConversionService는 FormattingConversionService에 기본적인 통화, 숫자 관련 몇 가지 기본 Formatter를 추가해서 제공한다.
✔️ FormattingConversionService는 ConversionService 관련 기능을 상속받기 때문에 결과적으로 Converter 및 Formatter 둘 다 등록이 가능
✔️ Spring Boot에서는 DefaultFormattingConversionService를 상속 받은 WebConversionService을 내부에서 사용함

✅ 스프링에 Formatter 적용 방법

1. Web Config에 등록하는 방법

  • Converter와 유사하며, addFormatter를 통해 formatter를 추가
@Configuration
public class WebConfig implements WebMvcConfigurer {

	@Override
    public void addFormatters(FormatterRegistry registry) {
  		registry.addFormatter(new MyNumberFormatter());
 	}
}

2. Converter를 빈으로 등록하는 방법 - Spring Boot 사용 시

  • @Component를 사용해 Bean으로 등록하여 사용
@Component
public class SampleFormatter implements Formatter<Number> {

  @Override
  //Number는 Interger나 Long과 같은 숫자 타입의 부모 클래스
  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);
  }
}

Spring이 제공하는 기본 Formatter

✔️ 객체의 각 필드마다 다른 형식으로 포맷을 지정하고 싶은 경우, 아래처럼 스프링이 제공하는 어노테이션 기반의 형식을 지정해서 유용하게 사용할 수 있음

  • @NumberFormat : 숫자 관련 형식 지정 Formatter 사용, NumberFormatAnnotationFormatterFactory
    • @NumberFormat(pattern = "###,###")
  • @DateTimeFormat : 날짜 관련 형식 지정 Formatter 사용, Jsr310DateTimeFormatAnnotationFormatterFactory
    • @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Data
    static class Form {
        @NumberFormat(pattern = "###,###")
        private Integer number;
        
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        private LocalDateTime localDateTime;
    }

주의할 점

✔️ 메시지 컨버터(HttpMessageConverter)에는 Conversion Service가 적용되지 않음
✔️ HttpMessageConverter의 역할은 HTTP 메시지 바디의 내용을 객체로 변환하거나 객체를 HTTP 메시지 바디에 입력하는 것이다. 예를 들어 Json을 객체로 변환할 때 Jackson 같은 라이브러리를 사용하는데, 이 때 포맷을 변경하고 싶을 때는 해당 라이브러리가 제공하는 설정을 통해 포맷을 지정하는 것이지, Conversion Service와는 무관함
✔️ Conversion Service는 @RequestParam, @ModelAttribute, @PathVariable, 뷰 템플릿 등에서 사용할 수 있다.




<참고 Reference>

profile
하루하루 꾸준히 포기하지 말고

0개의 댓글