ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [AngularJS] 8. 의존관계 주입과 서비스 - AngularJS 강좌
    Web/AngularJS 2016. 4. 8. 11:31

    의존관계 주입과 서비스

     위 개념은 AngularJS의 중요한 개념 중 하나입니다. 의존관계 주입은 AJS 기반의 애플리케이션을 개발하는 데 있어서 항상 있기 마련이고 서비스가 없는 AJS 웹 앱이란 상상하기 어렵습니다. 


     1. AJS에서의 의존관계 주입

      하나의 객체가 다른 객체를 사용하는 순간 의존관계가 성립됩니다. AJS 개발자 문서에는 자바스크립트 상에서 객체들 사이의 의존관계가 크게 3 가지 경우에 생성된다고 합니다.

      

      - new 키워드

      - 전역변수 참조

      - 인자를 통하여 참조를 전달



      처음 1번과 2번은 의존관계가 강하게 연결되었고 3번은 느슨하게 연결되었다고 말할 수 있습니다. 아래 예시를 통해 확인해봅시다.



    1
    2
    3
    4
    function demoCtrl() {
        var bookmark = new BookmarkRes(new Ajax(), new JsonParser());
    }
     
    cs




      위 데모 컨트롤러 함수는 BookmarkRes 함수를 사용하고 있습니다. 하지만 데모 컨트롤러 함수는 BookmarkRes 함수가 어떻게 bookmark 객체를 생성해야 하는지 정확히 알고 있어야 한다는 문제점이 있습니다. 즉, Ajax와 JsonParser 인자까지 알고 있어야 한다는 점입니다. 다음은 전역변수 참조를 통해 개선한 코드입니다.



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var factory = {
        getBookmarkRes: function() {
            return new BookmarkRes(factory.getAjax(), factory.getJsonParser());
        },
        getAjax: function() { return new Ajax(); },
        getJsonParser: function() { return new JsonParser(); }
    }
     
    function demoCtrl() {
        var bookmark = factory.getBookmarkRes();
    }
    cs




      전역변수 영역에 BookmarkRes 가 객체를 생성하는 역할을 담당하는 팩토리 객체를 선언하고 BookmarkRes는 팩토리의 getAjax(), getJsonParser() 함수를 호출하여 필요한 객체를 얻을 수 있게 되었습니다. 위 코드에서의 문제점은 각각의 기능에 대해서 단위 테스트 하기가 어렵다는 점입니다. 그러면 마지막으로 인자 참조를 통한 예제를 보도록 하겠습니다.



    1
    2
    3
    function demoCtrl (BookmarkRes) {
        var bookmark = BookmarkRes.get();
    }
    cs




      위 코드는 데모 컨트롤러가 BookmarkRes를 인자로 전달받아 의존관계가 성립된 것을 볼 수 있습니다. 데모 컨트롤러는 BookmarkRes가 어떻게 생성되는지 알 필요도 없고 단위 테스트에서도 얼마든지 필요한 테스트용 BookmarkRes를 주입받을 수 있게 되는 것입니다. 


      이처럼 AJS에서 주입되는 대상을 서비스라 하여 BookmarkRes를 서비스로서 개발하고 이를 컨트롤러나 다른 서비스 혹은 지시자 등에 주입되는 방식인 DI를 이용해 컴포넌트 별 의존 관계를 정의 할 수 있습니다.




      Module.factory를 이용한 Hello 서비스 만들기

       그러면 AJS의 서비스를 정의하는 방법을 간단한 예제를 통해서 알아보도록 합시다. 서비스를 만들려면 모듈 인스턴스가 필요합니다. 


     * ajsfactory.html


    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
    <!doctype html>
    <html ng-app="factoryExam">
        <head>
            <meta charset="UTF-8"/>
            <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
            <script>
                angular.module('factoryExam', []).
                    factory('hello', [function() {
                        var helloText = "님 안녕하세요.";
     
                        return {
                            say: function(name) {
                                return name + helloText;
                            }
                        };
                    }]).
                    controller('mainCtrl'function($scope, hello) {
                        $scope.hello = hello.say("Pal");
                    });
            </script>
        </head>
        <body>
            <div ng-controller="mainCtrl">
                <p>{{hello}}</p>
            </div>
        </body>
    </html>
    cs






       위 예제 코드를 보면 angular.module() 함수를 이용해 factoryExam 모듈을 선언하고 해당 인스턴스의 factory() 메서드를 호출해 hello 서비스를 만들었습니다. factory() 함수의 첫 번째 파라미터로 서비스 명을 주고 다음 인자로 서비스를 주입받을 때 반환할 객체를 설정하는 팩토리 함수를 줍니다. 이렇게 만든 hello 서비스는 컨트롤러에서 인자로 주입해 사용할 수 있습니다.





    [출처: AngularJS]



       위 예제에서 hello 서비스를 angular.module('factoryExam', []).factory() 메서드를 이용해서 정의했습니다. 사실 모듈 API의 factory() 메서드는 $provide.factory() 메서드와 같습니다. 이렇게 정의된 서비스는 $injector로 얻어오고 각 서비스의 의존 관계도 $injector에 의해서 생성되게 됩니다. 서비스를 특정 함수에게 주입하거나 등록된 서비스를 얻어오는 역할을 $injector가 하는 것입니다. $injector는 서비스의 싱글톤을 유지하기 위해 내부적으로 캐시를 가지고 있어 서비스가 매번 새로운 객체를 생성하는 것을 방지합니다. 그럼 $provide와 $injector에 대해 자세히 보도록 합시다.



      $provide를 이용한 Provider 정의

       AJS에서 $provide를 이용해 주입할 수 있는 서비스를 제공해 주는 프로바이더를 정의할 수 있습니다. 이 서비스 프로바이더가 특정 서비스를 제공해 주는 것입니다. 그래서 서비스를 단순하게 생각하면 $provide로 정의한 프로바이더가 생성하는 객체라고 할 수 있습니다. 이런 서비스를 만드는 방법은 크게 5 가지 방법입니다.


       - value로 정의하는 방법

        웹 앱을 개발하다 보면 앱 전역에서 사용하는 특별한 값이 필요한 경우가 있습니다. 가령 앱 이름이나 컨텍스트 루트가 그러할 것입니다. 이러한 값을 반환하는 서비스를 만들어 봅시다.


     * ajsvalue.html


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!doctype html>
    <html ng-app="valueExam">
        <head>
            <meta charset="UTF-8"/>
            <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
            <script>
                angular.module('valueExam', []).
                    value('AppName''Value Exam').
                    controller('mainCtrl'function($scope, AppName) {
                        $scope.AppName = AppName;
                    });
            </script>
        </head>
        <body ng-controller="mainCtrl">
            <p> 어플리케이션 이름 : {{AppName}} </p>
        </body>
    </html>
    cs


       





      valueExam이란 모듈을 만들고 해당 모듈의 value 메서드를 이용해 AppName 서비슬르 만든 것을 볼 수 있습니다. 






       - Factory로 정의하는 방법

       실제로 서비스를 만들게 되면 거의 factory를 이용해 만들게 됩니다. factory는 서비스를 생성하는 로직을 담는 함수입니다. 기본적인 factory의 정의는 아래와 같습니다.


    1
    module.factory('서비스이름'function([주입받을 서비스]) { ... } );
    cs



        모듈 API를 이용해 $provider.factory 메서드를 사용하는 데 value와 다르게 두 번째 인자를 함수로 전달해 줍니다. 해당 함수는 팩토리 함수로서 인자로는 주입받을 다른 서비스를 줄 수 있습니다. 다음 예제는 주어진 이름과 이메일을 추가하고 하단에 목록으로 보여주는 예제입니다.


     * ajsfactory2.html


    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
    <!doctype html>
    <html ng-app="factoryExam">
        <head>
            <meta charset="UTF-8"/>
            <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
            <script>
                angular.module('factoryExam', []).
                    factory('AppNm', [function () {
                        return 'demo App';
                    }]).
                    factory('UserRes'function() {
                        var userList = [];
     
                        return {
                            addUser: function(newUser) {
                                userList.push(newUser);
                            },
                            updateUser: function(idx, updatedUser) {
                                userList[idx] = updatedUser;
                            },
                            deleteUser: function(idx) {
                                userList[idx] = undefined;
                            },
                            selectUsers: function() {
                                return userList;
                            }
                        }
                    }).
                    controller('mainCtrl'function($scope, AppNm, UserRes) {
                        $scope.appNm = AppNm;
     
                        $scope.users = UserRes.selectUsers();
     
                        $scope.addNewUser = function(newUser) {
                            UserRes.addUser({
                                name: newUser.name,
                                email: newUser.email
                            });
                        };
                    });
            </script>
        </head>
        <body ng-controller="mainCtrl">
            <p> 어플리케이션 이름: {{appNm}} </p>
            <div>
                이름: <input type="text" ng-model="newUser.name">
                이메일: <input type="text" ng-model="newUser.email">
                <button ng-click="addNewUser(newUser)">Add new user</button>
            </div>
            <ul>
                <li ng-repeat="user in users">{{user.name}}, {{user.email}}</li>
            </ul>
        </body>
    </html>
    cs








      위 예제를 보면 UserRes 서비스를 factory 메서드로 정의하고 있고 addUser, updateUser, deleteUser, selectUsers 메서드를 가지는 객체를 반환하고 있습니다. 또한 UserRes의 userList는 메서드로만 접근 가능합니다. UserRes를 여러 컨트롤러에서 주입받을 수 있지만, UserRes는 싱글톤이므로 반환되는 객체의 메서드들은 모두 같은 userList 배열을 참조하게 됩니다.



       - Service로 정의하는 방법

        JavaScript 특성상 함수를 클래스로 사용하면서 상속과 같은 OOP 특성을 이용해 개발할 수 있습니다. Service 메서드는 이러한 생성자 함수를 객체화할 때 사용합니다. 


        일반적으로 new 키워드를 이용해 인스턴스를 얻게 됩니다. 이러한 new 키워드를 이용한 인스턴스화와 $provide.service 메서드와의 차이점은 매번 다른 새로운 객체를 반환하는 것이 아니라 싱글톤을 유지하여 같은 객체를 반환한다는 것입니다. 그럼 이제 예제를 통해 살펴보도록 합시다.


     * ajsservice.html


    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
    <!doctype html>
    <html ng-app="serviceExam">
        <head>
            <meta charset="UTF-8"/>
            <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
            <script>
            function Calculator() {
                this.lastValue = 0;
     
                this.add = function(a, b) {
                    var returnV = a + b;
                    this.lastValue = returnV;
     
                    return returnV;
                };
     
                this.sub = function(a, b) {
                    var returnV = a - b;
                    this.lastValue = returnV;
     
                    return returnV;
                };
            }
     
                angular.module('serviceExam', []).
                    factory('Calculate', [function() {
                        return new Calculator();
                    }]).
                    service('CalculateS', Calculator).
                    controller('mainCtrl'function($scope, Calculate, CalculateS) {
                        $scope.val1 = Calculate.add(154);
                        console.log(Calculate.lastValue);
                        $scope.val2 = CalculateS.sub(154);
                        console.log(CalculateS.lastValue);
                    });
            </script>
        </head>
        <body ng-controller="mainCtrl">
            <P> 15 + 4 = {{val1}} </P>
            <p> 15 - 4 = {{val2}} </p>
        </body>
    </html>
    cs






       위 예제는 factory와 service를 통해 작성된 예제입니다. 위 예제에서 service 쪽 코드를 보면 단순히 생성자 함수 레퍼런스만 전달하여 사용하는 것을 볼 수 있습니다. factory 메서드에 비해서 간단하게 작성이 됩니다. 





       - Provider로 정의하는 방법

        지금까지 $provide의 value, factory, service 메서드를 살펴봤습니다. 위 메서드들은 모두 provider 메서드를 사용해 의미상 alias처럼 제공되는 메서드들입니다. 실제로 서비스를 등록할 때 AJS는 $provide.provider만을 인지하게 됩니다.



     * ajsprovider.html


    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
    <!doctype html>
    <html ng-app="providerExam">
        <head>
            <meta charset="UTF-8"/>
            <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
            <script>
                angular.module('providerExam', []).
                    provider('Logger', [function() {
                        function Logger(msg) {
                            if (checkNativeLogger) console.log(msg);
                        }
     
                        Logger.debug = function(msg) { if (checkNativeLogger) console.debug(msg); };
                        Logger.info = function(msg) { if (checkNativeLogger) console.info(msg); };
     
                        function checkNativeLogger() {
                            if (consolereturn true;
                            return false;
                        }
     
                        this.$get = [function() {
                            return Logger;
                        }];
                    }]).
                    controller('mainCtrl'function($scope, Logger) {
                        Logger("console.log에서 출력되는 로그 메시지");
                        Logger.debug("console.debug에서 출력되는 로그 메시지");
                    });
            </script>
        </head>
        <body ng-controller="mainCtrl">
        </body>
    </html>
    cs






       별도의 Logger 서비스를 provider 메서드로 정의하고 있습니다. provider 메서드는 앞에서 본 factory, service와 마찬가지로 첫 번째 인자를 서비스 명을 주고 다음으로 함수를 정의합니다. 이 provider 함수는 꼭 this.$get = [function() {...}]를 포함하고 있어야 합니다. this.$get에는 배열 또는 함수를 줄 수 있는데 배열은 다음과 같이 다른 서비스를 주입할 때 사용합니다.



    1
    this.$get = ['$window'function(win) { win.console.log("..."); }];
    cs



       실제로 factory와 service가 provider를 어떻게 감싸고 있는지 살펴보면 factory는 다음 코드와 같습니다.


    1
    2
    3
    4
    5
    6
    7
    function factory(name, factory) {
        $provide.provider(namefunction() {
            this.$get = function($injector) {
                return $injector.invoke(factory);
            };
        });
    }
    cs



       factory 메서드는 결국 전달받은 팩토리 함수를 실행하고 그 결과를 반환하는 것입니다. 다음으로 service 메서드는 다음코드와 같습니다.


    1
    2
    3
    4
    5
    6
    7
    function service(name, Class) {
        $provide.provider(namefunction() {
            this.$get = function($injector) {
                return $injector.instantiate(Class);
            };
        });
    }
    cs



       service 메서드는 전달받은 생성자 함수를 인스턴스화하고 그 결과인 인스턴스 객체를 반환합니다. 


       다음으로 방금 정의한 서비스 프로바이더 자체를 주입받아 서비스를 주입하기 전에 설정하는 방법에 대해 살펴보도록 합시다.



       - 서비스 프로바이더 설정하기

       사용자 정의 서비스는 대부분 factory나 service 메서드로 정의하는데 특별한 경우에는 provider 메서드를 이용해야만 합니다. 바로 서비스를 주입하기 전에 별도의 설정을 해야할 때가 그렇습니다. 


       이때 Module API의 config 함수를 사용할 수 있는데 config 함수를 사용하려면 서비스 프로바이더가 설정할 수 있게 만들어져 있어야 합니다. 위 예제의 Logger를 로깅 레벨을 설정할 수 있게 변경해 봅시다.



     * ajsprovider.html

    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
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
     
    <!doctype html>
    <html ng-app="providerExam">
        <head>
            <meta charset="UTF-8"/>
            <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
            <script>
                angular.module('providerExam', []).
                    provider('Logger', [function() {
                        var defaultLogLevel = 'log';
     
                        function Logger(msg) {
                            if (checkNativeLogger) {
                                if (defaultLogLevel === "debug") {
                                    console.debug(msg);
     
                                    return;
                                } 
                                if (defaultLogLevel === "info") {
                                    console.info(msg);
     
                                    return;
                                }
     
                                console.log(msg);
                            }
                        }
     
                        Logger.debug = function(msg) { if (checkNativeLogger) console.debug(msg); };
                        Logger.info = function(msg) { if (checkNativeLogger) console.info(msg); };
     
                        function checkNativeLogger() {
                            if (consolereturn true;
                            return false;
                        }
     
                        this.setDefaultLevel = function(level) {
                            switch(level) {
                                case 'debug':
                                    defaultLogLevel = 'debug';
                                    break;
     
                                case 'info':    
                                    defaultLogLevel = 'info';
                                    break;
     
                                default:
                                    defaultLogLevel = 'log';
                            }
                        };
     
                        this.$get = [function() {
                            return Logger;
                        }];
                    }]).
                    config(['LoggerProvider'function (LoggerProvider) {
                        LoggerProvider.setDefaultLevel('debug');
                    }]).
                    controller('mainCtrl'function($scope, Logger) {
                        Logger("console.log에서 출력되는 로그 메시지");
                        Logger.debug("console.debug에서 출력되는 로그 메시지");
                    });
            </script>
        </head>
        <body ng-controller="mainCtrl">
        </body>
    </html>
    cs








       Logger 서비스 프로바이더가 setDefaultLevel 메서드로 기본 로깅 레벨을 설정하는 것을 볼 수 있습니다. 



      지금까지 $provider를 이용해 AJS 기반의 웹 앱에 서비스를 제공하는 서비스 프로바이더를 정의하는 방법을 살펴보았습니다. 다음은 AJS에서 제공하는 주요 사항별 각 메서드의 차이점을 정리한 표입니다.







    [출처: AngularJS API Guide]




      $injector를 이용한 서비스 주입

       AJS의 $injector는 $provider를 통해 등록된 서비스 프로바이더를 이용해 서비스 인스턴스를 생성하는 역할을 합니다. 그리고 $injector는 AJS 앱이 생성될 때 자동으로 생성되며 하나의 AJS 앱은 하나의 $injector만 가지게 됩니다. 


       $injector를 이용하면 특정 서비스 객체를 얻을 수 있습니다. 또는 특정 서비스가 해당 모듈에 정의되어 있는지 확인할 수도 있습니다. 그러면 간단한 예제로 $injector를 이용해 서비스 객체를 얻거나 확인하는 방법을 알아봅시다.



     * ajsinjector.html


    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
    <!doctype html>
    <html ng-app="injectorExam">
        <head>
            <meta charset="UTF-8"/>
            <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
            <script>
                angular.module('injectorExam', []).
                    factory('Hi', [function() {
                        return {
                            hiTo: function(name) {
                                console.log('hi ' + name);
                            }
                        };
                    }]);
     
                    var injector = angular.injector(['ng''injectorExam']),
                        hasHi = injector.has('Hi'),
                        hiSvc = null;
     
                    if (hasHi) {
                        hiSvc = injector.get('Hi');
                        hiSvc.hiTo("Pal");
                    }
            </script>
        </head>
        <body>
        </body>
    </html>
    cs





       injectorExam 모듈을 선언한 후 angular.injector(['ng', 'injectorExam'])를 이용해 $injector 객체를 가지고 왔습니다. 그리고 $injector의 has 메서드로 주어진 서비스가 ng 모듈과 injectorExam 모듈에 정의되어 있는지 확인합니다. 


       has 메서드로 정의를 확인 후 get 메서드를 통해 Hi 서비스 객체를 얻어와 hiTo 메서드를 통해서 콘솔 출력을 했습니다. 




      의존 관계를 주입 받을 수 있는 곳

       지금까지 컨트롤러 함수나 지시자 설정 함수에서 마법과도 같이 정의된 서비스를 주입받을 수 있었습니다. 하지만 $injector를 살펴본 지금은 AJS 내부에서 이 $injector로 컨트롤러 함수나 설정 함수가 invoke 메서드나 instantiate 메서드에 의해 호출되는 것을 생각할 수 있습니다. 


       다음 목록은 AJS 내부에서 $injector를 이용해 주어진 함수들의 매개변수에 적절한 서비스가 주입되는 곳입니다. 즉, AJS에서 Di 가 가능한 함수라고 볼 수 있습니다.


       - 컨트롤러 정의 함수

       - 지시자 정의 함수

       - 필터 정의 함수

       - $provider 의 provider 정의 함수 내 this.$get에 연결된 함수

       - $provider 의 factory 정의 함수

       - $provider 의 service 정의 함수

       - AJS가 제공하는 서비스




     2. AJS가 제공하는 서비스

      AJS에서는 프레임워크 기능이나 유틸리티적인 기능으로 여러 내장 서비스를 제공합니다. 자세한 내용은 아래 링크를 통해서 확인하시길 바랍니다.


      https://docs.angularjs.org/api  





    end


    * 이 포스팅은 '시작하세요! AngularJS 프로그래밍'을 바탕으로 작성하였습니다.

    댓글

Designed by Tistory.