Spring DI 흉내내기

 

1. 변경에 유리한 코드(1) - 다형성, factory method

class Car{}
class SportsCar extends Car {}
class Truck extends Car {}

다형성을 이용하면 코드를 작성할 때 실수가 줄어들고, 코드의 활용과 수정이 편리해진다.

 

 

변경에 유리한 코드(2) - Map과 외부 파일

(Properties는 Map과 거의 동일하나, 파일 내용을 불러오는 것이 더 편리함)

 

[config.txt]
car=com.fastcampus.ch3.SportsCar
engine=com.fastcampus.ch3.Engine

config.txt 파일의 내용이 다음과 같이 구성되어 있다.

p.load(new FileReader("config.txt")); 의 결과로

Properties 객체에 Key와 Value에 각각 '='을 구분자로 하여 저장함.

Key Value
"car" "com.fastcampus.ch3.SportsCar"
"engine" "com.fastcampus.ch3.Engine"

프로그램의 변경이 필요없이, config.txt 파일의 내용만 수정하면 변경이 가능하다는 장점이 있다. 

 

 

2. 객체 컨테이너(ApplicationContext) 만들기

 

ApplicationContext는 저장소라고 생각하면 된다.

class AppContext {
	Map map;
    
    AppContext() {
    	map = new HashMap();
        map.put("car", new SportsCar());
        map.put("engine", new Engine());
    }
    
    Object getBean(String id) { 
    	return map.get(id);
    }
}
AppContext ac = new AppContext();
Car car = (Car)ac.getBean("car");
Engine engine = (Engine)ac.getBean("engine");

(Java Bean = 객체라고 이해하면 됨)

 

AppContext의 생성자를 통해 Map에 직접 객체를 생성하여 저장한다.

AppContext의 인스턴스를 만들어 원하는 객체를 사용할 수 있다.

 

 

이 과정을 Properties와 접목시키면 다음과 같다.

Properties p = new Properties();
p.load(new FileReader("config.txt"));
map = new HashMap(p);

for(Object key : map.keySet()) {
	Class clazz = Class.forName((String)map.get(key));
	map.put(key, clazz.newInstance());
}

config.txt 파일에 원하는 객체의 정보를 Properties 객체에 불러와 저장한 후 다시 map에 저장한다.

반복문을 통해 객체의 정보를 꺼낸 후 인스턴스를 만들어 다시 동일한 key에다 객체를 생성해 저장한다.

 

즉, config 파일의 내용(객체의 정보)만 바꾸면 원하는 객체를 코드의 수정 없이 저장이 가능하게 된다.

 

 

3. 자동 객체 등록하기 - Component Scanning

@Component 

 

package com.fastcampus.ch3.diCopy3;

import com.google.common.reflect.ClassPath;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

@Component class Car {}
@Component class SportsCar extends Car {}
@Component class Truck extends Car {}
@Component class Engine {}

class AppContext {
    Map map; // 객체 저장소

    AppContext() {
        map = new HashMap();
        doComponentScan();

    }

    private void doComponentScan() {
        try {
            // 1. 패키지 내의 클래스 목록을 가져온다.
            // 2. 반복문으로 클래스를 하나씩 읽어와서 @Component 애너테이션이 붙어있는지 확인
            // 3. @Component가 붙어있으면 객체를 생성해서 map에 저장
            ClassLoader classLoader = AppContext.class.getClassLoader();
            ClassPath classPath = ClassPath.from(classLoader);

            Set<ClassPath.ClassInfo> set = classPath.getTopLevelClasses("com.fastcampus.ch3.diCopy3");

            for(ClassPath.ClassInfo classInfo : set) {
                Class clazz = classInfo.load();
                Component component = (Component) clazz.getAnnotation(Component.class);
                if (component != null) {
                    String id = StringUtils.uncapitalize(classInfo.getSimpleName());
                    map.put(id, clazz.newInstance());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    Object getBean(String key) {
        return map.get(key);
    }
}

public class Main3 {
    public static void main(String[] args) throws Exception {
        AppContext ac = new AppContext();
        Car car = (Car) ac.getBean("car");
        Engine engine = (Engine) ac.getBean("engine");
        System.out.println("car = " + car);
        System.out.println("engine = " + engine);
    }

}

 

 

4. 객체 찾기 - by Name, by Type

Object getBean(String key) { // byName
        return map.get(key);
    } 
    Object getBean(Class clazz) { // byType
        for(Object obj : map.values()) {
            if(clazz.isInstance(obj))
                return obj;
        }
        return null;
    }

 

기존의 방법은 Map에 저장된 key의 name으로 객체를 생성했다면(byName)

Class의 타입으로 객체를 생성할 수도 있다. (byType)

 

Class 타입을 입력하면 map에 저장된 모든 value들(객체)을 하나씩 꺼내서

입력했던 Class와 같은 클래스 타입이면 그 객체를 반환한다.

 

public class Main3 {
    public static void main(String[] args) throws Exception {
        AppContext ac = new AppContext();
        Car car = (Car) ac.getBean("car"); // byName으로 객체를 검색
        Car car2 = (Car) ac.getBean(Car.class); // byType으로 객체를 검색
        Engine engine = (Engine) ac.getBean("engine");
        System.out.println("car = " + car);
        System.out.println("car2 = " + car2);
        System.out.println("engine = " + engine);
    }

}

car2 = com.fastcampus.ch3.diCopy3.SportsCar@50cbc42f

 

로 나오게 되는데, 그 이유는 SportsCar가 car의 자손이기 때문이다.

 

 

 

5. 객체를 자동 연결하기(1) - @Autowired

(by Type)

 

class Car {
	Engine engine;
	Door door;
}
AppContext ac = new AppContext();
Car car= (Car)ac.getBean("car");
Engine engine = (Engine)ac.getBean("engine");
Door door = (Door)ac.getBean("door");

car.engine = engine;
car.door = door;

-> 수동으로 객체참조를 연결했다

 

 

@Autowired 애너테이션을 사용하면 객체를 자동으로 연결할 수 있다.

(byType //  Map의 value에서 해당 Type을 찾는다)

class Car {
	@Autowired Engine engine;
	@Autowired Door door;
}

 

 

 

6. 객체를 자동 연결하기(2) - @Resource 

(by Name)

 

class Car {
	@Resource Engine engine;
	@Resource Door door;
}

@Resource(name="engine") Engine engine; 는 @Resource Engine engine; 과 같다.

name을 생략하면 클래스의 첫 글자를 소문자로 바꿔서 engine에 해당하는 객체의 주소를 저장하게 된다.

 

생략하지 않고 원하는 Name 객체의 주소를 저장할 수도 있다.

 

@Resource(name="engine2") Engine engine;

-> engine2라는 이름을 가진 engine객체를 Car의 engine의 주소로 저장하게 된다.

 

 

 

 

ex) @Autowired, @Resource 실습

 

package com.fastcampus.ch3.diCopy4;

import com.google.common.reflect.ClassPath;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ImportResource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

@Component class Car {
    @Autowired // 또는 @Resource 사용 가능
    Engine engine;
    @Autowired // 또는 @Resource 사용 가능
    Door door;

    @Override
    public String toString() {
        return "Car{" +
                "engine=" + engine +
                ", door=" + door +
                '}';
    }
}
@Component class SportsCar extends Car {}
@Component class Truck extends Car {}
@Component class Engine {}
@Component class Door {}

class AppContext {
    Map map; // 객체 저장소

    AppContext() {
        map = new HashMap();
        doComponentScan();
        doAutowired();
        doResource();

    }

    private void doResource() {
        // map에 저장된 객체의 iv중에 @Resource가 붙어 있으면
        // map에서 iv의 이름에 맞는 객체를 찾아서 연결(객체의 주소를 iv에 저장)
        try {
            for(Object bean : map.values()) {
                for(Field fld : bean.getClass().getDeclaredFields()) {
                    if(fld.getAnnotation(Resource.class)!=null) // btName
                        fld.set(bean, getBean(fld.getName())); // car.engine = obj;
                }
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    private void doAutowired() {
        // map에 저장된 객체의 iv중에 @Autowired가 붙어 있으면
        // map에서 iv의 타입에 맞는 객체를 찾아서 연결(객체의 주소를 iv에 저장)
        try {
            for(Object bean : map.values()) {
                for(Field fld : bean.getClass().getDeclaredFields()) {
                    if(fld.getAnnotation(Autowired.class)!=null) // btType
                        fld.set(bean, getBean(fld.getType())); // car.engine = obj;
                }
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    private void doComponentScan() {
        try {
            // 1. 패키지 내의 클래스 목록을 가져온다.
            // 2. 반복문으로 클래스를 하나씩 읽어와서 @Component 애너테이션이 붙어있는지 확인
            // 3. @Component가 붙어있으면 객체를 생성해서 map에 저장
            ClassLoader classLoader = AppContext.class.getClassLoader();
            ClassPath classPath = ClassPath.from(classLoader);

            Set<ClassPath.ClassInfo> set = classPath.getTopLevelClasses("com.fastcampus.ch3.diCopy4");

            for(ClassPath.ClassInfo classInfo : set) {
                Class clazz = classInfo.load();
                Component component = (Component) clazz.getAnnotation(Component.class);
                if (component != null) {
                    String id = StringUtils.uncapitalize(classInfo.getSimpleName());
                    map.put(id, clazz.newInstance());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    Object getBean(String key) {
        return map.get(key);
    } // byName
    Object getBean(Class clazz) { // byType
        for(Object obj : map.values()) {
            if(clazz.isInstance(obj))
                return obj;
        }
        return null;
    }
}

public class Main4 {
    public static void main(String[] args) throws Exception {
        AppContext ac = new AppContext();
        Car car = (Car) ac.getBean("car"); // byName으로 객체를 검색
        Door door = (Door) ac.getBean(Door.class);  // byType으로 객체를 검색
        Engine engine = (Engine) ac.getBean("engine");

        // 수동으로 객체를 연결
//        car.engine = engine;
//        car.door = door;

        System.out.println("car = " + car);
        System.out.println("engine = " + engine);
        System.out.println("door = " + door);
    }

}

doAutowired와 doResource 메소드를 추가하였다.

doAutowired()는 byType 으로, doResource()는 byName 으로

해당 애너테이션이 붙어있는 클래스에 각각 객체를 찾아서 연결해주는 메소드이다.

 


출처 : 스프링의 정석 : 남궁성과 끝까지 간다

+ Recent posts