스프링이 제공하는 추상화 기술에 대해 알아보자. 순서는 다음과 같다.
- Resource 추상화
- Validation 추상화
- Data Binding 추상화
1. Resource 추상화
Resource 추상화란 무엇인가?
java.net.URL이라는 클래스를 Spring.core.io.resource라는 클래스로 감싸서 Low-level에 있는 리소스에 접근하는 기능이다.
(java.net.URL을 classpath기준으로가져오기 위해서)
@Override
public void run(ApplicationArguments args) throws Exception {
Resource resource = resourceLoader.getResource("classpath:test.txt"); // #1. Resource 사용 예제
System.out.println("resource exits?" + resource.exists());
System.out.println("resource : " + resource);
}
이를 추상화 하는 이유는 다음과 같다.
- classpath기준으로 리소스를 읽어오기 위해
- ServletContext를 기준으로 상대경로를 읽어오기 위해
- 특별한 URL 접미사를 만들어 사용하기 위해
Resource는 아까 소스 처럼 getResource부분에서 "classpath:test.txt" 처럼 구현체에 따라 접두어가 달라지는데,
Resource의 구현체는 아래와 같은 것들이 있다.
- UrlResource : java.net.URL에서 기본으로 지원하는 프로토콜 ( ex.http, https, ftp, file, jar)
- ClassPathResource : classpath 접두어로 넘겨받은 리소스를 찾는다.
- FileSystemResource : 파일시스템에서 리소스를 찾는다.
- ServletContextResource: Application 루트에서 상대 경로로 리소스를 찾는다.
그렇다면 읽어온 Resource의 타입은 어떻게 결정될까? 바로 location 문자열과 ApplicationContext의 타입에 따라 결정된다.
만약 ClassPathXmlApplicationContext를 사용해서 Xml파일로 ApplicationContext를 등록했다면,
Resource는 자동으로. ClassPathResource 타입으로 결정된다.
만약 FileSystemXmlApplicationContext를 사용해서 등록됐다면, Resource는 FileSystemResource 타입으로 결정된다.
만약 WebApplicationContext를 사용해서 등록됐다면, Resource는 ServletContextResource 타입으로 결정된다.
ApplicationContext의 타입에 상관없이 리소스 타입을 강제하려면 java.net.URL의 접두어나 classpath: 접두어를 사용할 수 있다.
@Override
public void run(ApplicationArguments args) throws Exception {
Resource resource = resourceLoader.getResource("classpath:config.xml"); //
Resource resource2 = resourceLoader.getResource("file://config2.txt"); //
System.out.println(resource.getClass());
// class org.springframework.core.io.ClassPathResource
System.out.println(resource2.getClass());
//class org.springframework.core.io.FileUrlResource
}
2. Validation 추상화
validation 추상화란, 객체 검증용 인터페이스이다. 여기서 객체 검증이란 Bean 검증인데 JEE 표준 중에 하나이다. 아래의 사이트에서 Java Bean Validation Api에 관련한 자바 문서를 확인할 수 있다.
https://docs.jboss.org/hibernate/beanvalidation/spec/2.0/api/
이러한 Validator 객체를 상속받고 난 후에 두가지 메소드를 구현해주어야 하는데, supports 메소드와 validate 메소드이다.
public class UserLoginValidator implements Validator {
private static final int MINIMUM_PASSWORD_LENGTH = 6;
public boolean supports(Class clazz) {
//#1. supports method
// 인자로 넘어온 clazz 가 Validator가 구현이 되어있는지 아닌지를 반환해주는 함수
return UserLogin.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
//#2. validate method
// 실제 validating logic 구현
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "field.required");
UserLogin login = (UserLogin) target;
if (login.getPassword() != null
&& login.getPassword().trim().length() < MINIMUM_PASSWORD_LENGTH) {
errors.rejectValue("password", "field.min.length",
new Object[]{Integer.valueOf(MINIMUM_PASSWORD_LENGTH)},
"The password must be at least [" + MINIMUM_PASSWORD_LENGTH + "] characters in length.");
}
}
}
기존에 Validation을 했던 방식은 다음과 같다. 이벤트 클래스를 생성하고, 해당 클래스의 유효성을 검증하는 구현체를 하나 만들고, 호출부에서 이 둘을 호출해 주는 것이다.
// #1. event class
public class Event {
Integer id;
String title;
//... getter & setter 생략
}
// #2. validator 를 상속받은 검증용 객체
public class EventValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Event.class.equals(clazz);
//#1. 인자로 넘긴 객체가 event 클래스인지 아닌지 검색
}
@Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors,"title","에러코드 나간다 -> title is empty","");
}
}
// 3. 호출부
@Component
public class AppRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
Event event = new Event();
EventValidator eventValidator = new EventValidator();
Errors errors = new BeanPropertyBindingResult(event,"event");
eventValidator.validate(event, errors);
System.out.println(errors.hasErrors());
errors.getAllErrors().forEach(e->{
System.out.println("====err code===");
Arrays.stream(e.getCodes()).forEach(System.out::println);
System.out.println(e.getDefaultMessage());
});
}
}
그리고 스프링 부트 2.0.5 이상부터는 @Validator 를 사용해서 검증 과정을 간단하게 줄일 수 있다.
먼저 build.gradle 파일에 validation 설정을 해준다.
소스코드로 알아보자.
// #1. validation 을 주입받는 event 호출부
@Component
public class AppRunner implements ApplicationRunner {
@Autowired
Validator validator; // #1.1 validator 주입
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("validation class" + validator.getClass());
Event evt = new Event();
evt.setTitle("");
evt.setEmail("222");
evt.setLimit(-1);
Errors errors = new BeanPropertyBindingResult(evt, "event");
validator.validate(evt, errors);
errors.getAllErrors().forEach(e->{
System.out.println("======errors=====");
Arrays.stream(e.getCodes()).forEach(System.out::println);
});
}
}
// #2. Event 클래스에서 애노테이션으로 검증 로직 추가
public class Event {
Integer id;
@NotEmpty
String title;
@Min(0)
Integer limit;
@Email
String email;
// ..getter & setter 생략
}
아래의 결과창에서 Validator의 class가 LocalValidationFactoryBean으로 주입된걸 확인할 수 있다.
또한 validation의 결과도 추출되었다.
3. 데이터 바인딩 추상화 : PropertyEditor
사용자가 입력한 값을 애플리케이션 도메인 모델에 변환해서 넣어주는 기능. 보통 사용자가 입력한 값은 String 이기 때문에 DataBinder 인터페이스를 통해 도메인의 변수타입에 맞춰 변환해주어야 한다. 먼저 기존에 사용하던 PropertyEditorSupport를 상속받는 Binding 전용 객체를 생성하고, 이를 컨트롤러에서 초기화할 때 호출해 주는 방식을 살펴보자.
// 1. Event.java : Event 객체를 하나 생성해주자
public class Event {
Integer id;
@NotEmpty
String title;
@Min(0)
Integer limit;
@Email
String email;
public Event(Integer id){
this.id = id;
}
... 다른 getter, setter 생략
}
// #2. EventController.java
@RestController
public class EventController {
@InitBinder
public void init(WebDataBinder webDataBinder){
// #2.2 String to Event Databinding을 위해 EventEditor를 바인드해준다.
webDataBinder.registerCustomEditor(Event.class, new EventEditor());
}
@GetMapping("/event/{event}")
public String getEvent(@PathVariable Event event){
//#2.1 화면에서는 event가 String으로 날라오는데, 이를 event객체로 받아줘야 함.
System.out.println("event : " + event);
return event.getId().toString();
}
}
// 3. EventEditor.java
// 얘는 절대로 Bean으로 등록하지 않음. 싱글톤일 경우 다른 객체에 데이터를 바인딩 시킬 수 있기 때문에 thread safe 하지 않음.
public class EventEditor extends PropertyEditorSupport {
// 3.1 PropertyEditor 상속받아야된다.
@Override
public String getAsText() {
Event event = (Event)getValue(); // 3.2 데이터를 받아서 event형으로 변환
return event.getId().toString();
}
@Override
public void setAsText(String text) throws IllegalStateException{
// 3.2 String에서 int 추출 후 생성자 호출
setValue(new Event(Integer.parseInt(text)));
}
}
// #4. EventControllerTests.java
@RunWith(SpringRunner.class)
@WebMvcTest
public class EventControllerTests {
@Autowired
MockMvc mockMvc;
@Test
public void getTest() throws Exception{
// #4.1 pathVariable에 String으로 들어가도(Event형 X) 정상 테스트 작동 확인.
mockMvc.perform(get("/event/1"))
.andExpect(status().isOk())
.andExpect(content().string("1"));
}
}
단, PropertyEditorSupport를 사용한 방식은 String <--> 객체로의 Binding만 가능하기 때문에 제약이 많다.
조금 더 Type General 한 Binding을 위해서는 Converter와 Formatter를 사용하면 된다.
// 1. EventConverter.java
// Bean으로 등록해주면 자동으로 Spring에서 Converter로 인식
public class EventConverter {
@Component
public static class StringToEventConverter implements Converter<String,Event> {
@Override
public Event convert(String source) {
return new Event(Integer.parseInt(source));
}
}
@Component
public static class EventToStringConverter implements Converter<Event,String>{
@Override
public String convert(Event source) {
return source.getId().toString();
}
}
}
// #2. Formatter class
// Bean으로 등록하면 Spring이 인식 해준다.
@Component
public class EventFormatter implements Formatter<Event> {
@Override
public Event parse(String text, Locale locale) throws ParseException {
return new Event(Integer.parseInt(text));
}
@Override
public String print(Event object, Locale locale) {
return object.getId().toString();
}
}
AppRunner 클래스에서 Conversion 인터페이스를 주입받으면, 해당 인터페이스가 기본적으로 Converter와 Formatter를 각각 ConversionService와 FormatterRegitstry에 등록해준다. 이는 쓰레드 세이프하게 사용이 가능하다.
@Component
public class AppRunner implements ApplicationRunner {
@Autowired
ConversionService conversionService; // #ConverisonService 인터페이스
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println(conversionService.getClass().toString());
System.out.println(conversionService);
}
}
출력 결과는 아래와 같이 conversionService의 객체가 WebConversionService로 찍힌 것을 알 수 있다.
스프링 부트에서는 웹애플리케이션의 경우 기본적으로 DefaultFormattingConversionService를 Web으로 등록해주기 때문이다.
그리고 스프링부트에서 기본적으로 제공하는 형 변환 리스트가 출력되었다.
'Study > Spring' 카테고리의 다른 글
[스프링 웹 MVC 동작원리] Servlet과 DispatcherServlet의 동작원리 (0) | 2022.03.01 |
---|---|
스프링 핵심기술 06. AOP (0) | 2022.02.13 |
스프링 핵심기술 04. ApplicationContext가 상속받는 인터페이스들 (0) | 2022.02.06 |
스프링 핵심기술 03. 스프링 IoC 컨테이너 - Bean의 Scope (0) | 2022.02.03 |
스프링 핵심기술 02. 스프링 IoC 컨테이너 - @Autowire (0) | 2022.02.02 |