Backend

Spring Core (3) Abstraction

wrallee 2020. 7. 2. 00:48

6. Abstraction

A. Resource

Resource는 java.net.URL 을 추상화 한 인터페이스로, 스프링 내부에서 정말 많이 사용된다. 단적인 예로 new ClassPathXmlApplicationContext("abc.xml") 생성자를 호출 할 경우 내부적으로 파일명(문자열)을 Resource 객체로 변환한다.

Resource로 추상화함으로써 클래스 패스, 웹 루트 등 다양한 경로의 리소스를 손쉽게 읽어올 수 있고, 해당 리소스에 대한 부가적인 정보도 쉽게 얻을 수 있다

Resource의 메소드

  • getInputStream()
  • exist()
  • isOpen()
  • getDescription()

Resource의 구현체

  • UrlResource
  • ClassPathResource
  • FileSystemResource
  • ServletContextResource

Resource의 타입은 ApplicationContext의 타입에 따라 결정되며, 일반적인 웹 애플리케이션의 경우 WebApplicationContext를 사용하기 때문에 ServletContextResource가 사용된다. location에 classpath, file:// 등의 접두사를 붙여 명시적으로 리소스 타입을 지정할 수 있다.

B. Validation

org.springframework.validation.Validator를 상속받아 애플리케이션 내에서 각종 검증 작업을 수행할 수 있다. supports 메소드와 validate 메소드를 구현하여 활용한다. validate 메소드 안에서 errors 파라미터의 메소드를 호출하며 검증할 수 있다. ValidationUtils를 사용하면 검증로직을 더욱 편리하게 구현할 수도 있다.

  • supports(Class clazz): 인자로 받은 클래스를 지원하는지 확인하는 메소드
  • validate(Object obj, Error e): 실제 검증로직을 수행하는 메소드

[EventValidator.java]

public class EventValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return Event.class.equals(clazz);
    }
    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "title", "notempty", "Empty title is not allowed.");
    }
}

[AppRunner.run()]

Event event = new Event();
EventHandler eventHandler = new EventValidator();
Errors errors = new BeanPropertyBindingResult(event, "event");

eventValidator.validate(event, errors);
System.out.println(errors.hasErrors());

errors.getAllErrors().forEach(e -> {
    System.out.println("===== error code =====");
    Arrays.stream(e.getCodes()).forEach(System.out::println);
    System.out.println(e.getDefaultMessage());
});

(권장)Annotation을 활용한 Validation

스프링 부트 2.0.5 이후부터 LocalValidationFactoryBean이 빈으로 자동 생성된다. 따라서 Validator를 선언하여 주입받아 validator.validate(...)를 호출하여 검증할 수 있다. 이 경우 Event.java는 아래와 같이 구현한다.

public class Event {
    @NotEmpty
    String title;
    @NotNull @Min(0)
    Integer limit;
    @Email
    String email;

    ...
}

C. Data Binding

데이터바인딩이란 프로퍼티 값을 타겟 객체에 설정하는 기능이다. 달리말하면 사용자 입력값을 변환하여 도메인 모델에 매핑하는 것을 말한다. 스프링에서는 org.springframework.validation.DataBinder 인터페이스를 사용하여 데이터바인딩을 구현할 수 있다.

일반적인 변환의 경우 대부분 스프링이 알아서 변환한다. 바인딩을 커스터마이징 하고 싶다면 아래와 같이 구현할 수 있다.

PropertyEditor

스프링 3.0 이전까지는 PropertyEditorSupport를 상속받아 getAsText(), setAsText(...) 를 구현하여 사용했다. 구현 시 setValue(...), getValue()를 사용하여 저장하고 꺼내쓰는데, 문제는 이 저장소(객체)를 여러 스레드에서 사용할 경우 Thread-safe를 보장하지 않는다. 따라서 싱글톤 빈으로 만들어서는 안되며, 만들어도 Thread 스코프 단위로 돌아가도록 만들어야 한다.

public class EventPropertyEditor extends PropertyEditorSupport {
    @Override
    public String getAsText() {
        return ((Event)getValue()).getTitle();
    }
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        int id = Integer.parseInt(text);
        Event event = new Event();
        event.setId(id);
        setValue(event);
    }
}

*PropertyEditor는 Object와 String간의 변환만 가능하다.

Converter & Formatter

Converter<S, T> 일 때 S를 T로 변환하는 인터페이스로, 상태정보(저장소)를 사용하지 않기 때문에 Thread-safe를 보장한다. ConverterRegistry에 등록하여 사용한다.

public class EventConverter {
    public static class StringToEventConverter implements Converter<String, Event> {
        @Override
        public Event convert(String source) {
            return new Event(Integer.parseInt(source));
        }
    }
}

Formater는 Object와 String간의 변환을 담당하는데, Locale에 따른 다국어 기능도 제공한다. Converter와 마찬가지로 Thread-safe하며, FormatterRegistry에 등록하여 사용한다.

public class EventFormatter implements Formatter<Event> {
    @Override
    public Event parse(String text, Locale locale) throws ParseException {
        ...
        return event;
    }
    @Override
    public String print(Event object, Locale locale) {
        return object.getId().toString();
    }
}

[WebConfig.java]

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new EventConverter.StringToEventConverter());
        registry.addFormatter(new EventFormatter());
    }
}

ConversionService

이런식으로 등록 된 Converter와 Formatter를 ConversionService를 통해 Thread-safe하게 사용할 수 있다. 스프링부트에서는 DefaultFormattingConversionService를 상속받은 WebConversionService를 빈으로 제공한다.

@Autowired
ConversionService conversionService;
@Override
public void run(ApplicationArguments args) throws Exception {
    ConversionService.canConvert(...);
}

Springboot에서는 별도의 Java Config를 만들지 않아도 Formatter와 Converter를 빈으로 등록하면 알아서 찾아 ConversionService에 등록해준다. 등록되어있는 Converter/Formater를 보려면 conversionService.toString()으로 찍어보면 된다.

7. SpEL(Spring Expression Language)

객체 그래프를 조회하고 조작하는 기능을 제공하는 표현식. Unified EL과 비슷하지만, 메소드 호출, 문자열 템플릿 기능 등을 추가적으로 제공한다. 스프링 3.0부터 지원된다.

아래와 같은 형태로 사용한다.

  • #{"표현식"}
  • ${"프로퍼티"}
  • 표현식은 프로퍼티를 가질 수 있지만 반대는 안된다.
    • #{${my.data} + 1}
  • 이외에도 여러 객체를 지원하며, 문자열/리스트/맵 등의 리터럴도 존재한다.

SpEL은 @Value 어노테이션, @ConditionalOnExpression 어노테이션 및 스프링 시큐리티, 스프링 데이터에서도 적극 활용할 수 있다.

아래와 같이 SpelExpressionParser를 사용하여 SpEL을 구현할 수 있다.

ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("2 + 100");
Integer value = expression.getValue(Integer.class);

이외에 properties, methods, fields를 다루거나 타입 변환을 수행하기 위해서는 EvaluationContext 인터페이스를 활용하면 된다. 스프링에서는 EvaluationContext의 구현체로 SimpleEvaluationContextStandardEvaluationContext를 제공한다.