본문 바로가기

백엔드 개발/SpringMVC

DI 흉내내기(2)

<개요>

저번 시간에 최종적으로 한 것은 DI를 흉내내어 변경에 유리한 코드를 만드는 것이었다. 

DI는 의존성 주입으로, 어떤 객체를 소스코드 내부에서 선언하는 것이 아니라, 외부에서 객체를 생성한 후 소스코드 내부로 주입하는 것을 의미한다. 

저번 포스팅에서는 DI를 흉내내기 위하여 객체 생성 기능을 하는 매소드를 이용했다.  해당 매소드에 인수로 보낸 값이 properties의 Key 값으로 있다면, 매소드는 해당 Key에 대응되는 Value (전체이름 = 경로주소.클래스이름)를 이용하여 Class class 객체를 만들고, 그 Class class 객체를 이용하여 실제 객체를 만들어 반환 했다. 

Properties는 map처럼 <K,V>형식을 띄고 있으나, K,V에 오직 String 값만 가능했다. 

 

이번에는 객체 컨테이너라는 객체 저장소를 따로 만들고, 그 안에 객체를 집어넣어 필요할 때 꺼내쓰는 방식으로 DI를 구현해보겠다. 이 또한 객체 컨테이너에 미리 만들어놓은 객체를 빼서 쓰는 거라, 소스코드 내부에서 객체를 생성하지 않는다. 

그래서 DI라고 볼 수 있다. 

 

** 객체 컨테이너 사용과 외부 파일 활용의 차이점**

외부 파일 이용하는 로직은 외부 파일 안에 <객체 이름, 객체의 경로주소.클래스이름>의 K V 형식의 map이 있었다. 

여기서 경로주소를 이용하여, 해당 클래스의 메타데이터를 뽑고, 그 메타데이터를 이용해 객체를 생성하여 반환했다. 

 

객체 컨테이너는 컨테이너 안에 Map 형식으로 <객체 이름, 객체>가 존재한다. 

객체를 Reflection API 사용하여 만들 필요 없이 바로 꺼내다가 쓰면 된다. 

 

**참고**

Reflection Api 는 
클래스의 메타데이터를 이용하여 실제 해당 클래스의 
변수나 매소드를 구현하여 사용할 수 있게 해주는 기능

 

일상 생활 예를 들면, 외부 파일 이용은 냉장고 안의 재료를 꺼내서 내가 직접 요리 해서 먹는 것이고, 객체 컨테이너는 냉장고에 엄마가 해두고 간 요리를 바로 꺼내 먹는 것이다. 

 

1. Map 이용하여 객체 컨테이너 구현

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

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

    // 생성자 -> 객체를 생성하면 할 행동들을 안에 정의 
    AppContext(){
        map = new HashMap<>();

        try {
            // properties에 config의 <K,V> 내용 받아오기
            Properties p  = new Properties();
            p.load(new FileReader("config.txt"));

            //proporties에 있는 내용을 map에 저장 아직 map 안에는 <객체이름, 경로주소>
            map = new HashMap<>(p);

            // 반복문으로 <K,V>를 <객체이름, 객체>로 갱신
            // Map은 Key값이 중복 저장 안됨을 이용. 젤 최근 내용으로 갱신됨.
            for (Object key :
                    map.keySet()) {
                // map의 Value 이용, Map은 <String, Object>이기 때문에 다시 Value를 String으로 형 변환 해줘야함.
                // 경로주소에 해당하는 클래스의 메타데이터를 clazz에 저장
                Class clazz = Class.forName((String) map.get(key));
                
                // map 갱신, 경로 주소 이용해 만든 진짜 객체로
                map.put(key, clazz.newInstance());

            }
            
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    //객체 이름을 인수로 넣으면 객체를 반환하는 매소드 
    Object getBean(String key) {
        return map.get(key);
    }

}

public class main2 {
    public static void main(String[] args) throws Exception {
        // 저장소 생성 
        AppContext ac = new AppContext();
        
        // 매소드의 반환타입이 Object 이므로, 받을 때 형변환 필요 
        Car car = (Car)ac.getBean("car");
        Engine engine = (Engine)ac.getBean("engine");
        System.out.println("car = " + car);
        System.out.println("engine = " + engine);
    }
}

 

2. 외부 파일 없이 자동으로 Map을 <객체 이름, 객체>로 채우고 쓰는 법

// 어노테이션 달려있는 것만 찾아서 객체 컨테이너(map)안에 넣어준다. 
// map안에 없으면 getBeans 함수가 찾을 수 없기에 객체가 null로 뜬다. 
@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 {
            ClassLoader classLoader = AppContext.class.getClassLoader();
            ClassPath classPath = ClassPath.from(classLoader);

            // 1. 패키지 내의 모든 클래스를 읽어서 Set에 저장
            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 ){
                    
                    // id(객체 이름)은 클래스의 첫 대문자를 소문자로 만들어 저장 Car -> car 
                    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);
    }
}

3. 스스로 해보기 

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

class AppContext {

    //객체 저장소
    Map map ;

    // 생성자 -> 객체를 생성하면 할 행동들을 안에 정의
    AppContext(){
        map = new HashMap();


        try {
            // properties에 config의 <K,V> 내용 받아오기
            Properties p = new Properties();
            p.load(new FileReader("config.txt"));
            //proporties에 있는 내용을 map에 저장(새로 초기화) 아직 map 안에는 <객체이름, 경로주소>
            map = new HashMap<>(p);
            // 반복문으로 <K,V>를 <객체이름, 객체>로 갱신
            for (Object obj :
                    map.keySet()) {
                Class clazz = Class.forName((String)map.get(obj));
                map.put(obj, clazz.newInstance());
            }
            // Map은 Key값이 중복 저장 안됨을 이용. 젤 최근 내용으로 갱신됨.

            // map의 Value 이용, Map은 <String, Object>이기 때문에 다시 Value를 String으로 형 변환 해줘야함.
            // 경로주소에 해당하는 클래스의 메타데이터를 clazz에 저장


            // map 갱신, 경로 주소 이용해 만든 진짜 객체로
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }


    }


    //객체 이름을 인수로 넣으면 객체를 반환하는 매소드
    Object getBean(String key){return map.get(key);}

}

public class main2Work {
    public static void main(String[] args) throws Exception {
        // 저장소 생성
        AppContext ac = new AppContext();

        // 매소드의 반환타입이 Object 이므로, 받을 때 형변환 필요
        Car car = (Car)ac.getBean("car");
        Engine engine = (Engine)ac.getBean("engine");
        System.out.println("car = " + car);
        System.out.println("engine = " + engine);
    }
}
car = com.fastcampus.ch3.diCopy2.Truck
engine = com.fastcampus.ch3.diCopy2.Engine

 

두번 째 방법으로 Map에 자동 등록하는 경우, 

패키지 외부의 클래스들은 경로를 외부로 설정해도 못 가져온다.

어떤 함수에 붙어 있던 어노테이션을 삭제하면  doComponentScan 매소드가 해당 어노테이션이 삭제된 클래스를 Map에 저장하지 않는다.