90. 프로퍼티를 이용한 객체 관리 (2)
3. ApplicationContext 클래스
ApplicationContext 클래스는 앞 절의 실습 시나리오에서 언급한 대로 프로퍼티 파일에 설정된 객체를 준비하는 일을 한다. spms.context 패키지를 생성한 뒤, 이 패키지에 ApplicationContext 클래스를 생성한다.
package spms.context;
import java.io.FileReader;
import java.lang.reflect.Method;
import java.util.Hashtable;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
//프로퍼티 파일을 이용한 객체 준비
public class ApplicationContext {
//객체 저장할 보관소
Hashtable<String,Object> objTable = new Hashtable<String,Object>();
//객체 꺼낼 메서드(getter)
public Object getBean(String key) {
return objTable.get(key);
}
//ApplicationContext 클래스의 생성자
public ApplicationContext(String propertiesPath) throws Exception {
//'이름=값'형태로 된 파일 다루는 Properties 클래스
Properties props = new Properties();
//load() : FileReader를 통해 읽어드린 프로퍼티 내용 키-값 형태로 내부 맵에 보관
props.load(new FileReader(propertiesPath));
prepareObjects(props);
injectDependency();
}
//프로퍼티 파일의 내용 로딩 후 객체 준비 메서드
private void prepareObjects(Properties props) throws Exception {
//JNDI 객체 찾을 때 사용할 InitialContext 준비
Context ctx = new InitialContext();
String key = null;
String value = null;
for(Object item : props.keySet()) {
key = (String)item;
value = props.getProperty(key);
if(key.startsWith("jndi.")) {//keySet() : key목록 가져옴
//lookup() : JNDI 인터페이스 통해 톰캣 서버에 등록된 객체 찾음
objTable.put(key, ctx.lookup(value));
}else {
//Class.forName() 호출하여 클래스 로딩하고, newInstance() 사용하여 인스턴스 생성
objTable.put(key, Class.forName(value).newInstance());
}
}
}
//각 객체가 필요로 하는 의존 객체 주입 메서드
private void injectDependency() throws Exception {
for(String key : objTable.keySet()) {
if(!key.startsWith("jndi.")) {
callSetter(objTable.get(key));
}
}
}
//매개변수로 주어진 객체에 대해 셋터 메서드 찾아서 호출
private void callSetter(Object obj) throws Exception {
Object dependency = null;
for(Method m : obj.getClass().getMethods()) {
if(m.getName().startsWith("set")) {
dependency = findObjectByType(m.getParameterTypes()[0]);
//의존 객체 찾았다면 셋터 메서드 호출
if(dependency != null) {
m.invoke(obj, dependency);
}
}
}
}
//셋터 메서드의 매개변수 타입이 일치하는 객체를 objTable에서 찾음 - 셋터 메서드 호출할 때 넘겨줄 의존 객체 찾음
private Object findObjectByType(Class<?> type) {
for(Object obj : objTable.values()) {
//타입이 일치하면 객체의 주소 리턴
if(type.isInstance(obj)) {//isInstance() : 주어진 객체가 해당 클래스 또는 인터페이스의 인스턴스인지 검사
return obj;
}
}
return null;
}
}
- 객체의 보관
프로퍼티에 설정된 대로 객체를 준비하면, 객체를 저장할 보관소가 필요한데 이를 위해 해시 테이블을 준비한다. 또한, 해시 테이블에서 객체를 꺼낼 (getter) 메서드도 정의한다.
//객체 저장할 보관소
Hashtable<String,Object> objTable = new Hashtable<String,Object>();
//객체 꺼낼 메서드(getter)
public Object getBean(String key) {
return objTable.get(key);
}
- 프로퍼티 파일의 로딩
ApplicationContext 클래스의 생성자가 호출되면 매개변수로 지정된 프로퍼티 파일의 내용을 로딩해야 한다. 이를 위해 java.util.Properties 클래스를 사용하였다.
//'이름=값'형태로 된 파일 다루는 Properties 클래스
Properties props = new Properties();
//load() : FileReader를 통해 읽어드린 프로퍼티 내용 키-값 형태로 내부 맵에 보관
props.load(new FileReader(propertiesPath));
Properties 클래스는 '이름=값' 형태로 된 파일을 다룰 때 사용하는 클래스이다. Properties 클래스의 load() 메서드는 다음 그림과 같이 FileReader 클래스의 생성자를 통해 읽어드린 프로퍼티 내용을 키-값 형태로 내부 맵에 보관한다(그림 1).
- prepareObjects() 메서드
프로퍼티 파일의 내용을 로딩했으면, 그에 따라 객체를 준비해야 한다. prepareObjects() 메서드가 바로 그 일을 수행하는 메서드이다. 먼저 JNDI 객체를 찾을 때 사용할 InitialContext 클래스를 준비한다.
//JNDI 객체 찾을 때 사용할 InitialContext 준비
Context ctx = new InitialContext();
그리고 반복문을 통해 프로퍼티에 들어있는 정보를 꺼내서 객체를 생성한다.
for(Object item : props.keySet()) {
key = (String)item;
value = props.getProperty(key);
if(key.startsWith("jndi.")) {//keySet() : key목록 가져옴
//lookup() : JNDI 인터페이스 통해 톰캣 서버에 등록된 객체 찾음
objTable.put(key, ctx.lookup(value));
}else {
//Class.forName() 호출하여 클래스 로딩하고, newInstance() 사용하여 인스턴스 생성
objTable.put(key, Class.forName(value).newInstance());
}
}
Properties 객체로부터 클래스 이름을 꺼내려면 키(key)를 알아야 한다. keySet() 메서드는 다음 그림과 같이 Properties 객체에 저장된 키 목록을 반환한다(그림 2).
만약 프로퍼티의 키가 "jndi."로 시작한다면 객체를 생성하지 않고, InitialContext 클래스를 통해 얻는다.
if(key.startsWith("jndi.")) {//keySet() : key목록 가져옴
//lookup() : JNDI 인터페이스 통해 톰캣 서버에 등록된 객체 찾음
objTable.put(key, ctx.lookup(value));
}
InitialContext 클래스의 lookup() 메서드는 JNDI 인터페이스를 통해 톰캣 서버에 등록된 객체를 찾는다.
그 밖의 객체는 Class.forName() 메서드를 호출하여 클래스를 로딩하고, newInstance() 메서드를 사용하여 인스턴스를 생성한다(그림 3).
else {
//Class.forName() 호출하여 클래스 로딩하고, newInstance() 사용하여 인스턴스 생성
objTable.put(key, Class.forName(value).newInstance());
}
이렇게 생성한 객체는 다음 그림과 같이 객체 테이블 "objTable"에 저장된다(그림 4).
- injectDependency() 메서드
톰캣 서버로부터 객체를 가져오거나(예 : DataSource) 직접 객체를 생성했으면(예 : MemberDao), 이제는 각 객체가 필요로 하는 의존 객체를 할당해 주어야 한다. 이런 일을 하는 메서드가 injectDependency() 메서드이다.
if(!key.startsWith("jndi.")) {
callSetter(objTable.get(key));
}
객체 이름이 "jndi."로 시작하는 경우 톰캣 서버에서 제공한 객체이므로 의존 객체를 주입해서는 안 된다. 그래서 제외하였다. 나머지 객체에 대해서는 셋터 메서드를 호출한다. 즉 셋터 메서드가 원하는 객체를 할당한다.
- callSetter() 메서드
callSetter() 메서드는 매개변수로 주어진 객체에 대해 셋터 메서드를 찾아서 호출하는 일을 한다. 먼저 셋터 메서드를 찾는다.
for(Method m : obj.getClass().getMethods()) {
if(m.getName().startsWith("set")) {
...
}
}
셋터 메서드를 찾았으면 셋터 메서드의 매개변수와 타입이 일치하는 객체를 objTable에서 찾는다.
dependency = findObjectByType(m.getParameterTypes()[0]);
의존 객체를 찾았으면, 셋터 메서드를 호출한다.
//의존 객체 찾았다면 셋터 메서드 호출
if(dependency != null) {
m.invoke(obj, dependency);
}
- findObjectByType() 메서드
이 메서드는 셋터 메서드를 호출할 때 넘겨줄 의존 객체를 찾는 일을 한다. objTable에 들어 있는 객체를 모두 뒤진다.
for(Object obj : objTable.values()) {
...
}
만약 셋터 메서드의 매개변수 타입과 일치하는 객체를 찾았다면 그 객체의 주소를 리턴한다.
//타입이 일치하면 객체의 주소 리턴
if(type.isInstance(obj)) {//isInstance() : 주어진 객체가 해당 클래스 또는 인터페이스의 인스턴스인지 검사
return obj;
}
Class의 isInstance() 메서드는 주어진 객체가 해당 클래스 또는 인터페이스의 인스턴스인지 검사한다.
참고도서 : https://freelec.co.kr/book/1674/
[열혈강의] 자바 웹 개발 워크북
[열혈강의] 자바 웹 개발 워크북
freelec.co.kr