들어가기에 앞서 본 포스팅은 백기선님의 인프런 강의를 기반으로 작성된 포스팅임을 알려드립니다.
처음 스프링을 배우는 입장이라 정확하지 않은 정보가 있을 수 있습니다.
댓글을 통해 알려주신다면 최대한 빨리 피드백하도록 하겠습니다!
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를 통하여 Resource 객체를 얻어올 수 있다.
그 외에도 CLASSPATH_URL_PREFIX를 통하여 "classpath:"라는 접두사를 지원한다는 사실도 알 수 있다.
디버그를 통하여 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
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를 모두 검색하여 해당 경로의 리소스를 선택
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을 등록해주며, 애노테이션을 사용하여 검증해준다.
이렇게 원하는 필드에 애노테이션을 달아주면 된다.