5.6 의존성으로써의 Resources
빈 자체가 어떤 동적인 과정을 통해 리소스 경로를 결정하고 제공할 것이라면 빈에 리소스를 로드하는데 ResourceLoader 인터페이스를 사용하는 것이 합리적일 것이다. 사용자의 역할(role)에 의존하는 특정 리소스가 있는 어떤 템플릿을 로딩하는 예제를 생각해 보자. 리소스가 정적이라면 ResourceLoader 인터페이스의 사용을 완전히 제거하고 빈이 필요한 Resource 프로퍼티를 그냥 노출해서 주입되도록 하는 것이 합리적이다.
이러한 프로퍼티 주입을 대수롭지 않게 만드는 것은 모든 어플리케이션 컨텍스트가 String 경로를 Resource 객체로 변환할 수 있는 특별한 자바빈 PropertyEditor를 등록하고 사용하는 것이다. 그래서 myBean에 Resource 타입의 템플릿 프로퍼티가 있다면 다음처럼 해당 리소스에 대한 간단한 문자열로 설정할 수 있다.
<bean id="myBean" class="...">
<property name="template" value="some/resource/path/myTemplate.txt"/>
</bean>
이 리소스 경로는 접두사가 없으므로 어플리케이션 컨텍스트 자체가 ResourceLoader로 사용되기 때문에 리소스는 컨텍스트의 정확한 타입에 의존하는 (적절한) ClassPathResource, FileSystemResource, ServletContextResource를 통해서 로드될 수 있다.
특정 Resource 타입을 사용하도록 강제해야 한다면 접두사를 사용할 수 있다. 다음 두 예제는 어떻게 ClassPathResource와 UrlResource를(후자는 파일시스템의 파일에 접근하는데 사용한다.) 강제할 수 있는지 보여준다.
<property name="template" value="classpath:some/resource/path/myTemplate.txt">
<property name="template" value="file:/some/resource/path/myTemplate.txt"/>
5.7 어플리케이션 컨텍스트와 Resource 경로
5.7.1 어플리케이션 컨텍스트 생성
(특정 어플리케이션 컨텍스트 타입에 대한) 어플리케이션 컨텍스트 생성자는 보통 컨텍스트의 정의를 구성하는 XML 파일같은 리소스의 위치 경로로 문자열이나 문자열의 배열을 받는다.
이 러한 위치 경로에 접두사가 없을 때 특정 Resource 타입은 해당 경로에서 만들어지고 특정 어플리케이션 컨텍스트에 의존적이고 적절한 빈 정의를 로드하는데 사용한다. 예를 들어 다음처럼 ClassPathXmlApplicationContext를 생성한다면
ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");
빈 정의는 ClassPathResource처럼 사용될 클래스패스에서 로드될 것이다. 하지만 FileSystemXmlApplicationContext를 생성한다면
ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/appContext.xml");
빈 정의는 이 경우에 현재 워킹디렉토리에서 상대적인 파일시스템 위치에서 로드될 것이다.
위치경로에 특별한 클래스패스 접두사나 표준 URL 접두사를 사용하면 정의를 로드하려고 생성한 Resource의 기본 타입을 오버라이드할 것이다. 그래서 이 FileSystemXmlApplicationContext...
ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");
... 클래스패스에서 해당 빈 정의를 실제로 로드할 것이다. 하지만 이는 여전히 FileSystemXmlApplicationContext이다. 이어서 ResourceLoader를 사용한다면 접두사가 없는 어떤 경로도 여전히 파일시스템 경로로 다룰 것이다.
5.7.1.1 ClassPathXmlApplicationContext 인스턴스 생성하기 - 단축키(shortcuts)
ClassPathXmlApplicationContext 는 편리한 인스턴스화를 할 수 있도록 다수의 생성자를 노출한다. 기본 아이디어는 하나만 XML 파일 자체의 파일명을(경로 정보없이) 담고 있는 문자열 배열을 제공하고 Class도 또한 제공한다. ClassPathXmlApplicationContext는 제공된 클래스에서 경로 정보를 얻을 것이다.
예제가 이를 명확하게 해줄 것이다. 다음과 같은 디렉토리 레이아웃을 생각해 보자.
com/
foo/
services.xml
daos.xml
MessengerService.class
ClassPathXmlApplicationContext 인스턴스는 다음처럼 인스턴스화될 수 있는 'services.xml'와 'daos.xml'에 정의된 빈들로 구성되어 있다.
ApplicationContext ctx = new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"}, MessengerService.class);
다양한 생성자에 대한 자세한 내용은 ClassPathXmlApplicationContext 클래스의 Javadoc을 참고해라.
5.7.2 어플리케이션 컨텍스트 생성자 리소스 경로에 와일드카드 사용하기
어 플리케이션 컨텍스트 생성자 값의 리소스 경로들은 (위에서 본 것처럼) 타겟 리소스에 일대일로 매핑되는 간단한 경로일 수도 있지만 "classpath*:"같은 접두사나 내부적으로 Ant 스타일의 정규표현식(스프링의 PathMatcher 유틸리티의 사용과 일치하는)을 포함할 수도 있다. 후자의 두 경우는 사실상 와일드카드이다.
어플리케이션은 컴포넌트 스타일로 조립하는 것이 이 메카니즘의 한가지 용도이다. 모든 컴포넌트들을 알고 있는 위치경로로 켄텍스트 정의 조각을 배포할 수 있고 classpath*:로 접두사가 붙은 같은 경로를 사용해서 최종 어플리케이션 컨텍스트를 생성할 때 모든 컴포넌트 조각들을 자동으로 선택할 것이다.
이 와일드카드는 어플리케이션 컨텍스트 생성자(또는 PathMatcher 유틸리티 클래스 계층을 직접 사용할 때)의 리소스 경로의 사용을 지정하고 생성시에 처리한다. 이는 Resource 타입 자체는 아무것도 하는 것이 없다. 리소스가 단순히 한번에 하나의 리소스만 가르키는 것처럼 실체 Resource를 생성하는 classpath*: 접두사를 사용하는 것은 불가능하다.
5.7.2.1 Ant 스타일의 패턴
예를 들어 다음처럼 위치 경로가 Ant 스타일 패턴을 포함하고 있을 때
/WEB-INF/*-context.xml
com/mycompany/**/applicationContext.xml
file:C:/some/path/*-context.xml
classpath:com/mycompany/**/applicationContext.xml
... 리졸버는 복잡하지만 와일드카드를 처리하려고 정의되어 있는 정차를 따른다. 리졸버는 마지막의 와일드카드가 아닌 부분의 경로로 Resource를 생성하고 그로부터 URL을 획득한다. 이 URL이 "jar:" URL이나 컨테이너가 지정한 변형(예를 들어 WebLogic의 "zip:"나 WebSphere의 "wsjar" 등)이 아니라면 java.io.File를 URL에서 획득하고 파일시스템을 탐색해서 와일드차드를 처리한다. jar URL일 경우에 리졸버는 URL에서 java.net.JarURLConnection를 얻거나 와일드카드를 처리하기 위해 수동으로 jar URL을 파싱해서 jar파일의 내용을 탐색한다.
이식성의 영향(Implications on portability)
지정된 경로가 이미 파일 URL이라면(명시적이든 암묵적이든) 기반 ResourceLoader가 파일시스템이기 때문에 와일드카드가 이식성에서도 완전히 동작한다는 것을 보장한다.
지 정한 경로가 클래스패스라면 리졸버는 Classloader.getResource() 호출을 통해서 와일드카드가 아닌 마지막 경로 부분을 반드시 획득해야 한다. 이는 단순히 경로의 한 부분(결국 파일은 아니다)이므로 사실상 정의되지 않았고(ClassLoader Javadoc에서) 이 경우에 정확한 URL의 종류가 리턴된다. 실제로 이는 항상 클래스패스 리소스를 파일시스템 위치로 처리하는 디렉토리나 jar 위치로 처리하는 jar URL을 나타내는 java.io.File이다. 여전히 이 작업에서 이식성 이슈가 있다.
와 일드카드가 아닌 마지막 부분으로 획득한 jar URL을 획득했다면 리졸버는 URL에서 java.net.JarURLConnection를 얻거나 jar의 컨텐츠를 탐색할 수 있도록 jar URL을 수동으로 파싱해서 와일드카드를 처리할 수 있어야 한다. 이는 대부분의 환경에서 동작할 것이지만 몇몇 환경에서는 실패할 것이다. jar 리소스의 와일드카드 처리는 사용하기 전에 의존환경에 대한 완전히 테스트해 볼 것을 강력히 추천한다.
5.7.2.2 classpath*: 접두사
XML 기반의 어플리케이션 컨텍스트를 생성할 때 위치 문자열은 특수한 classpath*: 접두사를 사용할 것이다.
ApplicationContext ctx =
new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml");
이 특수한 접두사는 주어진 이름과 일치하는 모든 클래스패스 리소스들이 획득되어야 한다는 것을 가르키고 (내부적으로는 ClassLoader.getResources(...) 호출을 통해서 이뤄진다.) 최종 어플리케이션 컨텍스트 정의의 형식으로 합쳐진다.
Classpath*: 이식성
와 일드카드 클래스패스는 의존하는 클래스로더의 getResources() 메서드에 기반한다. 최근의 어플리케이션 서버 대부분은 자신만의 클래스로더 구현체를 제공하므로 이 동작은 jar파일을 다룰 때 특히 다를 것이다. classpath*가 동작하는 지 확인하기 위한 간단한 테스트는 클래스패스의 jar 내에서 파일을 로드하도록 클래스로더를 사용하는 것이다: getClass().getClassLoader().getResources("<someFileInsideTheJar>") 이 테스트를 이름은 갖지만 다른 두가지 위치에 있는 파일로 시험해봐라. 적절하지 않은 결과가 나온 경우에 클래스로더의 동작을 조정할 수 있는 세팅과 관련해서 어플리케이션 서버의 문서를 확인해라.
와 일드카드 클래스패스는 의존하는 클래스로더의 getResources() 메서드에 기반한다. 최근의 어플리케이션 서버 대부분은 자신만의 클래스로더 구현체를 제공하므로 이 동작은 jar파일을 다룰 때 특히 다를 것이다. classpath*가 동작하는 지 확인하기 위한 간단한 테스트는 클래스패스의 jar 내에서 파일을 로드하도록 클래스로더를 사용하는 것이다: getClass().getClassLoader().getResources("<someFileInsideTheJar>") 이 테스트를 이름은 갖지만 다른 두가지 위치에 있는 파일로 시험해봐라. 적절하지 않은 결과가 나온 경우에 클래스로더의 동작을 조정할 수 있는 세팅과 관련해서 어플리케이션 서버의 문서를 확인해라.
"classpath*:" 접두사는 위치 경로의 다른 부분에서 PathMatcher 패턴과 조합할 수도 있다. 예를 들면 "classpath*:META-INF/*-beans.xml"처럼 사용할 수 있다. 이 경우에 처리 전략은 아주 간단하다. ClassLoader.getResources() 호출은 클래스 로더 계층에서 일치하는 모든 리소스들을 얻기 위해 마지막 와일드카드가 아닌 경로 부분을 사용하고 각 리소스를 빼와서 위에서 설명한 같은 PathMatcher 처리 전략을 와일드카드 하위경로에 사용한다.
5.7.2.3 와일드 카드와 관련된 추가적인 내용
"classpath*:"를 Ant 스타일 패턴과 조합해서 사용할 때는 실제 타겟 파일이 파일시스템에 존재하는냐와 상관없이 패턴이 시작하기 전에 최소한 하나의 루트 디렉토리가에서만 확실하게 동작할 것이다. 이 말은 "classpath*:*.xml" 같은 패턴은 jar 파일의 루트에서 파일을 획득하는 것이 아니라 확장된 디렉토리의 루트에서 획득한다는 의미이다. 이는 (검색할 잠재적인 루트를 가르키는) 전달된 빈 문자열에 대해서 파일 시스템 위치만 리턴하는 JDK의 ClassLoader.getResources() 메서드의 제한에서 비롯된 것이다.
"classpath:"를 가진 Ant 스타일 패턴의 리소스는 검색할 루트 팩키지가 여러 클래스 경로에서 있을 경우 일치하는 리소스를 찾는다는 보장을 하지 않는다. 이는 다음과 같은 리소스 때문이다.
com/mycompany/package1/service-context.xml
딱 한 위치에만 있을 것이지만 경로가 다음과 같은 경우
classpath:com/mycompany/**/service-context.xml
경로를 처리하려고 시도하면 리졸버는 getResource("com/mycompany")가 리턴한 URL을 (먼저) 없앨 것이다. 이 기반 패키지 노드가 여러 클래스로더 위치에 존재한다면 실제 최종 리소스는 최하단에 존재하지 않을 수 있다. 그러므로 이러한 경우에는 차라리 루트 패키지를 포함하는 모든 클래스 경로를 검색하도록 같은 Ant 스타일 패턴과 "classpath*:"를 사용해라.
5.7.3 FileSystemResource의 주의사항
FileSystemApplicationContext에 소속되지 않은 FileSystemResource(즉 FileSystemApplicationContext는 실제 ResourceLoader가 아니다.)는 기대한 것처럼 절대경로 대 상대경로로 다룰 것이다. 상대 경로는 현재 워킹디렉토리의 상대경로이고 절대경로는 파일시스템의 루트의 상대적인 경로이다.
하지만 하위 호환성(역사적인) 때문에 FileSystemApplicationContext가 ResourceLoader일 때는 달라진다. FileSystemApplicationContext는 간단하게 모든 붙은(attached) FileSystemResource 인스턴스가 경로가 슬래시로 시작하던지 아니던 지에 상관없이 모든 위치경로를 상대적으로 다루도록 한다. 사실 이는 다음 두 가지가 같다는 것을 의미한다.
ApplicationContext ctx =
new FileSystemXmlApplicationContext("conf/context.xml");
ApplicationContext ctx =
new FileSystemXmlApplicationContext("/conf/context.xml");
다음 두 가지도 같다.(한 경우는 상대경로이고 다른 경우는 절대경로인 것처럼 다른 것이 당연하다고 하더라도)
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("some/resource/path/myTemplate.txt");
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("/some/resource/path/myTemplate.txt");
실제로 진짜 파일시스템 절대경로가 필요하다면 FileSystemResource / FileSystemXmlApplicationContext와 함께 절대경로를 사용하는 것은 삼가하는 것이 더 낫다. 그리고 그냥 file: URL 접두사를 사용해서 UrlResource의 사용을 강제해라.
// 실제 컨텍스트 타입과는 상관없이 Resource 는 항상 UrlResource가 될 것이다
ctx.getResource("file:/some/resource/path/myTemplate.txt");
// UrlResource로 해당 정의를 로드하는데 이 FileSystemXmlApplicationContext을 사용하도록 강제한다
ApplicationContext ctx =
new FileSystemXmlApplicationContext("file:/conf/context.xml");
감사합니다. 정말 많이 도움이 되었습니다. ^^!
네 댓글 감사합니다. ^^