본문 바로가기

백엔드 개발/SpringMVC

DI 활용하기 - 실습

0. 지금까지 한 것들 

지금까지는 DI라는 컴퓨터 본체의 내부를 뜯어 봤다. 

Main1은 Di의 개념: 외부에서 객체를 생성해서 소스코드 내부로 주입한다는 게 어떤 의미인지 알아보았다.

Config.txt라는  문서에 객체이름 = "해당 객체 구현하는 클래스의 경로주소" 로 값들을 저장해놓는다. 

그러면 Main1 내부의 함수가 문서 내용을 받아와서, 경로주소를 ReflectionApi를 이용해 메타데이터로 진짜 객체를 만들어서 반환한다.

우리는 new 선언 없이 해당 매소드만을 이용해 객체 생성이 가능해졌다.

Main2는 객체 저장소라는 개념을 소개 했고 그것이 어떻게 돌아가는지 로직을 설명했다.

객체 저장소는 Config.txt의 내용을 Map 형태로 일단 받아온다.

그러면 Key는 객체이름, Value는 해당 객체 만드는 클래스의 경로주소로 들어온다. 

이때 Value들을 loop 돌려서, Reflection APi를 이용해 진짜 객체로 만들어 다 갱신한다. 

이제 우리는 객체 저장소 Map을 이용해 이미 만들어진 객체를 이용해서 일을 처리한다.

Main3는 더 이상 외부 문서인 Config.txt를 사용하지 않고, @Component를 이용해 객체 저장소에 객체를 채우는 법을 설명 했다. 

패키지 내에 @Component가 붙은 클래스들이 있는지 전수조사를 한다. 

@Component가 붙은 클래스들은 해당 클래스의 첫글자를 소문자로 바꾼 이름을 객체저장소의 Key로 넣는다. 

해당 클래스의 객체를 하나 만들어 Value로 넣는다. 

이제 우리는 외부 문서 없이도 @Component만 쓰면 Spring이 위의 동작들을 자동으로 해주어 이미 생성된 객체들을 필요 시마다 꺼내 쓸 수 있게 된다. 

Main4는 A클래스의 iv로 B,C 클래스의 객체가 있을 경우, A의 iv로 생성된 B,C의 객체를 어떻게 연결시키는지에 대해 설명했다. 이때 사용하는 것이 @AutoWired와 @Resource였다.

위의 @Component를 통해 이미 객체 저장소에 Map 형태로 객체들이 저장되어 있다고 가정하자.

근데 만약 A클래스의 iv가 각각 B,C 클래스의 객체일 경우, 해당 객체들에 @AutoWired나 @Resource를 미리 붙여 놓는다. 

그러면 Spring이 해당 어노테이션이 붙은 녀석과 같은 타입의 객체(@AutoWired 썼을 시), 같은 이름의 객체(@Resource 썼을 시)가 Map안에 있는지 찾아보고 있으면 자동으로 iv 객체들에 대입 시킨다. 

해당 어노테이션을 이용하면, 우리가 수동으로 생성된 객체들간의 관계를 선언하지 않아도 된다. 외부에서 다 해주기 때문. 

2. 코드리뷰

이제 해당 어노테이션들을 실전에서 바로 써보겠다.

 

A. Main2의 원리 (객체 저장소를 만들어 외부 문서의 내용을 불러들여 객체들을 미리 만들어 놓고 꺼내 쓰는 것)도 실전에서 쓰인다. 

Logic은 다음과 같다. 

(1) Spring 환경 설정 파일을 위의 Config.txt 처럼 만들어서 객체이름 = "해당 객체 구현할 클래스의 경로주소"들을 저장한다. 

(2) 객체 저장소를 SpringFrameWork로부터 수입한다. 객체 저장소 객체를 만들어 아까 만든 환경설정 파일을 로드 한다. 

그러면 Spring이 자동으로 <객체이름, 객체>의 Map 저장소를 만들어준다. 

(3) 객체 저장소 내장 매소드인 getBeans()를 이용하여, 객체 선언 시 이미 생성된 객체를 거기에 대입 시켜준다.

 

-----로직 세부 설명-----

(1)환경 설정 파일 만들기

위에서 Spring Config를 누르자. 이는 Spring 환경설정 파일을 만드는 것이다.

(1-1) Bean에 등록 

우리가 Di 흉내낼때, txt 파일에 위와 같이 Map 형식으로 적어 놓았고, 이걸 불러들여서 객체 저장소의 Map을 채워넣은 적이 있었다.

해당 과정은 실전에서는 환경설정파일 (XML configuration file)에 Bean으로 우리가 객체 저장소에 저장할 내용들을 저장하는 방식으로 구현할 수 있다. <객체이름, 클래스 경로주소>라는 형식에서 매우 유사하다. 

 

(2)객체 저장소 SpringFrameWork에서 가져오기, 내장 함수 getBeans 이용해 선언된 객체에 만들어진 객체 대입 

GenericXmlApplicationContext를 써야 환경설정 파일을 로드해서 쓸 수 있다. 

객체 저장소의 내장 매소드인 getBean는 리턴 타입이 Object임으로 형변환이 항상 필요하다. 

하지만 두 번째 인자로 해당 객체의 메타데이터를 넣어주면 형변환을 매소드 내부에서 해주어서 따로 형변환 할 필요가 없다. 

그러면 위의 출력 결과를 한번 보자. 

Car와 Car2가 같은 객체를 참조하고 있음을 알 수  있다. 이는 Beans가 기본적으로 Sington을 지향하고 있기 때문이다. 

Sington이란 하나의 클래스에 하나의 객체만 생성한다는 원칙으로, 브라우저가 같은 기능을 여러 번 요청하면 해당 요청들 하나하나 따로 객체를 만들어 대응하지 말고, 하나의 객체만 만들어 계속 재활용하여 대응하는 것을 의미한다. 이는 용량 효율을 높인다. 

하지만 어떤 상황에서는 Car와 Car2가 완전히 다른 객체여야 하는 경우도 있다. 

이 경우 Beans에 다음과 같은 속성을 추가로 걸어준다. 

scope ="prototype" 으로 걸어두면, "car" 혹은 Car type에 대한 요청이 여러 번 오면 매번 다른 객체를 만들어서 반환한다. 

밑에 scope="singleton" 이란 속성도 걸어놨는데 이는 bean의 default 값이어서 굳이 따로 선언 안해도 된다. 

위의 설정 파일로 실행 결과 두 객체가 서로 다른 객체를 참조하는 것을 알 수 있다. 

 

B. 환경 설정 파일의 Bean을 이용해서 A 클래스 내부에 B,C 클래스의 객체 iv와 객체 저장소 Map에 생성된 객체들을 연결 시켜 주는 법

(1) 먼저 Car 내부를 기본형과 참조형iv들로 채우자. (test 위해)

(2) 수동으로 초기화 시켜보기 (수동으로 연결)

(3) property라는 Bean 내부 태그를 이용해 iv 초기화 

(4) Constructor-arg라는 태그를 이용해 iv 초기화 

 

------- 세부 설명-------

(1)

class Car {
    String color ;
    int oil;
    Engine engine;
    Door doors[];



    public void setColor(String color) {
        this.color = color;
    }

    public void setOil(int oil) {
        this.oil = oil;
    }

    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public void setDoors(Door[] doors) {
        this.doors = doors;
    }

    @Override
    public String toString() {
        return "Car{" +
                "color='" + color + '\'' +
                ", oil=" + oil +
                ", engine=" + engine +
                ", doors=" + Arrays.toString(doors) +
                '}';
    }
}

**toString의 역할 **

toString() 매소드는 원래 모든 클래스의 조상 Object 클래스의 매소드로 해당 객체 자체를 출력할 시에 작동하는 매소드 이다. 

우리가 toString 매소드를 재정의해주지 않은 채 Car 객체를 출력하면 밑과 같이 출력된다.

이는 내용물이 뭔지 보기 어렵다. 

위의 코드처럼 toString을 재정의 하면 car 객체 자체를 출력할 시 밑과 같이 출력한다.

(2) 수동으로 iv 초기화

** 결과 **

Doors의 배열의 원소들이 다 같은 객체인 것을 알 수 있다. 이는 Bean 속성에서 scope="prototype"으로 바꾸면 된다.

 

(3) Beans의 속성 이용해, 객체들을 선언만 해도 자동으로 초기화 되도록 하는 방법

property 태그 이용. 

그러면 Car 객체 내의 iv인 engine과 door 와 같은 type의 객체 정보가 Beans에 등록되어 있는지 보고 있다면, 객체저장소에 그 정보가 구현된 해당 객체들과 iv를 연결시켜줌.

기본형은 value로 초기화 / 참조형은 ref로 초기화 / 배열은 array라는 태그가 한번 더 필요함.

해당 Property란 태그는 클래스 내부의 setter 매소드를 이용한다. 

따라서 Car 클래스 내부에 각 iv에 대한 Setter를 구현해놓지 않는다면 Property가 작동하지 않아서 iv가 초기화 되지 않는다.

 

(4) property가 class의 setter 매소드를 사용한다면, constructor-arg는 class의 constructor 를 사용하여 iv들을 초기화 한다.

 

인수 넣어서 실제 활용하는 생성자 말고, 그냥 빈 기본 생성자도 만들어놔야 오류가 안 뜬다. 

잘 나온다.

 

C. Bean에 수동 등록하지 않고, ComponentScan이란 태그 이용해서 자동 등록하기

지금까지 환경설정 파일 (configuration.xml)에 직접 Bean을 작성하여 등록했다. 

Bean에는 id = "객체이름" , class= "해당 객체 구현하는 클래스의 경로주소"로 저장이 되어 있었고, 

소스코드내의 선언한 객체 저장소가 해당 정보를 받아가서 소스코드 내에 <객체이름, 객체>의 Map을 만들었다. 

그리고 우리는 그것을 사용했다. 

 

이제 ComponentScan 태그를 사용하여 자동으로 환경설정 파일에 bean을 등록해 보겠다.

 

(1) Bean 수동 등록했던 거 다 지우고 @Component 어노테이션 달린 클래스들 자동으로 환경 설정 파일에 bean으로 등록해주는 ComponentScan 쓰기 

 

(2) 결과 확인 에러

why? 예전에 다른 diCopy 폴더에서 연습하기 위해 썼던 Car라는 파일이 똑같이 또 있기 때문에.

그렇다면 해당 폴더들의 클래스들은 제외하고 나머지 다 bean에 등록하라는 명령어는?

 

(3) @Component 붙은 클래스 등록할 때 id 이름을 바꾸고 싶다면? 

default로 생성되는 id(객체 이름)은 클래스의 앞 글자 소문자로 바꾼 것, 

바꾸려면? 

 

(4)Engine의 자식 클래스 만들고, 객체 저장소에 Engine 계열 클래스 모두 등록한 뒤, Car의 iv에 연결

- ac.getBean 매소드(객체 저장소 내부 매소드) 이용시 오류

- @AutoWired 사용 시  해당 오류는 잡았지만, 또 오류 나는 상황이 있음. 

- @AutoWired의 오류를 극복하는 방법

 

(1)

xml 환경 설정 파일 내부

"~"안은 뒤져볼 주소, 해당 주소의 하위 폴더는 전부 뒤져서 @Component 붙은 클래스들 찾아내어 Bean 등록

소스코드 내부에 Bean 등록할 클래스들에 어노테이션 붙이기 

run 하기 

 

(2)

오류 발생. 이유는 경로 주소 내에 Car라는 클래스가 @Component 어노테이션을 붙은 채 중복되어 존재한다. 

그래서 Spring은 뭘 이용해서 객체 저장소에 객체 만들어 저장할지 모르겠다는 것이다. 

 

<해결방안 - 해당 ch3.diCopy 폴더들은 뒤지지 말라>

해당 태그 추가 

 

<다시 run>

이제 오류는 안 나지만 값들이 다 Null 임 

이유는 객체 생성만 했지, 객체들간의 관계를 연결을 안했다. 

여기서는 Car의 iv 값으로 engine과 doors를 넣어주지 않았다. 

 

a. 수동으로 넣기. Car 클래스의 setter를 이용해 수동으로 넣기 

<결과>

여기서 door 배열 내에서 객체가 같음을 알 수 있다. 

이건 컴포넌트 스캔으로 쓰면 고칠 수 없는 부분이다. 원래는 property 태그의 scope를 prototype으로 해서, 

객체 생성 요청이 있을 때마다 새로운 객체를 만들어 반환하도록 만들어야 했다. 

 

이제 그러면, setter 써서 사용자가 직접 값들을 연결 짓는 방법 말고, Spring이 이를 자동으로 해주는 방법에 대해 알아보자.  우리가 Di 흉내내기에서도 배웠듯이 2가지 방법이 있다. 하나는 객체 저장소 Map에서 Value(객체들의 type)을 뒤져서 

Class의 iv와 맞는 객체를 연결시켜 주는 @AutoWired 이고, 다른 하나는 Map에서 Name (객체 이름)으로 찾는 @Resource가 있다. 

 

먼저 @AutoWried를 먼저 써보겠다. 

그리고 기능 테스트를 위하여 Engine의 자손 클래스들을 만들고 @Component를 만들어 객체 저장소에 추가한다.

이제 객체저장소 ac의 Map은 대략 이런 형태일 것이다. 

Key (객체이름) Value (객체)
car Car 객체
engine Engine 객체
superengine SuperEngine 객체
turboengine TurboEngine 객체
door Door 객체 

** 참고할 점 ** 

우리는 ac.getBean이라는 객체 저장소 내장 함수를 통해 선언된 객체에 이미 만들어진 객체들을 대입해 왔었다. 

 

(3)

@Component ("Map의 키로 넣고 싶은 객체의 이름") 클래스 이름.

이렇게 적어주면 객체 저장소의 <K, V>로 값이 저장될 때, "" 안의 내용으로 Key가 저장된다. 

이러한 설정을 따로 해주지 않고 @Coponent 클래스 이름을 쓰면 

@Component ("클래스 이름 첫 글자 소문자로 한 것") 클래스 이름과 같은 의미이다. 

Engine의 자손 클래스들을 더 만들어 객체 저장소에 등록하기 전까지는 ac.getBean의 인수를 "객체이름"으로 하든지 아니면 Engine.class 와 같은 메타데이터(객체의 type)을 넣든지 아무 상관이 없었다. 

하지만 이제 

이렇게 넣으면 에러가 난다.

이런 에러가 나타나는 이유를 터미널에서 설명해 주었다. 

Engine.class란 type과 일치하는 객체가 3개나 있어서 무엇을 객체 선언부와 연결해줘야 할지 모르겠다는 것이다. 

클래스 타입으로 객체 저장소의 value를 뒤져서 찾는 방법은 해당 클래스의 객체가 중복 생성되어 있거나, 해당 클래스의 자손 클래스 객체가 저장소 내에 있는 경우, 선언부와 연결할 객체를 특정하지 못해서 에러가 난다. 

따라서 객체 이름으로 정확하게 지정하여 객체 선언부와 이어 줘야 한다. 

올바른 예시

 

다시 돌아와서 @AutoWired를 Car 클래스의 iv에다가 등록해보겠다.

참고로 기본형 iv에 값을 연결해주려면 Value("원하는 값")으로 어노테이션을 걸어주면 된다. 

기본형 타입이 뭐든지 간에 Value의 인수는 String이다. Spring이 String 인수를 받아도, 기본형 타입에 맞게 형변환 해주니까 걱정 안해도 된다. 

이 경우 결과가 정상적으로 나온다. 

AutoWired를 쓰면 배열에 하나의 원소 밖에 못 넣는다는 단점이 있다. 

근데 AutoWired도 분명 Class type으로 Value를 뒤져 일치하는 녀석을 iv와 연결 시켜주는데, 이상하게 ac.getBeans(Class.type)과 달리 에러가 나지 않았다. 

그러면 AutoWired는 어떻게 같은 클래스 계열인 Engine, SuperEngine, TurboEngine 객체들 사이에서 Engine을 골랐을까? 

@AutoWired는 먼저 클래스 타입으로 Value를 검색한 뒤, 같은 클래스 타입이 여러 개이면, 그 중에서 현재 어노테이션 걸린 iv의 클래스 (여기선 Engine)의 앞 글자를 소문자로 바꾼 객체 이름이 Map의 Value들 사이에 있는지 찾는다. 그리고 있다면 그 녀석을 우선적으로 연결한다. 

 

그렇다면 이번엔 Engine이라는 객체를 객체 저장소에서 제외해보자! 

@Component 태그를 지우면 Bean 등록이 안될 것이고, 그러면 ac에도 들어가지 않을 것이다.

그러면 오류가 난다. 왜냐면 해당 상황은 @AutoWired가 객체를 찾는 우선순위 메뉴얼에 없는 상황이기 때문이다!

@AutoWired는 SuperEngine 객체와 TurboEngine 객체 사이에 뭘 고를지 몰라서 에러를 내보낸다. 

이 경우 @Qualifier("객체이름")라는 어노테이션을 이용하여, 해당 클래스 type이 여러 개일 시 뭘 선택해서 써줘야 할지 골라주면 된다. 

그러면 해당 engine이란 iv는 저장소에 있는 superEngine의 객체와 연결이 된다. 

** 참고**

@AutoWired 부분 진행하면서, 밑의 Engine과 Door 선언부들은 주석처리 했다. 

해당 내용들은 engine,door 란 객체 선언하고 객체 저장소에 이미 있는 Engine과 Door의 객체를 연결 시켜주는 내용이다. 

하지만 이제 우리는 Car 객체 내 iv 연결만 신경 쓰면 되므로 필요 없는 내용이다. Car만 객체 생성 해주면 된다.

사실 Resource로 하면 위의 과정이 한번에 해결된다. 

하지만 둘의 로직은 다르다. @AutoWired와 @Qualifier("객체이름")를 쓰는 경우, 처음에는 class type으로 찾아서 같은 계열의 객체들을 뽑은 다음 그 중에서 객체 이름으로 찾는 것이다. 

@Resource(name="객체이름")은 처음부터 Map 저장소의 Key(객체이름)을 뒤져서 같은 걸 찾는다. 

 

실무에서는 전자가 더 많이 쓰인다. 클래스 타입은 잘 바뀌지 않지만, 객체이름은 변동성이 상대적으로 크기 떄문이다. 

3. 스스로 해보기 

위의 해석을 하며 전 과정 내가 다 해보았다 .