설정 파일, 클래스, 템플릿, 프로퍼티 — Spring은 끊임없이 "어딘가에서 바이트를 읽어온다". 그런데 그 "어딘가"는 클래스패스일 수도, 파일시스템일 수도, 원격 URL일 수도, JAR/모듈 안일 수도 있다. 매번 new FileInputStream(...)과 getClass().getResourceAsStream(...)을 손으로 분기하면 코드가 위치에 종속된다.
core.io 패키지는 이 모든 출처를 Resource라는 단일 핸들로 추상화한다. 호출자는 "이 리소스가 어디에 있든" 동일한 메서드로 존재 여부를 확인하고 스트림을 연다. 그리고 문자열 위치("classpath:app.xml", "file:/etc/x")를 Resource로 변환하는 일은 ResourceLoader 전략이 담당한다.
소스: spring-core/src/main/java/org/springframework/core/io/
InputStreamSource (getInputStream() 하나만 가진 최소 인터페이스)
▲
│ extends
┌───────────┐
│ Resource │ exists/isReadable/getURL/getFile/createRelative/getFilename …
└─────┬─────┘
│ extends
┌──────────────┐
│WritableResource│ getOutputStream() — 쓰기 가능 자원
└──────────────┘
Resource 구현체 (대표)
┌────────────────────┬───────────────────────────────────────────┐
│ ClassPathResource │ 클래스패스 상대/절대 경로 │
│ FileSystemResource │ java.io.File / NIO Path 기반 │
│ UrlResource │ 임의의 URL (http:, jar:, file: …) │
│ FileUrlResource │ file: URL 이면서 쓰기 가능 │
│ ByteArrayResource │ 메모리 byte[] │
│ InputStreamResource│ 이미 열린 스트림 1회성 래핑 │
│ ModuleResource │ JPMS 모듈 내부 리소스 (6.1+) │
│ VfsResource │ JBoss VFS │
│ DescriptiveResource│ 실체 없이 설명만 (placeholder) │
└────────────────────┴───────────────────────────────────────────┘
Resource의 핵심은 "핸들과 실체의 분리" 다. getResource(...)로 핸들을 얻는 것은 항상 성공하지만, 그것이 실제로 존재한다는 보장은 아니다. 그래서 별도로 exists()를 호출해 확인해야 한다(Resource.java의 클래스 주석이 이를 명시한다).
Resource는 위치 종류를 자가 진단하는 메서드도 가진다. isFile()이 true면 getFile() 호출이 성공할 "가능성이 높음"을 뜻하고(보수적으로 기본 false), isReadable()은 5.1부터 exists() 의미를 함의한다.
ResourceLoader는 위치 문자열 → Resource 변환 전략이다. 기본 구현 DefaultResourceLoader.getResource(location)의 분기가 전체 규칙을 압축해서 보여준다.
// DefaultResourceLoader.getResource(String) 발췌
for (ProtocolResolver protocolResolver : getProtocolResolvers()) { // 1) 커스텀 프로토콜 우선
Resource resource = protocolResolver.resolve(location, this);
if (resource != null) return resource;
}
if (location.startsWith("/")) {
return getResourceByPath(location); // 2) 경로
}
else if (location.startsWith(CLASSPATH_URL_PREFIX)) { // "classpath:"
return new ClassPathResource(location.substring(...), getClassLoader());
}
else if (location.startsWith(CLASSPATH_ALL_URL_PREFIX)) { // "classpath*:" (7.1)
return new ClassPathAllResource(location.substring(...), getClassLoader());
}
else {
try {
URL url = ResourceUtils.toURL(location); // 3) URL 로 파싱 시도
return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
}
catch (MalformedURLException ex) {
return getResourceByPath(location); // 4) 실패 시 경로로
}
}해석 순서가 곧 우선순위다.
- 등록된
ProtocolResolver가 먼저다.addProtocolResolver(...)로 끼워 넣은 커스텀 해석기는 기본 규칙을 덮어쓸 수 있다. (확장 포인트) classpath:접두사 →ClassPathResource(가장 가까운 하나).classpath*:접두사 →ClassPathAllResource(7.1 신규:getResource에서도 다중 콘텐츠 핸들을 노출).- 그 외에는 URL로 파싱 시도, 실패하면 경로(기본은
ClassPathContextResource)로 폴백.
DefaultResourceLoader는 또한 ConcurrentHashMap 기반 리소스 캐시(getResourceCache(valueType))를 제공한다. ASM MetadataReader 같은 비싼 파생물을 Resource별로 공유 캐싱하는 데 쓰인다(컴포넌트 스캔 성능의 핵심).
ClassPathAllResource는 같은 이름의 모든 리소스(여러 JAR에 흩어진 messages.properties 등)를 한 핸들로 묶는다. getInputStream()은 ClassLoader.getResources()로 모든 URL을 모아 SequenceInputStream으로 연결하고, 새로 추가된 Resource.consumeContent(consumer)는 파일마다 콜백을 한 번씩 돌려준다.
"classpath*:META-INF/x.properties"
│ ClassLoader.getResources(path) → [url1, url2, url3]
▼
getInputStream() ─▶ SequenceInputStream(url1+url2+url3) (병합된 단일 스트림)
consumeContent() ─▶ consumer(url1); consumer(url2); consumer(url3) (개별 콜백)
단일 위치가 아니라 "classpath*:com/**/*.xml" 같은 패턴을 여러 Resource로 펼치려면 ResourceLoader의 확장 인터페이스 ResourcePatternResolver가 필요하다.
ResourceLoader (getResource: 1개)
▲ extends
ResourcePatternResolver (getResources: N개) ← getResources(pattern): Resource[]
▲ implements
PathMatchingResourcePatternResolver ← 표준 단독 구현
PathMatchingResourcePatternResolver는 AntPathMatcher(util)를 써서 *, **, ? 와일드카드를 디렉토리/JAR 엔트리를 순회하며 매칭한다. ApplicationContext는 이 인터페이스를 구현하므로, 컨텍스트에 주입된 ResourceLoader가 사실은 패턴 리졸버인 경우가 많다.
core.io.support는 Resource를 토대로 한 상위 도구들의 집합이다.
| 타입 | 역할 |
|---|---|
PathMatchingResourcePatternResolver |
Ant 패턴 → Resource[] 표준 구현 |
EncodedResource |
Resource + Charset/encoding 묶음 (프로퍼티 로딩 시 인코딩 보존) |
PropertiesLoaderUtils |
Resource에서 Properties 로드 (xml/properties 포맷) |
PropertySourceFactory / ResourcePropertySource |
Resource를 PropertySource로 (3장과 연결) |
SpringFactoriesLoader |
META-INF/spring.factories 로딩 (7장에서 상세) |
ResourceRegion |
리소스의 부분 범위(HTTP Range 응답 등) |
- 전략 패턴:
ResourceLoader/ResourcePatternResolver가 "위치 → 자원" 변환 전략.ApplicationContext가 이를 구현해 컨텍스트별 로딩 정책(서블릿 상대 경로 등)을 주입한다. ProtocolResolver훅: 새 의사 프로토콜을 추가하는 SPI.addProtocolResolver로 등록하면 기본 규칙보다 먼저 평가된다.- 핸들/실체 분리:
Resource획득은 가볍고, I/O는 실제 접근 시점에 발생.exists()로 명시적 확인. - Default file system 한계 인지: 7.0부터
getFilePath()(NIOPath)가 추가되어 비기본 파일시스템(예: ZIP FS)도 다룰 수 있다.getFile()은 기본 파일시스템 전용이다.
core.io는 "어디서 읽든 똑같이 다룬다"는 단 하나의 약속을 Resource로 구현하고, "문자열을 자원으로" 바꾸는 책임을 ResourceLoader에 위임한다. classpath:/classpath*:/file:/URL을 일관되게 해석하며, 패턴 전개는 ResourcePatternResolver가 맡는다. 이 추상화 덕분에 빈 정의, 컴포넌트 스캔, 메시지 번들 등 상위 기능이 위치에 묶이지 않고 동작한다.