강좌 ch 3. 1~2강 요약
먼저 SportCar라는 클래스의 객체를 생성한다고 가정해보면, 다음과 같이 쓸 수 있다.
SportsCar car = new SportsCar();
하지만 이 객체를 Truck 객체로 바꾼다고 하면,
Truck car = new Truck();
이렇게 2곳을 바꿔 써주어야 한다. 하나의 객체를 생성만 할 때는 직접 바꿔주는 것이 크게 불편하지 않지만, 만약에 이 car 객체를 100개 생성했다면 200번을 바꿔주어야 한다.
다형성을 사용하면 200번을 100번으로 줄일 수 있다. SportsCar과 Truck이 Car를 상속한다고 하면,
Car car = new SportsCar();
Car car = new Truck();
업캐스팅으로 생성자 메서드를 호출하는 부분만 바꿔주면 된다.
그래도 개발자들은 100번은 너무 많다고 생각해 더 일을 줄이는 방법이 없을까 하고 고민하게 된다. 코드에는 기능을 제공하는 코드가 있고, 그 기능을 사용하는 코드가 있다. 여기에 착안하여 Car을 생성하는 기능을 따로 분리하게 된다.
Car car = getCar();
static Car getCar();
return new SportsCar();
car을 생성하려면 이 메서드를 호출하면 된다.
만약 Car 객체 100개를 Truck 객체로 바꾸어야 한다고 하면 메서드의 반환값만 1번 바꿔주면 된다.
static Car getCar();
return new Truck();
이렇게 100번의 일이 1번의 일로 줄어들었다.
그런데 여기서 더 변경에 유리하게 바꿀 수 있다. 프로그램을 변경하면 필수적으로 다시 컴파일하고, 테스트를 해야 하기 때문에 개발자들은 코드를 변경하지 않고 SportsCar을 Truck으로 바꾸는 방법을 고안하게 되었다. 그래서 외부에 클래스 설계 등을 저장한 파일을 만들어 놓고, 작성한 코드가 그 파일을 읽어 클래스를 설계하게 만들었다.
public static void main(String[] args) throws Exception{
Car car = getCar();
System.out.println(car);
}
static Car getCar() throws Exception{
// config.txt 읽어 Properties에 저장
Properties p = new Properties();
p.load(new FileReader("config.txt");
// 클래스 객체(설계도) 얻어
Class clazz = Class.forName(p.getProperty("car");
// 객체 생성하여 반환
return (Car)clazz.newInstance();
// config.txt
car = com.fastcampus.ch3.diCopy1.Truck
Properties는 Map과 같이 키-값 쌍으로 된 객체이다. 차이점은 키-값이 둘 다 문자열이라는 것이다. 파일을 읽어오는 데 load() 메서드가 편리하여 쓰였다.
위처럼 설계하면 코드에 변경 하나 주지 않고 외부 파일을 변경하여 실행 내용을 바꿀 수 있다.
이렇게 변경에 유리한 코드는 기능을 분리시키고 프로그램의 변경을 최소화한다. 객체 지향 프로그래밍이 등장한 것도 이 맥락이다.
이제 위의 getCar()을 getObject()로 바꾸고, 전달받은 이름의 클래스로 객체가 생성되도록 조금 더 유연하게 바꿔볼 수 있다.
public static void main(String[] args) throws Exception{
Car car = (Car)getObject("car"); // Truck
System.out.println("car = " + car);
Engine engine = (Engine)getObject("engine");
System.out.println("engine = " + engine);
}
static Object getObject(String key) throws Exception{
Properties p = new Properties();
p.load(new FileReader("config.txt");
Class clazz = Class.forName(p.getProperty(key));
return clazz.newInstance();
// config.txt
car = com.fastcampus.ch3.diCopy1.Truck
engine = com.fastcampus.ch3.diCopy1.Engine
객체 컨테이너라는 것은 객체 저장소이다. 이번에는 아까와 비슷하지만 Map을 가지고 있는 AppContext를 만들어서 SportsCar와 Engine 객체를 만들어본다.
class AppContext{
Map map;
AppContext(){
map = new HashMap();
map.put("car", new SportsCar());
map.put("engine",new Engine());
}
Object getBean(String key){
return map.get(key);
}
}
public class Main2 {
public static void main(String[] args) throws Exception{
AppContext ac = new AppContext();
Car car = (Car)ac.getBean("car"); // Truck
System.out.println("car = " + car);
Engine engine = (Engine)ac.getBean("engine");
System.out.println("engine = " + engine);
}
}
실행해보면 객체를 잘 받아온다. 하지만 이 AppContext는 해시맵에 객체 생성을 하드코딩해둔 것이기 때문에 변경하려면 코드를 건드려야 한다. 이 부분도 아까처럼 Properties로 config.txt에서 읽어와서 객체를 생성하도록 바꿀 수 있다.
class AppContext{
Map map;
AppContext(){
try {
Properties p = new Properties();
p.load(new FileReader("config.txt"));
// Properties의 내용을 map에 저장
map = new HashMap(p);
// 반복문으로 클래스 이름을 얻어 객체 생성, value에 저장
for (Object key: map.keySet()){
Class clazz = Class.forName((String)map.get(key));
map.put(key,clazz.newInstance());
}
} catch (Exception e) {
e.printStackTrace();
}
}
newInstance는 deprecated 메서드로, 더는 사용하지 않는 편이 좋다. 편의상 일단 이렇게 쓰고 나중에 바꿀 수 있는 메서드를 찾아봐야겠다.
config.txt 처럼 외부파일을 사용하여 객체를 등록하는 방법이 있는가 하면 객체를 생성하여 저장하는 방법에는 자동객체등록이라는 방법도 있다.
필요한 클래스에 @Component라는 애너테이션을 붙이고, guava라는 라이브러리를 사용하면 @Component가 붙은 클래스를 찾아 객체를 자동으로 생성하여 map에 저장해준다.
config.txt같은 경우에는 새로운 객체가 추가될 때마다 직접 파일을 편집해야 하고, 여러 사람이 개발을 할 때는 수정이 쉽지 않기도 하다.
따라서 config.txt파일에는 애플리케이션에서 공통으로 사용할 객체들을 적어놓고, 개별적으로는 각자 자기가 맡은 클래스에 @Component 애너테이션을 사용하여 객체 저장소에 등록한다. 두 가지 방법을 적절히 사용하는 것이다.
Component 스캐닝을 하려면 guava라는 라이브러리가 필요하므로 Maven Repository에서 의존성 추가를 해준다. main 함수는 그대로 변경 없이 사용한다.
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.2-jre</version>
</dependency>
AppContext 안에 doComponentScan() 메서드를 만든다.
private void doComponentScan() {
try {
// 1. 패키지 내의 클래스 목록을 가져오기
ClassLoader classLoader = AppContext.class.getClassLoader();
ClassPath classPath = ClassPath.from(classLoader);
Set<ClassPath.ClassInfo> set = classPath.getTopLevelClasses("com.fastcampus.ch3.diCopy3");
// 2. 반복문으로 클래스를 하나씩 읽어 @Component 여부 확인
for (ClassPath.ClassInfo classInfo:set){
Class clazz = classInfo.load();
Component component = (Component) clazz.getAnnotation(Component.class);
// 3. @Component가 붙어있으면 객체 생성하여 map에 저장
if (component != null){
String id = StringUtils.uncapitalize(classInfo.getSimpleName());
map.put(id,clazz.newInstance());
}
}
} catch (Exception e) {
e.printStackTrace();
}
사실 여기서부터는 모르는 객체와 모르는 메서드가 많이 나와서 좀 혼란스러웠다. 다시 봐야겠다.
이렇게 작성하고, Car, Engine 등에 @Component 애너테이션을 붙여준 후 실행하면 객체가 맵에 생성된 것을 확인할 수 있다.