스프링 프레임워크 따라해보기 - 멀티 모듈과 빈 생성 및 등록

    멀티 모듈로 변경한 이유


    먼저 다른 기능들을 구현해보기에 앞서 멀티모듈로 변경한 이유부터 말해보려 한다.

    기존 프로젝트의 경우에는 한 개의 src 파일 아래로

    src
    | - framework
    | - project

    이런식으로 2개의 패키지를 구성해서 framework 패키지 아래로는 스프링을 따라해본 구현체를 사용하고, project 패키지에는 우리가 흔히 웹 서비스를 만드는 것처럼 controller, service, repository를 사용하려고 했다.

     

    그런데, 곰곰히 생각을 해보니 결국 framework 패키지는 외부에서 주입받아 사용되어야 하기 때문에 이를 1모듈 2패키지가 아니라, 2모듈 2패키지로 진행하는 것이 좋을 것 같다는 생각을 해서 분리를 하게 되었다.

     

    또한, 앞선 글에서는 basepackage를 내가 사용하는 package를 그대로 손수 타이핑을 했지만 실제로는 Main.class를 인자로 받아 이에 해당하는 package명을 동적으로 체크하는 것이 여러 방면에서 도움이 될 것이라 생각했다.

     

    IoC 컨테이너


    앞선 글에서는, init을 할 때에 클래스들을 찾고, 또한 컨트롤러를 실행해야 하는 순간에 @Controller가 붙어있는 클래스를 찾는 식으로 그 순간 순간마다 클래스 객체를 가져와서 진행을 하였다.

    하지만, 스프링 프레임워크에서는 IoC 컨테이너라는 것을 이용해서 빈(Bean)을 관리한다.

    빈(Bean)이란, 스프링 IoC 컨테이너에 의해 관리되는 자바 객체를 의미한다고 볼 수 있다. 우리가 흔히 @RestController, @Service, @Repository 등의 어노테이션을 자주 사용하게 되는데 실제 어노테이션의 안쪽으로 계속 들어가다 보면 @Component라는 어노테이션이 붙어있는 것을 확인할 수 있다.

    @ResController의 메타 어노테이션인 @Contoller 어노테이션을 확인해보면 

    @Component 어노테이션이 붙어있는 것을 확인할 수 있다.

     

    또한 IoC 컨테이너의 역할은 의존성 주입(DI, Dependency Injection)을 통하여 애플리케이션을 구성하는 빈(Bean)들의 생명주기를 개발자 대신 관리해주는 것이다.

    따라서, 흐름을 생각해보자면 초기에 스프링 프레임워크가 설정을 진행할 때에 IoC 컨테이너 라는 객체를 사용한다. 그런데 이 IoC 컨테이너는 빈(Bean)이라는 형태로 객체들을 관리하는데, 패키지 내의 클래스 파일들을 분석해 @Component가 있는 파일을 빈(Bean)으로 등록하여 생명주기를 관리한다.

     

    와 같이 생각할 수 있다. 그런데 실제로 객체를 생성함에 있어 어떻게 관리되는 것이 효율적일까?

    우리는 객체를 생성하는 2가지 방법을 알고 있다.

    • new 연산자를 이용한 동적 할당
    • static을 이용한 정적 할당

    일반적으로는 객체지향 프로그래밍을 하며 new 연산자를 이용한 동적 할당을 많이 사용한다. 그런데, 지금 우리가 만들고자 하는 부분은 초기 프레임워크 설정 부분이며, 굳이 여러 번의 설정을 진행할 필요가 없다.

    따라서 서버 실행 초기 부분에 "단 한번"만 실행되면 되고 IoC 컨테이너가 여러 개일 필요가 없기 때문에 static 을 이용한 정적 할당을 하여 인스턴스의 사용성을 높이고, 싱글톤 패턴을 이용해서 1개의 인스턴스만 보장되게 하면 좋을 것이라 생각한다.

    싱글톤 패턴이란, 객체의 인스턴스를 한 개만 생성되게 하는 패턴이다. 주로, 애플리케이션 내에서 하나의 객체만 존재해야 하며 내부적으로 여러 부분에서 이 객체를 공유해서 사용할 때 주로 사용된다.


    싱글톤 패턴을 사용하게 되면, 여러 개의 객체를 사용하지 않아 메모리와 속도적인 측면에서 이점이 있으며 데이터를 애플리케이션 내부적으로 공유하기 쉬워지기 때문에 초기 설정 이라는 측면에서 이점이 된다.

    또한, 스프링 프레임워크는 각 빈들의 생성자를 미리 생성하여 저장해두고 추후 사용될 때에는 그 빈 1개만을 사용하기 때문에 효율적이라고 볼 수 있다.

    예를 들어 다음과 같은 상황을 가정했을 때

    HelloController을 사용하기 위해서는 HelloService라는 객체가 필요하다.

    이 때, HelloService를  미리 생성해둔 후, HelloController를 생성할 때 필요한 매개변수인 HelloService를 생성해둔 객체를 가져다가 사용하면 굳이 2개의 HelloService가 필요하지 않기 때문에 효율적이라고 생각된다.

     

    그럼 이제 실제로 구현을 진행해보자.

    먼저, static을 이용해서 정적 할당을 해주고, 외부에서 참조 시 getInstance() 메소드를 이용해서 가져다 쓸 수 있게 생성해주었다.

     

    그리고 MiniSpringApplication 에서 순서 등을 조금 변경하였다.

    기존 프레임워크 모듈에서 설정을 init한 후에, 이를 의존하는 참조 모듈의 패키지를 스캔하여 빈들을 생성했다. 이 때, 패키지명은 참조하는 모듈에 따라 동적으로 변경될 수 있도록 source(ex. Main.class) class의 패키지 이름을 가져와 빈을 생성하도록 진행했다.

     

    setUpBean 메소드는 위와 같이 구성하였는데, 원래 기존에 디렉토리를 탐색하고, .class파일을 찾고, 그 중 어노테이션이 붙은 파일을 찾는 과정을 컨테이너의 내부 메소드로 지정했었다. 하지만, 이렇게 하다 보니 좋은 설계가 아닌 것 같아 Bean 등록, 관리 등을 따로 하는 BeanFactory라는 클래스를 생성하였다.

     

    BeanFactory


    BeanFactory 또한 여러 개의 설정을 할 필요가 없기 때문에 싱글톤 패턴으로 만들어주었고, getter 등을 통해 이름 혹은 전체 빈을 가져올 수 있도록 해주었다.

    dependencyGraph와 inDegree 의 경우는 진행을 하며 왜 구성했는지 설명하겠다.

    먼저 이전에 디렉토리 등을 scan한 것과 같이, scanClasses에서는 패키지 내부의 클래스 파일을 스캔하여 List에 저장을 한다.

    그 후에 createAndRegisterBeans을 통해서 진행을 하게 되는데

    Component 어노테이션을 가지고 있으면서, Interface, Enum, Annotation 파일이 아닌 클래스를 빈으로 생성하게 된다.

     

    AnnocationScanner

    AnnotationScanner.hasAnnotation()의 경우에는 타겟 어노테이션이 존재하는 지 파악하는 역할을 한다.

    이 또한, 한 개의 클래스 내에서 진행을 하기에는 역할을 분리할 수 있을 것이라 생각했기 때문에 아래와 같이 구성해주었다.

    기존에 Componen.class을 찾는 방식에서 메타 어노테이션까지 파악하여 찾는 방식을 추가해주었다.

    이유는 다음과 같다.

    이러한 Controller 어노테이션은 Component 어노테이션을 메타 어노테이션으로 가지고 있다. 하지만 단순히 isAnnotationPresent()을 통해서는 메타 어노테이션을 파악할 수 없어, 컨트롤러가 등록되지 않는 현상이 있었다.

    이렇게 주석처리를 해놓고 결과를 확인해본다면

    모두 Component 어노테이션을 가지고 있지 않다고 파악이 된다. 하지만 이를 다음과 같이 변경해보았을 때는

    Controller와 Service 어노테이션이 붙은 클래스에서 Component 어노테이션을 확인하고 true를 체크하는 모습을 확인할 수 있다.

     

    그럼 다음으로 넘어가서 createBean 부분이다.

    CreateBean

    일단 구조 자체는 간단하다. 클래스의 생성자를 파악하고 매개변수가 있다면 bean에서 매개변수에 해당하는 객체를 가지고 와서 매개변수로 활용해 생성자로 만드는 것이다.

    하지만, 여기서 문제가 하나 발생했는데... 그것은 바로 현재 내 코드에는 생성자를 만드는 순서(?)가 존재하지 않는 것이다.

    위쪽의 HelloController의 경우 매개변수로 HelloService 객체를 가지고 있고, HelloService 객체는 매개변수가 존재하지 않는다.

    그럼 사람의 머리로는 HelloService를 만들고 그걸 이용해서 HelloController을 만들면 되는 것 아냐?

    라고 생각하겟지만, 컴퓨터는 난 아무것도몰라~ 와 같이 되서 다음 오류가 발생했다.

    HelloService가 없어서 HelloController을 만들지 못하겠다는 파업 선언이었다...

     

    결국 빈들의 생성 순서를 조절해주어야 겠다는 생각을 했고, 내가 선택한 방식은 매개변수가 적은 클래스부터 많은 클래스 순으로 생성을 하는 것이었다. 이렇게 하면, 모든 클래스에서 생성자를 받을 수 있기 때문에 괜찮지 않을까 라는 생각을 했다.

     

    정렬 방식을 통해 매개변수가 없는 생성자부터 시작하여 순서대로 클래스를 만들어갔고 현재의 간단한 구조에서는 통과를 할 수 있게 되었다.

    또한 bean들의 이름은 .getClass().getSimpleName() 을 통해 카멜 케이스로 맨 앞 부분만 소문자로 만들어주는 식으로 하였다.

    (HelloController -> helloController)

     

    코드 전역적으로 빈의 이름을 가져올 때는 이 방식을 선택하도록 진행했다.

     

    DispatcherServlet 변경점


    기존에는 DispatcherServlet에서 디렉토리를 탐색하고 class 파일들을 탐색하였지만 이 부분을 BeanFactory로 넘기게 되었다.

    따라서, controller들을 보관하는 Map을 구성할 때에

    이런식으로 BeanFactory의 bean에서 해당 객체를 가져올 수 있도록 변경했다.

    메소드의 매개변수들을 가져와 이를 BeanFactory를 통해서 가져올 수 있도록 했고, invoke 메소드에 가변인자로서 params를 전달하게 하였다. 이 부분도 나중에 하나의 객체 혹은 함수로 변경할 수 있으면 좋을 것 같다.(글을 작성한 이후 변경 예정)

    기존에 이런식으로 매개변수가 있다면 invoke() 메소드에 controller밖에 없었기 때문에

    500 Internal Error 가 발생했지만, params를 넣은 후에는 매개변수가 존재하더라도 정상적으로 웹 출력이 되는 것 을 확인할 수 있었다.

     

    글을 작성하며 생각해본 변경할 점

    구현을 진행할 당시에는, 매개변수의 수에 따라서 bean을 투입하면 될 것이라 생각했는데 다시 생각해보니 이는 틀린 방법인 것 같다.

    만약에 이러한 방식으로 클래스가 존재한다면, 현재 내 방식에서 생성 순서는 Controller -> UtilService -> Service가 될 것이지만 실질적으로 UtilService의 매개변수는 Servcie이기 때문에 에러가 발생할 것이다.

     

    지금으로서 드는 생각은 매개변수를 위해 beanMap에서 빈을 찾을 때, 존재하지 않는다면 이에 대한 생성자를 생성하는 식으로 재귀적으로 들어가는 것이 나을지에 대한 고민을 하며 이번 포스팅을 마친다.

     

    또한 간단하게나마 IoC 컨테이너를 구현해보며 IoC 컨테이너의 역할에 대해서 조금 더 자세히 알게 된 것 같았다.

    • 제어의 역전
      • 실제로 객체를 만들고 bean을 등록하는 과정을 통해서, 개발자들이 따로 객체를 생성하지 않아도 되어 유지보수적인 측면에서 훨씬 편해지고 개발자의 부담을 줄일 수 있다는 것을 파악했다.
    • 라이프사이클 관리
      • PostContructor, PreDestroy 등 현재 코드에서는 구현되어 있지 않지만, 어째서 이 어노테이션을 사용하면 객체가 생성, 파괴되는 과정에서 메소드가 실행될 수 있는지 파악할 수 있었다.
    • 테스트 코드 작성 시 Mocking
      • 테스트 코드 작성을 할 때, 우리는 when().then() 등을 통해서 객체가 return 하고자 하는 값을 임의로 지정해줄 수 있었다. 이러한 방식이 어떻게 가능한 지에 대해 궁금증이 있었는데 빈을 생성하는 과정에서 이를 처리하여 Mock 객체를 담을 수 있다면 테스트 시에 이러한 Mock 객체를 사용할 수 있을 것이라는 생각을 했다.

    댓글