교재 실습/자바 웹 개발 워크북

90. 프로퍼티를 이용한 객체 관리 (2)

Jint 2022. 8. 10. 22:32

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).

그림 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).

그림 2 (Properties 객체에 보관된 프로퍼티들의 키 목록)

만약 프로퍼티의 키가 "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());
}

그림 3 (클래스 로딩과 인스턴스 생성)

이렇게 생성한 객체는 다음 그림과 같이 객체 테이블 "objTable"에 저장된다(그림 4).

 

그림 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