Spring

Resource / Validation

들어가기에 앞서 본 포스팅은 백기선님의 인프런 강의를 기반으로 작성된 포스팅임을 알려드립니다.


처음 스프링을 배우는 입장이라 정확하지 않은 정보가 있을 수 있습니다.

댓글을 통해 알려주신다면 최대한 빨리 피드백하도록 하겠습니다!

 

Resource 추상화

org.springframework.core.io.Resource : java.net.URL을 추상화한 클래스

추상화를 한 이유 : docs.spring.io

기존의 java.net.URL의 경우 classpath 기준의 상대경로로 읽어오는 기능 부재
ServletContext 기준의 상대경로로 읽어오는 기능 부재
구현의 복잡성과 편의성 메소드의 부족

 

 

Resource를 받아오는 상황으로 파일이나 URL에서 받아오는 경우를 생각해볼수 있다.

 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.core.io;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import org.springframework.lang.Nullable;

public interface Resource extends InputStreamSource {
    boolean exists();

    default boolean isReadable() {
        return this.exists();
    }

    default boolean isOpen() {
        return false;
    }

    default boolean isFile() {
        return false;
    }

    URL getURL() throws IOException;

    URI getURI() throws IOException;

    File getFile() throws IOException;

    default ReadableByteChannel readableChannel() throws IOException {
        return Channels.newChannel(this.getInputStream());
    }

    long contentLength() throws IOException;

    long lastModified() throws IOException;

    Resource createRelative(String var1) throws IOException;

    @Nullable
    String getFilename();

    String getDescription();
}

Resource 인터페이스의 내부를 살펴보면 몇 가지 주요 메소드를 확인할 수 있다.

 

  • InputStream에서 상속받는 getInputStream(),
  • resource의 존재여부를 판단하는 exists(),
  • resource를 작업하고있는 stream의 존재여부를 판단하는 isOpen(),
    • true : resource 누수를 방지하기 위해 stream은 한번 읽고 닫아야 한다.
    • false : 통상적인 resource 구현에서 InputStreamResource를 반환한다.
  • resource의 FQFN이나 URL을 표시해주는 getDescription()

이 외의 메소드는 실제 URL이나 File 객체를 얻을 수 있다.

 

 

 

이 인터페이스의 일부 구현체들은 resource에 쓰기작업을 수행할 수 있게 해주는 WritableResource를 함께 상속한다.

( ex :  FileSystemResource, FileUrlResource )

 

Resouce 인터페이스의 구현체들은 이곳에서 확인할 수 있다.

 

 

 

스프링에서 Resource는 ApplicationContext가 상속하고있는 ResourceLoader를 통하여 접근할 수 있다.

ApplicationContext 가 ResourcePatternResolver를 상속,
ResourcePatternResolver가 ResourceLoader를 상속.

 

 

 

ResourceLoader

 

이 ResourceLoader를 통하여 Resource 객체를 얻어올 수 있다.

그 외에도 CLASSPATH_URL_PREFIX를 통하여 "classpath:"라는 접두사를 지원한다는 사실도 알 수 있다.

 

 

 

 

ResourceLoader를 통한 getResource() 호출

 

실제 getResource() 요청 수행

디버그를 통하여 getResource()의 작동하는 과정을 살펴보면

Application을 통한 getResource -> null판단 후 실제 getResource로 이동 -> 실제 getResource 처리의 과정을 거친다.

 

 

 

 

@Component
public class MyRunner implements ApplicationRunner {

    @Autowired
    ApplicationContext context;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        Resource resource = context.getResource("classpath:mySample.txt");

        System.out.println(resource.getDescription());
        File file = resource.getFile();

        try(BufferedReader br = new BufferedReader(new FileReader(file))){
            System.out.println(br.readLine());
        }

    }
}

위 코드와 같이 resources 디렉토리 밑에 있는 "mySample.txt"를 ResourceLoader를 통하여 얻어오는게 가능하다.

 

 

 

파일경로 입력 시 사용가능한 접두사

 

접두사 예시 설명
classpath: classpath:com/myapp/config.xml classpath부터 경로를 읽는다
file: file:///data/config.xml FileSystemResource를 이용하여 filesystem에서 URL
https: https://myserver/logo.png URL에서 Resource를 읽는다
  /data/config.xml ApplicationContext에 의존한다.

 

 

 

 

 

 

 

ResourcePatternResolver

 

ResourcePatternResolver

 

ApplicationContext가 상속하고있는 클래스이며, ResourceLoader의 확장판이다.

classpath 패턴밑의 모든 Resource를 얻어올 수 있는 getResources() 를 제공한다.

 

 

 

 

 

classpath vs classpath*

 

ResourcePatternResolver를 확인해보면 " classpath*: " 라는 경로를 확인할 수 있다.

 

@Component
public class MyRunner implements ApplicationRunner {

    @Autowired
    ApplicationContext context;

    @Override
    public void run(ApplicationArguments args) throws Exception {


        Resource[] resources = context.getResources("classpath*:");

        Arrays.stream(resources).forEach(resource -> {
            try {
                System.out.println(resource.getURL().toString());
            } catch (IOException e) {
                e.printStackTrace();
            }
        });

    }
}

해당 경로를 이용하여 Resource들을 받은다음 출력해보면 이런 결과를 확인할 수 있다.

각 라인의 접두어를 살펴보면 file과 jar로 나뉘어 있는 것을 확인할 수 있다.

 

여기서 jar로 시작하는 라인 spring이 참조하고있는 jar파일들을 의미하며,

file로 시작하는 라인은 내가 생성해낸 프로젝트의 target/classes/ 를 의미한다.

 

 

classpath:

=> 현재 프로젝트에서 탐색을 하는 것 ( 한개 탐색 )

=> 현재 프로젝트가 실행 되었을 때 현재 classloader에 해당하는 경로의 리소스만 선택

 

 

classpath*:

=> 현재 프로젝트 + 참조하고있는 jar파일들에서 탐색하는 것 ( 여러 개 탐색 )

=> 현재 프로젝트가 실행 되었을 때 현재 classloader의 경로 뿐만 아니라 상위 classloader를 모두 검색하여 해당 경로의 리소스를 선택

 

참조 : okky.kr/article/286428

 

 

 

 

 

 

Validation 추상화

 

 

org.springframework.validation.validator : 객체를 검증하기위한 인터페이스

 

 

 public class UserLoginValidator implements Validator {

    private static final int MINIMUM_PASSWORD_LENGTH = 6;

    public boolean supports(Class clazz) {
       return UserLogin.class.isAssignableFrom(clazz);
    }

    public void validate(Object target, Errors errors) {
       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.");
       }
    }
 }

Spring api 문서를 확인해보면 스프링이 제공하는 Validator를 상속받는 클래스는 supports와 validate, 두 메소드를 재정의 해야한다.

 

 

supports( Class ) : Validator가 검증을 원하는 클래스를 검증할 수 있는지 판단한다.

validate(Object, Errors) : 실제 검증로직을 구현하는 메소드이며 검증에러가 발생하면 Errors 객체에 해당 에러를 넘긴다.

 

 

 

 

 

 

 

 

public class Event {

    private String name;

    private Integer age;

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

만약 Event클래스에서 name이 null 반드시 입력되어야하고, age는 1 ~ 100 사이의 값이어야 한다고 가정해보자.

이런 상황에서 생성된 Event 객체를 검증하기 위해 작성한 Validator는 아래와 같다.

 

 

 

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, "name", "name.empty");
        // name이라는 필드에 값이 있는지 판단

        Event event = (Event) target;

        Integer age = event.getAge();

        if(age <= 0){
            errors.rejectValue("event", "NegativeValue", "Negative");
        }else if(age > 100){
            errors.rejectValue("event", "event.tooOld", "TooOld");
        }
    }
}

 

여기서 사용된  ValidationUtils 라는 추상클래스는 Validator를 실행하거나 필드값이 비어있는지를 판단하는데 사용된다.

 

 

 

 

 

@Component
public class EventRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        Event event = new Event();

        event.setAge(1);
        event.setName(null);

        EventValidator validator = new EventValidator();
        validator.supports(Event.class);

        Errors errors = new BeanPropertyBindingResult(event, "event");

        validator.validate(event, errors);

        errors.getAllErrors().forEach(e ->{
            Arrays.stream(e.getCodes()).forEach(System.out::println);
        });


    }
}

이 상태에서 name에 null을 주고 validate를 실행하게되면 errors 에 에러코드가 들어간다.

 

이 에러코드들을 살펴보면 각각

 

  • 에러코드.객체명.필드명
  • 에러코드.필드명
  • 에러코드.필드타입
  • 에러코드

 

의 형태를 띄고있다.

 

이 값들은 MessageCodesResolver 를 통해서 결정되는 값들로 MessageSource를 통해서 해당 값들을 얻어올 수 있다.

 

 

 

 

기본적으로는 아무런 값도 들어있지않아 에러를 발생하기에 messages.properties 파일에 해당 key값들과 value값들을 적어줘야한다.

 

 

 

 

 

@Component
public class EventRunner implements ApplicationRunner {

    @Autowired
    MessageSource messageSource;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        Event event = new Event();

        event.setAge(1);
        event.setName("");

        EventValidator validator = new EventValidator();
        validator.supports(Event.class);

        Errors errors = new BeanPropertyBindingResult(event, "event");

        validator.validate(event, errors);

        errors.getAllErrors().forEach(e -> {
            Arrays.stream(e.getCodes()).forEach(code->{
                String message = messageSource.getMessage(code, null, Locale.getDefault());
                System.out.println(message);
            });
        });
    }
}

 

message들을 출력해보면 내가 입력한 값이 출력되는 것을 확인할 수 있다.

 

 

 

 

 

 

 

 

 

@Component
public class EventRunner implements ApplicationRunner {

    @Autowired
    Validator validator;

    @Override
    public void run(ApplicationArguments args) throws Exception {

        System.out.println(validator.getClass().toString());
    }
}

SpringBoot에서는 기본적으로 LocalValidatorFactoryBean을 등록해주며, 애노테이션을 사용하여 검증해준다.

 

 

 

 

 

 

이렇게 원하는 필드에 애노테이션을 달아주면 된다.