도구 다루기-3

의존성 주입이란?

ConferenceWebSvc 객체에 서비스를 캡슐화하고 메시지를 화면에 표시할 자바스크립트 객체 Messenger를 작성한다.

참가자는 1인당 세션을 10개까지 등록할 수 있다. 참가자가 한 세션을 등록하면 그 결과를 성공/실패 메시지로 화면에 표시하는 함수를 개발해야 한다. 아래 코드는 해당 기능을 구현하기 위한 초기 버전이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Attendee = function (attendeeId) {
// 'new'로 생성하도록 강제
if (!(this instanceof Attendee)) {
return new Attendee(attendeeId);
}

this.attendeeId = attendeeId;
this.service = new ConferenceWebSvc();
this.messenger = new Messenger();
}

// 주어진 세션에 좌석 예약을 시도하고 성공/실패 여부를 메시지로 알려줌.
Attendee.prototype.reserve = function (sessionId) {
if (this.service.reserve(this.attendeeId, sessionId)) {
this.messenger.success(
`좌석 예약이 완료되었습니다! 고객님은 ${this.service.getRemainingReservations()} 좌석을 추가로 예약하실 수 있습니다.`
);
} else {
this.messenger.failure('죄송합니다. 해당 좌석은 예약하실 수 없습니다.');
}
}

이 코드는 ConferenceWebSvc, Messenger, Attendee 객체가 각자 자신만의 임무를 갖고 모듈로 조화를 이룬것 처럼 보인다. Attendee.reserve는 너무 간단해서 굳이 단위 테스트를 하지 않아도 되는데, 어차피 그럴 수도 없다. ConferenceWebSvc 내부에는 HTTP 호출이 있다. Messenger는 메시지마다 OK 버튼이 있어야 하는데, 이 또한 이 모듈에서 단위 테스트할 대상은 아니다. 단위 테스트는 자바스크립트 코드를 바르게 작성하기 위한 핵심인데, 모든 단위가 미처 준비도 되기 전에 시스템 테스트의 늪으로 빠지는 게 싫다.

요는, Attendee 객체가 아니라 이 객체가 의존하는 코드다. 의존성을 주입하는 식으로 바꾸면 해결할 수 있다. 즉, ConferenceWebSvc와 Messenger와의 의존성을 하드 코딩하지 말고 이들을 Attendee에 주입하는 것이다. 실제 운영 환경에서는 진짜 의존성을 주입하겠지만, 단위 테스트용으로는 모의체(fake)나 재스민 스파이 같은 대체제를 주입하면 된다.

1
2
3
4
5
// 운영 환경:
var attendee: new Attendee(new ConferenceWebSvc(), new Messenger(), id);

// 개발 환경:
var attendee = new Attendee(fakeService, fakeMessenger, id);

이처럼 의존성을 주입하는 것을 두고 ‘빈자의 의존성 주입’이라 한다. 아래 코드는 빈자의 의존성 주입 방식으로 작성한 Attendee 객체다.

1
2
3
4
5
6
7
8
9
10
Attendee = function (service, messenger, attendeeId) {
// 'new'로 생성하도록 강제
if (!(this instanceof Attendee)) {
return new Attendee(attendeeId);
}

this.attendeeId = attendeeId;
this.service = service;
this.messenger = messenger;
}

의존성을 주입하여 믿음직한 코드 만들기

의존성을 주입하여 다른 방법으로는 할 수 없는 단위 테스트를 어떻게 하는지 알았다. 아무래도 테스트를 통과한, 자동화한 테스트 꾸러미로 계속 테스트할 수 있는 코드가 더 믿음직하다. 이 뿐만 아니라, 의존성 주입은 실제 객체보다 주입한 스파이나 모의 객체에 더 많은 제어권을 안겨주므로 다양한 에러 조건과 기이한 상황을 만들어내기 쉽다.

의존성 주입은 코드 재사용을 적극적으로 유도한다. 의존성을 품은, 하드 코딩한 모듈은 보통 재사용하기 어렵다. 초기 Attendee 모듈도 Messenger를 하드 코딩하여 쓴 탓에 서버 측에서 재사용할 수 없었다. 의존성 주입으로 바꾼 다음에는 성공/실패 메서드만 있으면 어떤 messenger 라도 사용할 수 있다.

의존성 주입의 모든 것

의존성 주입은 어렵지 않다. 몇 가지 개념만 기억하면 잘 활용할 수 있다.
어떤 객체를 코딩하든 어떤 객체를 생성하든 스스로 다음 질문을 해봤을때 한 가지라도 답변이 ‘예’ 라면 직접 인스턴스화 하지 말고 주입하는 방향으로 생각을 전환해야 한다.

  • 객체 또는 의존성 중 어느 하나라도 DB, 설정 파일, HTTP, 기타 인프라 등의 외부 자원에 의존 하는가
  • 객체 내부에서 발생할지 모를 에러를 테스트에서 고려해야 하나
  • 특정한 방향으로 객체를 작동시켜야 할 테스트가 있는가
  • 서드파티 제공 객체가 아니라 온전히 내가 소유한 객체인가

경량급 의존성 주입 프레임워크 개발

지금까지는 의존성 주입을 하드 코딩했다. 전문가다운 의존성 주입 프레임워크는 이렇게 작동한다.

  1. 애플리케이션이 시작되자마자 각 인젝터블(주입 가능한 모든 의존성을 집합적으로 일컫는 말) 명을 확인하고 의존성을 지칭하며 순서대로 DI 컨테이너에 등록한다.
  2. 객체가 필요하면 컨테이너에 요청한다.
  3. 컨테이너는 일단 요청받은 객체와 그 의존성을 모두 재귀적으로 인스턴스화한다. 그리고 요건에 따라 필요한 객체에 각각 주입한다.

컨테이너는 인젝터블과 의존성을 등록하고 요청시 객체를 내어주는 두 가지 일을 한다. register 함수의 인자는 세 가지를 받는다.

  • 인젝터블 명
  • 의존성 명을 담은 배열
  • 인젝터블 객체를 반환하는 함수. 인젝터블 인스턴스를 요청하면 컨테이너는 이 함수를 호출하여 반환 값을 다시 그대로 반환한다.

TDD는 단계마다 가급적 조금씩 코딩하는게 좋다. 먼저 빈 DiContainer와 register 함수를 생성한다.

1
2
3
4
5
6
7
DiContainer = function() {

};

DiContainer.prototype.register = function(name, dependencies, func) {

};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
describe('DiContainer', function () {
var container;
beforeEach(function () {
container = new DiContainer();
});

describe('register(name,dependencies,func)', function () {
it('인자가 하나라도 빠졌거나 타입이 잘못되면 예외를 던진다.', function () {
var badArgs = [
// 인자가 아예 없는 경우
[],
['Name'],
['Name', ['Dependency1', 'Dependency2']],
['Name', function () { }],
// 타입이 잘못된 경우
[1, ['a', 'b'], function () { }],
['Name', [1, 2], function () { }],
['Name', ['a', 'b'], 'should be a function']
];

badArgs.forEach(function (args) {
expect(function () {
container.register.apply(container, args);
}).toThrowError(container.messages.registerRequiresArgs);
});
})
});
})
  • container는 ‘테스트 대상’으로 beforeEach에서 생성된다. 테스트마다 인스턴스를 갓 구워내면 다른 테스트의 결과를 어지럽히지 않아도 된다.
  • TDD 순수주의자는 badArgs 원소마다 테스트를 따로 만들라고 하겠지만, 실제로 그렇게까지 개발자에게 부담을 주면 필요한 조건을 모두 테스트하기도 전에 질려버릴지도 모른다.

위의 테스트는 당연히 실패한다. DiContainer에 수정이 필요하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
DiContainer = function () {
if (!(this instanceof DiContainer)) {
return new DiContainer();
}
};

DiContainer.prototype.messages = {
registerRequiresArgs: '이 생성자 함수는 인자가 3개 있어야 합니다. 문자열, 문자열 배열, 함수'
}

DiContainer.prototype.register = function (name, dependencies, func) {
var ix;

if (
typeof name !== 'string' ||
!Array.isArray(dependencies) ||
typeof func !== 'function'
) {
throw new Error(this.messages.registerRequiresArgs);
}

for (ix = 0; ix < dependencies.length; ++ix) {
if (typeof dependencies[ix] !== 'string') {
throw new Error(this.messages.registerRequiresArgs);
}
}
};

register 함수는 여전히 아무 일도 하지 않지만, 이 함수만으로는 의존성을 다시 끌어낼 방법이 없으므로 컨테이너에 의존성이 잘 들어갔는지 테스트하기 어렵다. 따라서 자연스레 나머지 반쪽 그림에 해당하는 get 함수에 관심이 쏠린다. 이 함수의 유일한 인자는 조회할 의존성 명이다.

1
2
3
4
5
6
7
8
9
DiContainer.prototype.get = function (name) {

}

describe('get(name)', function () {
it('성명이 등록되어 있지 않으면 undefined를 반환한다.', function () {
expect(container.get('notDefined')).toBeUndefined();
})
})

이제 get 함수 작성과 DiContainer 를 수정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
DiContainer = function () {
...
this.registrations = [];
};

DiContainer.prototype.register = function (name, dependencies, func) {
...
this.registrations[name] = { func: func };
};

DiContainer.prototype.get = function (name) {
var registration = this.registrations[name];
if (registration === undefined) {
return undefined;
}
return registration.func();
}

it('등록된 함수를 실행한 결과를 반환한다.', function () {
var name = 'MyName',
returnFromRegisteredFunction = "something";

container.register(name, [], function () {
return returnFromRegisteredFunction;
});

expect(container.get(name)).toBe(returnFromRegisteredFunction);
})

이제 get은 자신이 반환하는 객체에 의존성을 제공할 수 있다. 아래 코드는 1개의 메인 객체와 2개의 의존성을 등록하는 테스트로, 메인 객체는 두 의존성의 반환값을 합한 값을 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
DiContainer.prototype.register = function (name, dependencies, func) {
...
this.registrations[name] = { dependencies: dependencies, func: func };
};

DiContainer.prototype.get = function (name) {
var self = this,
registration = this.registrations[name],
dependencies = [];

if (registration === undefined) {
return undefined;
}

registration.dependencies.forEach(function (dependencyName) {
var dependency = self.get(dependencyName);
dependencies.push(dependency === undefined ? undefined : dependency);
});

return registration.func.apply(undefined, dependencies);
}

it('등록된 함수에 의존성을 제공한다.', function () {
var main = 'main',
mainFunc,
dep1 = 'dep1',
dep2 = 'dep2';

container.register(main, [dep1, dep2], function (dep1Func, dep2Func) {
return function () {
return dep1Func() + dep2Func();
};
});

container.register(dep1, [], function () {
return function () {
return 1;
}
});

container.register(dep2, [], function () {
return function () {
return 2;
}
});

mainFunc = container.get(main);
expect(mainFunc()).toBe(3);
});

참조: 자바스크립트 패턴과 테스트
github

댓글

You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.