🌱 해당 포스트는 한걸음 스터디에서 발표한 내용입니다. 발표 내용을 아래 영상에서 확인하실 수 있습니다.
🌱한걸음은 각자 학습한 내용을 토대로 블로그 글을 작성하고, 대면으로 모여서 발표하며, 녹화해 유튜브에 업로드하는 스터디입니다.
들어가며
Spring 어플리케이션을 개발하다 보면 필연적으로 Resource 관리를 만나게 됩니다.
대표적으로 resources 패키지안에 있는 application.properties 파일도 Resource라고 할 수도 있고 외부의 파일들을 업로드하는 경우도 있습니다.
스프링에선 이를 어떻게 개발자가 쉽게 활용할 수 있도록 제공하는지 알아보겠습니다.
Resource Interface
로우 레벨인 리소스들을 개발자가 쉽게 사용할 수 있게 하는 가장 쉬운 방법은 추상화죠.
스프링에선 org.springframework.core.io. 패키지 안에 Resource 라는 인터페이스를 만들어서 추상화 처리를 해줬습니다.
public interface Resource extends InputStreamSource {
boolean exists();
boolean isReadable();
boolean isOpen();
boolean isFile();
URL getURL() throws IOException;
URI getURI() throws IOException;
File getFile() throws IOException;
ReadableByteChannel readableChannel() throws IOException;
long contentLength() throws IOException;
long lastModified() throws IOException;
Resource createRelative(String relativePath) throws IOException;
String getFilename();
String getDescription();
}
Resource 인터페이스는 InputStreamSource 라는 인터페이스를 상속받고 있습니다.
public interface InputStreamSource {
InputStream getInputStream() throws IOException;
}
Resource 인터페이스의 메소드 중 중요한 것을 살펴보겠습니다.
메서드 | 설명 |
getInputStream() | 리소스를 읽기 위한 InputStream을 반환 |
exists() | 리소스가 존재하는지 확인. |
isOpen() | 리소스가 스트림을 열고 있는 상태인지 확인. 반드시 한 번에 하나만 읽도록 하여 컴퓨팅 리소스 사용을 적게함 |
getDescription() | 리소스에 대한 설명(문자열)을 반환. 에러에 대한 결과물도 반환된다. |
인터페이스의 구성 요소들을 보면 아시겠지만, 이건 기능 구현이라기보다는 Wrapper에 가까운 느낌이라고 이해하시면 됩니다.
Built-in Resource Implementations
사실 인터페이스를 알고 있더라도 대다수는 Resource 인터페이스로 구현된 구현체들을 주로 사용합니다.
구현체들을 나열해보면 다음과 같습니다.
- UrlResource
- ClassPathResource
- FileSystemResource
- PathResource
- ServletContextResource
- InputStreamResource
- ByteArrayResource
UrlResource
java.net.URL을 가지고 있는 구현체로 파일, https 타겟, ftp 타겟 등과 같은 URL로 접근하고자 하는 Resource가 있을 경우 사용할 수 있습니다.
일반 URL처럼 String을 통해 표현하고 "프로토콜:경로" 형식으로 표현합니다.
지원하는 프로토콜은 다음과 같습니다.
- file - 로컬 파일 시스템 경로를 참조
- http - http 프로토콜을 통해 원격 리소스 참조
- https - https 프로토콜을 통해 원격 리소스 참조
- ftp - ftp 프로토콜을 통해 원격 리소스 참조
- jar - jar 파일 내 리소스를 참조
ClassPathResource
클래스 패스에 있는 리소스를 처리하기 위한 구현체입니다.
클래스패스(ClassPath)란?
애플리케이션이 실행될 때 자바 클래스 및 리소스를 찾는 경로
일반적으로 JAR 파일 내부에 있는 리소스를 읽는 데 사용됩니다. 중요한 점은 클래스패스 리소스가 파일 시스템에 존재하는 경우엔 이를 java.io.File로 변환할 수 있지만, 클래스패스 리소스가 jar 파일에 포함되어 있고 파일 시스템으로 확장되지 않은 경우엔 java.io.File로 변환할 수 없습니다. 따라서 모든 Resource 구현체는 항상 java.net.URL 리소스로 처리할 수 있도록 지원합니다.
앞선 URL과 비슷한 형식으로 "classpath:data/example.txt" 식으로 사용합니다.
FileSystemResource
java.io.File 에 대해 구현체로 파일 시스템의 리소스를 처리할 때 사용됩니다. Path, URL, Fiile 객체로의 변환이 가능한 것이 특징입니다. 다만 Path로 사용할 순 있지만 java.nio.file.Files 라는 파일 기반에서 작동하기 때문에, 상세한 path처리는 뒤에 설명할 PathResource를 사용해야 합니다.
사용 예시
파일을 읽은 다음 URL 변환
FileSystemResource resource = new FileSystemResource("/path/to/file.txt");
URL url = resource.getURL();
System.out.println("File URL: " + url);
PathResource
java.nio.file.path 를 활용하는 경우 사용됩니다.
사용 예시
PathResource resource = new PathResource("/path/to/file.txt");
Path path = resource.getPath();
System.out.println("Path: " + path);
ServletContextResource
ServletContext에 기반하여 웹 애플리케이션의 리소스를 처리하기 위해 설계되었습니다. 웹 어플리케이션 컨텍스트의 루트 경로 기준으로 상대적인 경로를 사용하는 특징이 있습니다.
주로, Html, Css, 이미지, jsp 파일 등을 접근할 때 사용합니다.
내부적으로 WebApplicationContext에서 생성되지만, 명시적 생성도 가능합니다.
InputStreamResource
InputStream에 대한 구현체입니다.
다른 것과 다르게 한번 구현된 순간, 항상 Open 상태이므로 isOpen()는 언제나 true를 리턴합니다.
따라서 여러 번 읽을 필요가 없습니다.
ByteArrayResource
byte array 그 자체를 받기 위한 구현체입니다.
ResourceLoader
구현체를 생성자를 통해 만들 수 있지만, ResourceLoader를 사용하면 더 편합니다.
이는 다양한 종류의 리소스들을 일관된 API로 처리할 수 있도록 설계했기 때문입니다.
따라서 리소스 유형을 자동으로 인식합니다.
http:, classpath:, file: 등의 접두사를 사용하여 인식하는 방식입니다.
ResourceLoader 인터페이스
주석을 보면 어떤 것을 지원해야 하는지 적혀있습니다.
사용 예시
Resource resource = resourceLoader.getResource("classpath:config/settings.xml");
InputStream inputStream = resource.getInputStream();
Context의 구분에 따라 Resource가 맞춰서 반환되기도 합니다.
ClassPathXmlApplicationContext에선 ClassPathResource를, FileSystemXmlApplicationContext에선 FileSystemResource를..
이런 식으로 Spring을 구성하기 위해 내부적으로 처리됩니다.
그러면 classpath:, file: 과 같은 접두사 동적 처리를 어디서 할까요??
ResourcePatternResolver Interface
public interface ResourcePatternResolver extends ResourceLoader {
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
Resource[] getResources(String locationPattern) throws IOException;
}
ResourceLoader를 확장하고 있으며 와일드카드 패턴(모든 경로를 포함하는) 등의 패턴 기반 리소스 검색을 위한 인터페이스입니다.
ApplicationContext는 ResourcePatternResolver를 구현하고 있기 때문에 스프링 애플리케이션 내에서의 리소스들을 쉽게 검색할 수 있습니다.
그러면 ApplicationContext 안의 ResourcePatternResolver 인터페이스 구현체는 누구일까요??
PathMatchingResourcePatternResolver 클래스입니다.
그러면 ResourceLoader를 어떻게 사용할 것인가? - ResourceLoaderAware
이때까진, ResourceLoader의 내부 로직에 대해 알아봤습니다.
그러면 실제 개발에서 이를 어떻게 사용할 수 있을까요??
여러 방법 중 ResourceLoaderAware 라는 것이 있습니다.
이는, 현재 ResourceLoader 인스턴스를 자동으로 주입받을 수 있도록 해줍니다.
@Component
public class MyBean implements ResourceLoaderAware {
private ResourceLoader resourceLoader;
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
public void loadMyResource() {
try {
Resource resource = resourceLoader.getResource("classpath:application.properties");
InputStream inputStream = resource.getInputStream();
// 리소스 처리 로직...
} catch (IOException e) {
e.printStackTrace();
}
}
}
위와 같이 ResourceLoaderAware를 구현함으로써 Bean을 이용하여 ResourceLoader 인스턴스를 가져옵니다.
(쉽게 @Autowired 를 사용해도 됩니다)
이 방식은 동적으로 리소스 경로를 결정해야 하는 경우에 적합한 과정입니다.
빈에 올려두었다는 것 자체가 동적 처리를 할 수 있음을 의미하기 때문입니다.
비동적 리소스와 동적 리소스??
정적 리소스 (Static Resources)
변경되지 않거나 실행 중에 변경되지 않는 리소스를 의미한다.
대표적인 예시로, application.properties, html, css, js, 서버에 고정된 이미지 파일 등이 있다.
동적 리소스 (Dynamic Resources)
애플리케이션 실행 중에 변경될 수 있는 리소스를 의미한다.
대표적인 예시로, 사용자 역할에 따른 리소스 로딩, 외부 API에서 동적으로 로드되는 파일이나 사용자에 따라 다른 리소스 경로 등이 있다.
정적 리소스 처리 - PropertyEditor
그러면 정적 리소스에 더 적합한 처리 방식은 무엇일까요??
공식 문서은 스프링에선 모든 application contexts 들은 특별한 자바 빈인 PropertyEditor를 사용하고 있다고 합니다. 이 PropertyEditor는 String paths를 Resource object로 변환할 수 있는 기능이 있기 때문에, 빈에서 리소스를 직접 설정하지 않고 프로퍼티를 통해 리소스를 주입시킵니다.
내부적으로 구현이 PropertyEditor가 구현되어 있다고 생각하시면 됩니다.
예시를 들어 더 설명해보도록 하겠습니다.
@Component
public class MyBean {
private final Resource[] templates;
public MyBean(@Value("${templates.path}") Resource[] templates) {
this.templates = templates;
}
}
@Value 어노테이션을 통해 주입된 문자열을 내부적으로 Bean을 만들 때, Resource 객체로의 변환 부분을 PropertyEditory를 사용하고 있다고 이해하시면 됩니다.
@Value("2025-01-01")
private Date myDate;
위 경우에도 2025-01-01이라는 문자열을 Date 객체로 변환하기 위해 PropertyEditor를 내부적으로 사용합니다.
<bean id="myBean" class="example.MyBean">
<property name="template" value="classpath:/templates/myTemplate.txt"/>
</bean>
xml 을 사용할 때도 마찬가지로 내부에서 알아서 객체를 만들기 위해 PropertyEditor를 사용하고, 경로에 접두사를 주게 되면 명시적으로 그 객체로 변환할 수 있습니다.
마지막으로 Application Context에서 Resource 경로를 처리하는 방식에 대해 알아보겠습니다.
Application Context가 Resource 경로를 결정하는 방식
공식 문서에선 ClassPathXmlApplicationContext와 FileSystemXmlApplicationContext에 대해 설명하고 있습니다.
이 두 Context는 많이 사용하는 중요한 Context죠.
이들이 Resource 경로를 결정하는 방식은 아주 단순합니다.
생성자를 사용합니다.
ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");
ApplicationContext ctx =
new FileSystemXmlApplicationContext("conf/appContext.xml");
경로를 쓰는 건 귀찮습니다.
만약 여러 xml 파일이 있다면 더 귀찮겠죠.
이를 위해 ClassPathXmlApplicationContext는 String 배열을 통해 classpath에 있다고 가정하여 여러 정보를 생성자에 주입할 수 있습니다.
예를 들어
com/
example/
services.xml
repositories.xml
MessengerService.class
example 패키지에 3개의 파일이 있고 이를 주입한다고 한다면 다음과 같은 코드를 작성할 수 있습니다.
ApplicationContext ctx = new ClassPathXmlApplicationContext(
new String[] {"services.xml", "repositories.xml"}, MessengerService.class);
아주 간단하죠??
여기까지 기본적으로 Spring에서 Resource를 관리하는 방법입니다.
감사합니다.
출처
'Spring' 카테고리의 다른 글
[Spring 공식문서 정리하기] Spring IoC Container와 Beans(Feat. 의존성 주입(DI)) (1) | 2024.12.25 |
---|