ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] Bean이 도대체 뭘까?
    JAVA/Spring Boot 2021. 8. 31. 15:23

     

    방학 동안 JAVA 공부를 조금씩 하면서 스프링 강의를 들으며 좀 깊게 공부하려고 해봤다.

    돌아보면 방학 때 뭐했지 싶긴 한데,, 또 막상 이것저것 건드린게 많긴 했다.

     

    최근들어 하반기 공고도 많이 올라오면서 다시 열심히 공부를 해야할 것 같았다.

    가고 싶은 곳은 많은데 한 번 들어가기가 쉽지많은 않구나 싶다. 기업마다 또 원하는 인재상이 조금씩 틀린것 같다.

     

    8월이 가기 전에 방학동안 한 것들을 토대로 정리를 해볼까 하다가 다음주에 또 면접이 있는 바람에 준비를 하는데,

    더 늦어지면 쭉 안 할 것같아서 조금씩 작성하기로 했다.

     

    7,8월 간 갓영한님의 Spring 핵심원리, Spring MVC ... Query Dsl 강의까지 한 번 쭉 들었다. 

    앞으로 개발을 하면서 모르는 내용은 다시 한 번 pdf 파일을 보던지, 그 부분 강의를 다시 보던지 할 계획이다.

     

    이번 포스팅은 Spring 핵심원리 강의 중 Bean에 관해 한 번 정리해 볼까 한다.

     

    목차 다음과 같이 설명할 예정이다.

     

    1. DI (Dependency Injection) 란?

     

    2. IOC (Inversion Of Control) 란?

     

    3. BeanFactory, ApplicationContext

     

     

    Spring 핵심 Bean을 이해하기 위해서는 DI와 IOC의 개념을 필수적으로 알고 있어야 한다.그렇기 때문에 Bean을 설명하기 전 DI와 IOC에 대해서 소개한 뒤 Bean을 소개하도록 하겠다.

     

     1. DI (Dependency Injection)

    Dependency Injection ... 의존성 주입은 일단 말부터 쉽진 않다. 처음 이 단어를 들었을 때 도대체 이게 뭐지..? 싶었다.

    ' 정의: 필요한 객체를 직접 객체를 생성하는것이 아닌 외부로부터 생성한 객체를 주입 받는 것이다. '

     

    쉽게 를 들어 생각해보자.

     

    객체 A,B가 존재하고 A는 B를 의존한다.

    의존한다 -> 객체 A에서 '객체 B를 생성하고, 생성한 B를 통해 매소드를 쓰거나 해당 객체의 변수를 쓴다'

    여기서 A -> Client(사용자), B -> KbBank(국민은행)

     

    Client 객체

    public class Client {
        KbBank kbBank;
    
        public Client (){
            kbBank = new KbBank();
        }
    
        public void useBank(){
            kbBank.service();
        }
    
        public static void main(String[] args) {
            Client client = new Client();
            client.useBank();
        }
    }

    KbBank 객체

     

    public class KbBank {
    
        public void service() {
            System.out.println("국민은행 서비스 입니다.");
        }
        
    }

     

    Client는 Bank를 이용한다. 하지만, Client 쪽에서 쓰려고하는 Bank를 직접 생성한 뒤 사용한다.

    이렇게 되었을 때 문제점이 무엇인지 한 번 보도록 해보자.

     

    Client 측에서 다른 은행의 서비스를 이용하고 싶어하게 된다면, 코드는 다음과 같이 바뀔것이다.

     

    Client 객체 

     

    public class Client {
        KakaoBank kakaoBank;
    
        public Client (){
            kakaoBank = new KakaoBank();
        }
    
        public void useBank(){
            kakaoBank.service();
        }
    
        public static void main(String[] args) {
            Client client = new Client();
            client.useBank();
        }
    }

     

    KakaoBank 객체

     

    public class KakaoBank {
        
        public void service() {
            System.out.println("카카오 뱅크 서비스 입니다. 무엇을 도와드릴까요?");
        }
    }

     

    KakaoBank 객체를 추가하고, Client 객체를 바꾸었다. 생성자 안의 new로 생성해준 Bank를 바꾸고 usBank()

    매소드의 service를 주체하는 객체또한 변경이 일어났다.

     

    이 정도 규모에서 바꾸는건 당연히 이 정도면 괜찮지 않나..? 싶기도 하지만 위의 KbBank를 쓰는 곳이 많아질 경우

    모든 kbBank를 kakaoBank로 바꾸어 주어야 할것이다. 

     

    이렇게 Client와 Bank 사이 강한 의존성이 생겨 어떤 은행인지 바꿀 때마다 모든 곳을 보며 바꿔줘야 할 것이다.

     

    이 코드를 DI 개념을 적용해서 바꾸어 보자. 외부에서 bank를 생성한 뒤 넣어줘야 한다.

    2번에 걸쳐 바꾸어 보겠다.

     

    1번째 바꾼 코드

     

    public class Client {
        KakaoBank kakaoBank;
    
        public Client (KakaoBank kakaoBank){
            this.kakaoBank = kakaoBank;
        }
    
        public void useBank(){
            kakaoBank.service();
        }
    
        public static void main(String[] args) {
            KakaoBank kakaoBank = new KakaoBank();
            Client client = new Client(kakaoBank);
            client.useBank();
        }
    }

     

    외부에서 KakaoBank 객체를 넣어주었다. 눈치가 빠르다면 역시나 이 코드는 Client에 의존성이 남아있다.

     

    그렇다면, KbBank와 KakaoBank를 공통 인터페이스로 따로 빼 구현체로 두어 주입을 해보면 어떨까??

     

    2번째 바꾼 코드

     

    Bank 인터페이스

     

    public interface Bank {
        void service();
    }

    KbBank 객체

     

    public class KbBank implements Bank{
    
        @Override
        public void service() {
            System.out.println("국민은행 서비스 입니다. 무엇을 도와드릴까요?");
        }
    
    }

     

    KakaoBank 객체

     

    public class KakaoBank implements Bank{
    
        @Override
        public void service() {
            System.out.println("카카오 뱅크 서비스 입니다. 무엇을 도와드릴까요?");
        }
    }

     

    Bank 인터페이스를 상속받게 바꾸어 공통 매소드인 service를 따로 뺐다.

     

    Client의 객체

     

    public class Client {
        Bank bank;
    
        public Client (Bank bank){
            this.bank = bank;
        }
    
        public void useBank(){
            bank.service();
        }
    
        public static void main(String[] args) {
            Bank bank = new KakaoBank();
            Client client = new Client(bank);
            client.useBank();
        }
    }

     

    인터페이스를 주입 받고 외부에서 kakaoBank 객체를 생성하여 주입 받았다.

     

    만약 KbBank의 서비스를 이용하고 싶다면 다음과 같이 바꾸어 주면 된다.

     

    public class Client {
        Bank bank;
    
        public Client (Bank bank){
            this.bank = bank;
        }
    
        public void useBank(){
            bank.service();
        }
    
        public static void main(String[] args) {
            Bank bank = new KbBank();
            Client client = new Client(bank);
            client.useBank();
        }
    }

    위의 코드와 변화가 보이는가?

     

    Bank bank = new KakaoBank() -> Bank bank = new KbBank()의 변화 외엔 딴 것들은 아무것도 바꾸어 주지 않았다.

     

    클라이언트 입장에서는 공통 인터페이스를 주입 받으면서 코드가 훨씬 유연해졌다.

    외부에서 어떤 서비스를 이용하면 좋을 지 선택해서 해당 객체를 주입(DI)만 시켜준다면 해당 객체의 매소드를 이용할 수 있다.

    또한, 새로운 Bank추가할 때 Bank 인터페이스를 상속받아 구현체를 작성하여 추가할 수 있어 확장성도 좋아졌다.

     

    이렇게 두 번의 과정을 거쳐서 DI를 적용해 보았다.

     

    예시를 작성해보니 그렇게 와닿지는 않는 예시지만, 대충 감은 익히지 않았을까 생각해본다.. ㅎ

     

    2. IOC (Inversion Of Control)

    IOC: 제어의 흐름의 역전이란 뭘까

    ' 정의: 간단히 프로그램의 제어 흐름 구조가 뒤바뀌는 것 ' 앞으로 이게 무엇인지 알아보자.

     

    DI만 적용을 해서 보았을 때, main에서 기존에 없었던 역할을 하게 되었다.

     

    어떤 Bank를 할 지 정하고 생성하는 것이다. 여기서 main은 사실 Client가 제대로 동작하는지 확인을 위한 테스트와 같은 것이다.

    하지만 어떤 Bank를 생성하는지의 책임까지 떠맡고 있다. (이해가 안 가시더라도 다음 예시를 봐주세요)

     

    이 관심사를 분리하기 위해 우리는 Object Factory를 만들어 해결해볼 것이다.

     

    Factory

    이 클래스의 역할은 객체의 생성 방법을 결정하여 만들어진 객체를 반환하는 것이다.

     

    KbBank를 이용하는 Client와 KakaoBank를 이용하는 Client를 Factory클래스를 통해 표현해보자.

     

    ClientFactory 객체

    public class ClientFactory {
        public Client KbClient(){
            return new Client(kbBank());
        }
    
        public Client kakaoClient(){
            return new Client(kakaoBank());
        }
    
        public KakaoBank kakaoBank(){
            return new KakaoBank();
        }
    
        public KbBank kbBank(){
            return new KbBank();
        }
    }

     

    Client 객체

     

    public class Client {
        Bank bank;
    
        public Client (Bank bank){
            this.bank = bank;
        }
    
        public void useBank(){
            bank.service();
        }
    
        public static void main(String[] args) {
            Client client = new ClientFactory().KbClient();
            client.useBank();
        }
    }

     

    main() 함수를 보았을 때 따로 Bank를 생성하는 부분을 ClientFactory에 위임했다.

     

    제어의 역전의 개념은 Client와 ClientFactory에 적용된 개념이다.

     

    위에서 보았을 때, Client는 ClientFactory에 의해 생성된다. main에서 직접 Client와 Bank를 직접 선택하지 않고,

    Client를 제어하는 제어권을 ClientFactory에 넘겨주었다.

     

    Client는 수동적인 존재가되어 ClientFactory에 사용할 권한을 넘겨주게 되었다.

     

    자연스럽게 각자의 책임들을 나누면서 ClientFactory를 만드는 과정IOC의 개념이 적용하는 것이라고 볼 수 있다.

     

    이렇게 함으로써 각 객체들은 각자의 책임에 충실하도록 역할에 따라 분리하는 작업을 해보았다.

     

     

     다음으로 Bean에 대해 드디어 알아보도록 하자.

     

    3. BeanFactory, ApplicationContext

     

    위의 개념을 토대로 Bean에 대해서 설명 해보겠다.

     

    Bean 객체는 Spring이 관리하며, 제어권을 가지고 직접 만들고 관계를 부여한다.

    이말인 즉, Spring 컨테이너가 생성관계설정, 사용 등을 제어해주는 제어의 역전(IOC)의 개념이 적용된 객체를 가리킨다.

    그리고 이를 담당하는 부분이 바로 BeanFactory이다.

     

    Bean: IOC 방식으로 관리하는 객체.   ex) Client, kbBank .. 등

    BeanFactory: IOC를 담당하는 핵심 컨테이너.   ex) ClientFactory

    ApplicationContext: BeanFactory에서 상속을 받아 확장한 것. 좀 더 다양한 기능을 할 수 있다.

     

    - 토비의 Spring 1장 내용 중 -

     

    그렇다면, 스프링에서 Bean은 어떤 객체를 말하는 것일까?

     

    먼저 스프링에서는 @ComponentScan을 통해 모든 @Component 들이 붙은 객체들을 Bean으로 등록한다.

     

    @SpringBootApplication
    public class CoreApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(CoreApplication.class, args);
        }
    
    }

     

    실제로 @SpringBootApplication 의 어노테이션 안에 들어가 보면 아래와 같이 @ComponentScan이 있다.

     

    @SpringBootApplication 내부

    Spring은 @ComponentScan을 통해 @Component들을 전부 Bean 객체로 등록하여 싱글톤 방식으로 관리한다.

     

    @Controller, @Service, @Repository 등 또한 @Component가 다 포함되어 있는 어노테이션이다.

    밑을 통해 Controller, Service, Repository 어노테이션이 스프링 컨터에너를 통해 관리되고 있다는 것을 알 수 있다.

    @Controller 내부

     

    @Repository 내부

     

    @Service 내부

     

    물론 위의 그림 외에 Spring 컨테이너에서는 많은 객체들을 싱글톤으로 관리하고 있다.

     

    @Configuration 을 통해 위 뿐만 아니라 Bean으로 등록시키려는 객체에 대한 설정정보를 작성할 수 있으며,

    객체를 생성하는 부분은 @Bean을 통해 등록할 수 있다.

    예시로 들었던 ClientFactory 또한 @Configuration을 통해 등록할 수 있다.

     

    @Configuration 을통해 Bean객체를 등록하는 방법도 결국 @Configuration 내부에 @Component가 있어서 스캔의

    대상에 포함되기 때문에 등록될 수 있다.

     

    위의 예제로 구현한 Bean 설정정보.

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class ClientFactory {
    
        @Bean
        public Client KbClient(){
            return new Client(kbBank());
        }
    
        @Bean
        public Client kakaoClient(){
            return new Client(kakaoBank());
        }
    
        @Bean
        public KakaoBank kakaoBank(){
            return new KakaoBank();
        }
    
        @Bean
        public KbBank kbBank(){
            return new KbBank();
        }
    }

     

    이렇게 간단하게 Bean을 알아보았다.

     

    Bean의 정보를 조회하고, 응용하고 싶다면 스프링 핵심원리 김영한 님의 강의를 추천합니다.

     

     

     

    댓글

Designed by Tistory.