Spring DI 미리보기(1)

강서진·2023년 11월 24일
0

Spring

목록 보기
1/18
post-custom-banner

강좌 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

객체 컨테이너(Application Context)

객체 컨테이너라는 것은 객체 저장소이다. 이번에는 아까와 비슷하지만 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 메서드로, 더는 사용하지 않는 편이 좋다. 편의상 일단 이렇게 쓰고 나중에 바꿀 수 있는 메서드를 찾아봐야겠다.

자동객체등록 (Component Scanning)

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 애너테이션을 붙여준 후 실행하면 객체가 맵에 생성된 것을 확인할 수 있다.

post-custom-banner

0개의 댓글