7장 의존성 주입 (2/2) - 간단한 Javascript DI 컨테이너 구현체

이 글은 7장 의존성 주입 (1/2)에서 설명한 DI 컨테이너의 간단한 구현체를 제시한다. Javascript이기 때문에 타입 정보를 얻을 수 없어 String으로 의존성을 판단하는 부분을 참고하기 바란다.


이 글의 코드는 출처에서 배포된 코드를 가져왔음을 밝힌다.


1. DI 컨테이너 구현

diContainer.js

아쉽게도 패키지 전체를 미리 스캔하여 자동으로 의존 관계를 파악하고 의존성 주입을 수행하지는 않는다. 기능은 크게 get, factory, register가 있다. 자세한 설명은 주석을 참고하라.

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
50
51
52
53
54
"use strict";

// fnArgs는 함수의 인자 목록을 String 배열로 반환한다.
const fnArgs = require("parse-fn-args");

module.exports = () => {
const dependencies = {};
const factories = {};
const diContainer = {};

// factory, register 둘 다 단순 등록 기능이다.
// factory 메소드의 경우 의존성 주입이 필요한 객체인 경우 사용한다.
diContainer.factory = (name, factory) => {
factories[name] = factory;
};

// register 메소드의 경우 의존성 주입이 필요 없는 객체(상수 등)를 등록할 때 사용한다.
diContainer.register = (name, dep) => {
dependencies[name] = dep;
};

/*
1. get은 dependencies에 없는 경우 factory로 간주하고 가져옴
2. 만약 가져오려 했던 객체가 존재하면 해당 객체로 inject를 호출함 (inject를 통해 재귀적으로 의존성을 resolve.)
3. (2)의 결과를 dependencies에 저장
4. 만약 그래도 dependencies에 없는 경우 모듈을 찾을 수 없는 것.
*/
diContainer.get = (name) => {
if (!dependencies[name]) {
const factory = factories[name];
dependencies[name] = factory && diContainer.inject(factory);
if (!dependencies[name]) {
throw new Error("Cannot find module: " + name);
}
}
return dependencies[name];
};

/*
1. factory로 등록된 객체를 전달받음
2. fnArgs는 함수(factory의 경우, 의존성을 명시한 함수를 export 함.)의 인자를 가져옴
3. 인자에 대해 map으로 get을 수행한 배열을 args 변수에 저장함
4. factory(생성자)를 resolved 된 dependencies로 호출함
*/
diContainer.inject = (factory) => {
const args = fnArgs(factory).map(function (dependency) {
return diContainer.get(dependency);
});
return factory.apply(null, args);
};

return diContainer;
};


2. 컨테이너 사용

1. app.js

DI 컨테이너에 각 객체를 등록하는 과정을 이 파일을 진입점 삼아 수행하였다. 좀 더 좋은 DI 컨테이너라면 Reflection 등을 이용해 자동으로 mark된 객체를 등록하고 의존성 주입을 진행할 것이다.

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
"use strict";

//...

const diContainer = require("./lib/diContainer")();

// register는 추가적으로 의존성 주입이 필요 없는 객체를 등록한다. (상수 등)
diContainer.register("dbName", "example-db");
diContainer.register("tokenSecret", "SHHH!");

// factory는 의존성 주입이 필요한 객체를 등록한다.
diContainer.factory("db", require("./lib/db"));
// Service 객체 등록 (의존성 주입 필요한 상태)
diContainer.factory("authService", require("./lib/authService"));
// Controller 객체 등록 (의존성 주입 필요한 상태)
diContainer.factory("authController", require("./lib/authController"));

// get은 의존성을 반환한다. (재귀적으로 의존성 주입이 된 채로 반환된다.)
const authController = diContainer.get("authController");

// Express에 Controller 등록
app.post("/login", authController.login);
app.get("/checkToken", authController.checkToken);

//...

2. authController.js

의존성 주입이 적용되는 객체 1이다. 주석 참고.

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
"use strict";

// Express에서 일반적으로 사용되는 Controller 예제이다.
// 모듈 차원에서 함수로 내보내며(DI 컨테이너 작동 방식에 맞춤), 인자에 이름으로 의존성을 명시한다.
module.exports = (authService) => { // DI 컨테이너에 의해 authService 의존성을 주입 받게 된다.
const authController = {};

authController.login = (req, res, next) => {
authService.login(req.body.username, req.body.password,
(err, result) => {
if (err) {
return res.status(401).send({
ok: false,
error: 'Invalid username/password'
});
}
res.status(200).send({ok: true, token: result});
}
);
};

authController.checkToken = (req, res, next) => {
authService.checkToken(req.query.token,
(err, result) => {
if (err) {
return res.status(401).send({
ok: false,
error: 'Token is invalid or expired'
});
}
res.status(200).send({ok: 'true', user: result});
}
);
};

return authController;
};

3. appService.js

의존성 주입이 적용되는 객체 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
"use strict";

const jwt = require('jwt-simple');
const bcrypt = require('bcrypt');

// 역시 모듈을 함수로 내보내며 의존성을 명시했다.
// 이 예제에서는 db 객체에 대해선 생략하였다. (DI 설명에서 의미 없는 구성 요소)
module.exports = (db, tokenSecret) => {
const users = db.(...);
const authService = {};

authService.login = (username, password, callback) => {
users.get(username, (err, user) => {
if (err) return callback(err);

bcrypt.compare(password, user.hash, (err, res) => {
if (err) return callback(err);
if (!res) return callback(new Error('Invalid password'));

const token = jwt.encode({
username: username,
expire: Date.now() + (1000 * 60 * 60) //1 hour
}, tokenSecret);

callback(null, token);
});
});
};

authService.checkToken = (token, callback) => {
let userData;
try {
userData = jwt.decode(token, tokenSecret);
if (userData.expire <= Date.now()) {
throw new Error('Token expired');
}
} catch(err) {
return process.nextTick(callback.bind(null, err));
}

users.get(userData.username, (err, user) => {
if (err) return callback(err);
callback(null, {username: userData.username});
});
};

return authService;
};

TODO:

Node.js 스트림 이어서 포스팅하기

7장 의존성 주입 (2/2) - 간단한 Javascript DI 컨테이너 구현체

https://jsqna.com/ndp-7-dependency-injection-2/

Author

Seongbin Kim

Posted on

21-03-03

Updated on

21-03-03

Licensed under

댓글