본문 바로가기

백엔드 개발/SpringMVC

DI 흉내내기

<개요>

DI란 Dependency Injection으로서 의존성 주입을 뜻한다.

이는 어떤 클래스의 객체를 new 연산자를 통해 소스코드 내부에서 만드는 것이 아니라, 해당 클래스의 객체를 외부에서 만들어서 소스코드 내부로 주입 시키는 것을 말한다. 

이렇게 하는 이유는 변경에 유리한 코드를 만들어, 개발자가 할 수 있는 실수를 줄이고, test 횟수도 줄이기 위해서 이다. 

 

그럼 변경에 유리한 코드라는 것이 무엇인지 알아보고, 이를 어떻게 표현해야하는지 알아보자. 

 

0. 가정

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


SportsCar car 	= new SportsCar ();
// (바꾸려는 것) 
Truck car = new Truck();

먼저 SprotsCar와 Truck은 Car의 자식 클래스 이고, 우리가 하려는 것은 객체 car를 SportsCar의 객체에서 Truck의 객체로 수정하려는 것이다.

1. 변경에 유리한 코드 (다형성 이용, Method 이용)

(1)다형성 이용

** 다형성이란? 

하나의 객체가 여러 Type을 가질 수 있는 것을 말한다.

자바에서는 부모 클래스의 참조변수로 자식 클래스의 객체를 참조할 수 있는 것으로 실현했다.

Car car = new SprotsCar();

Car car = new Truck();

다형성을 이용하기 전에는 객체의 자료형과 new 뒤의 동적할당 부분으로 고쳐할 곳이 2 군데 였지만, 

다형성을 이용하면, 동적할당 부분만 고치면 되어서 수정 부분이 많이 줄었다.

하지만 Truck으로 선언을 재 수정 해야하는 객체가 N 개 있을 경우, N번의 객체 선언 수정이 이루어져야 한다는 단점이 있다.

(2)Method 이용 (기능과 사용의 분리)

Method이용은 다형성을 이용한 경우의 단점을 보완할 수 있다. 

기능과 사용을 분리하므로서 그렇다. 

기능과 사용의 분리는 동적할당하는 부분(기능)과 선언하는 부분(사용)을 나누는 것을 의미한다. 

Car car = getCar();


// (수정 전)
static Car getCar() {
	return new SportsCar();
}

// (수정 후)
static Car getCar(){
	return new Truck();
}

이렇게 만들면 Truck으로 재수정 해야하는 객체가 N 개 있더라도 선언부를 고칠 필요가 없다. 

함수의 return 부분을 한 줄만 고치면 되서 변경에 더 유리하다. 

2. DI의 원리를 흉내내서 만들어보기

위의 Method 원리를 활용하여 더 변경에 유리한 코드를 만들 수 있다. 

Car car = getCar();

static Car getCar() throws Exception {

	//Properties는 <K,V>를 각각 <String, String>으로만 받는 Map이라 생각하면 된다.
	Properties p = new Properties();
    	// 파일의 내용 읽어와서 Properties에 Key Value 형식으로 저장
    	p.load(new FileReader("config.txt"));
    
    // 해당 경로주소에 있는 클래스의 메타데이터를 담은 Class 객체 clazz 생성
	Class clazz = Class.forName(p.getProperty("car"));    
    
    // .newInstance()는 clazz의 메타 데이터를 이용해 Car의 객체를 만들고, Object Type으로 반환 
	return (Car)clazz.newInstance();
}

config.txt 에 있는 내용

Properties 클래스는 Map이랑 거의 똑같다. 

Properties 클래스도 값을 Key = Value 형식으로 받는다.

다만 다른 점은 Map은 <Key, Value> 쌍을 각각 Object로 받지만, Properties는 <String, String>으로 받는다. 

 

DI의 좋은 점. 

매소드 이용은 변경 사항이 생길 경우, 매소드 내부를 수정해줘야 했다. 

DI는 이보다 더 변경에 유리하다. 

그 이유는 DI는 수정 사항이 생길 경우 변경해야 하는 내용이 소스코드 밖에 있기 때문이다! 

그래서 변경 사항이 생겨도 소스 코드를 바꿀 필요가 없고, config.txt 같이 불러오는 문서의 내용을 바꾸면 된다. 

소스 코드 자체를 안 건드려도 된다는 것은 수정 시마다 test를 할 필요가 없고, 변경하며 할 수 있는 개발자의 실수를 최소화 할 수 있다는 것이다. 

 

** 심화 ** 

위의 소스코드는 getCar 매소드가 오로지 Car의 객체 선언시에만 사용할 수 있다는 단점이 있다. 

만약 다른 클래스의 객체 선언 시에도 위와 같이 변경에 유리한 코드를 쓰려면 하나 더 만들어야 한다. 

이를 보완한 것이 심화의 내용이다.

static Object getObject(String key) throws Exception {
    // <String, String> 만 되는 map, Properties 객체 생성
    Properties p  = new Properties();

    // config.txt의 내용 읽어서 p의 <K,V>안에 넣기.
    // config.txt 또한 K,V 형태로 적혀있기 때문에 문제 없다. config.txt의 K는 객체 이름, V는 해당 객체의 클래스의 경로 주소
    p.load(new FileReader("config.txt"));

    // 어떤 key 값에 대응하는 value는 클래스의 경로 주소임.
    // Class 클래스의 forName 매소드는 어떤 클래스의 전체 이름(경로 주소.클래스이름)을 인수로 받아,
    // 그 클래스의 class 객체 (메타 데이터)를 반환함.
    Class clazz = Class.forName(p.getProperty(key));

    // 메타데이터(설계도)를 통해 진짜 객체를 만들어 반환
    return clazz.newInstance();
}

위의 매소드는 객체 선언 시 선언하려는 클래스가 무엇인지 인수로 받게끔 하였다. 

이를 forName의 경로 이름 찾을 때 사용하여 그에 해당하는 Class 클래스, 객체를 만들 수 있게 도왔다.

이런 식으로 코드를 바꾸면, 어떤 클래스든 간에 객체를 만들 수가 있다. 

여기서 보면 반환 값이 object인데, return에는 어떠한 형 변환도 이루어져 있지 않다.

public static void main(String[] args) throws Exception {
    // 객체를 생성 하는데 getObject라는 매소들르 이용
    Car car = (Car)getObject("car");
    Engine engine = (Engine)getObject("engine");

    // 객체가 제대로 생성 되었는지 확인
    System.out.println("car = " + car);
    System.out.println("engine = " + engine);
}

그 이유는 다음과 같다. 매소드로 들어오는 값마다 타입이 천차 만별이라 매소드 내에서 형 변환 시키기가 어렵다. 

따라서 선언부에서 형변환을 해준다. 

config.txt에 들어가 있는 값

3. 스스로 해보기 

package MyWork;

import java.io.FileReader;
import java.util.Properties;

class Lunch {}

class KoreanFood extends Lunch {}
class JapanFood extends Lunch {}

public class mainWork {
    public static void main(String[] args) throws Exception{

        Lunch today = (Lunch) getObject("KR");
        Lunch yesterday = (Lunch) getObject("JPN");
        System.out.println("today = " + today);
        System.out.println("yesterday = " + yesterday);
    }

    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 안 속

KR = MyWork.KoreanFood
JPN = MyWork.JapanFood

경로가 부정확하면 클래스의 메타데이터를 얻어올 수가 없어서 에러가 난다.