Spring Framework의 Bean을 이해해보자

·

7 min read

느슨한 결합과 강한 결합 알아보기

강한 결합

  • 해당 코드에서 GameRunner 클래스는 MarioGame과 강하게 결합되어 있다.

      public class GameRunner {
          private MarioGame game;
          public GameRunner(MarioGame marioGame) {
              this.game = marioGame;
          }
    
          public void run() {
              System.out.println("Running game : " + game);
              game.up();
              game.down();
              game.left();
              game.right();
          }
      }
    
  • 실행하려는 게임을 바꾸려면 코드를 변경해야 한다.

느슨한 결합

  • 인터페이스를 도입해보자.
public interface GamingConsole {
    void up();
    void down();
    void left();
    void right();
}
public class GameRunner {
    private GamingConsole game;
    public GameRunner(GamingConsole game) {
        this.game = game;
    }

    public void run() {
        System.out.println("Running game : " + game);
        game.up();
        game.down();
        game.left();
        game.right();
    }
}

Spring Framework를 도입하여 Java 앱 느슨하게 결합하기

var game = new MarioGame(); // 1
var gameRunner = new GameRunner(game); // 2
  • 1,2 모두 객체를 생성하지만 2는 의존성 결합도 같이 하고 있다.

  • 2에서 game이 의존성이다 → GamingConsole은 GameRunner 클래스의 의존성이다

  • 현재 객체 생성을 완전히 우리가 관여하고 있지만, 수동으로 객체를 관리하는 대신 Spring Framework가 하게 하자.

Java Spring Bean 및 Java Spring 설정 시작

애플리케이션을 실행하면 JVM이 시작되는데, 이때 JVM 내부에서 Spring 컨텍스트를 생성하고 스프링 프레임워크가 name을 관리하도록 할 것이다. 그러기 위해서 다음 단계를 거칠 것이다.

1. Spring Context 실행

2. Spring 프레임워크가 관리하도록 설정 - @Configuration

Configuration 파일 생성

  • @Configuration 애너테이션을 통해 설정 파일임을 나타낼 수 있다.

  • Configuration 클래스에서 메서드를 정의하여 Spring Bean을 생성할 수 있다.

😽 빈(Bean)은 스프링 컨테이너에 의해 관리되는 재사용 가능한 소프트웨어 컴포넌트이다. 즉, 스프링 컨테이너가 관리하는 자바 객체를 뜻하며, 하나 이상의 빈(Bean)을 관리한다.

@Configuration
public class HelloWorldConfiguration {

}

Spring Boot Context 준비 및 실행

var context = new AnnotationConfigApplicationContext(HelloWorldConfiguration.class);
  • 관리하고자 하는 항목(HelloWorldConfiguration) 선택

  • 오류 없이 실행된다면 설정 파일을 이용해 Spring 컨텍스트를 실행할 수 있게 된 것임.

빈 생성하기

@Configuration
public class HelloWorldConfiguration {
    @Bean
    public String name() {
        return "예림";
    }

}
  • Spring Bean을 만들려면 @Bean을 호출해야 하는 과정이 필요하다.

  • 이렇게 만들어진 Bean은 스프링 컨테이너가 관리할 것이다.

  • JVM이 있고, Spring은 특정 객체 name을 관리하는 것이다.

Spring이 관리하는 Bean 검색

System.out.println(context.getBean("name"));

// 예림
  • Bean은 context.getBean에 Bean의 이름을 부여하여 검색할 수 있다.

Spring Java 설정 파일에서 더 많은 Bean 만들기

record를 이용해 Bean 생성하기

record Person (String name, int age) { };

@Configuration
public class HelloWorldConfiguration {
    @Bean
    public Person person() {
        var person = new Person("Ravi", 20);
        return person;
    }
}
  • JDK16에서 추가된 새로운 기능

  • Java 클래스를 만들 때에는 많은 Getter, Setter, 생성자, 해시코드 등을 만들어야 하는데, 레코드를 이용하면 세터, 게터, 생성자 등을 만들지 않아도 자동으로 생성됨.

  • 위 예제에서는 name과 age 게터 세터 생성자가 자동으로 만들어짐.

    • person.name()

    • person.age()

    • 으로 가져올 수 있음

  • 빈 검색 결과

      Person[name=Ravi, age=20]
    

Spring Framework Java 구성 파일에서 자동 연결 구현

Bean 이름 설정하기

Bean의 디폴트 네임은 메서드명임

@Bean(name = "yourCustomBean")
public Address address() {
    return new Address("한국", "인천 광역시");
}

getBean의 인자로 클래스 넘기기

  • Bean의 이름이 아닌 클래스로도 Bean을 가져올 수 있음
System.out.println(context.getBean(Address.class)); 
// Address[country=한국, city=인천 광역시]

Bean에 등록된 값을 이용해 새로운 객체 만들어보기

두 가지 방법이 있다.

  1. 메서드 호출

     @Bean
     public Person person2MethodCall() {
         return new Person(name(), age());
     }
    
     System.out.println(context.getBean("person2MethodCall"));
    

     record Person (String name, int age, Address address) { }
    
     @Bean
     public Person person() {
         return new Person("Ravi", 20, new Address("미국", "뉴욕"));
     }
    
     System.out.println(context.getBean("person")); 
     // Person[name=Ranga, age=22, address=Address[country=한국, city=인천 광역시]]
    
  2. 매개변수 설정

     @Bean
     public Person personParameters(String name, int age, Address address2) {
         return new Person(name, age, address2);
     }
    

    Bean의 이름을 address2로 설정해주었으므로 매개변수명도 그렇게 설정해주어야 함

     System.out.println(context.getBean("personParameters"));
    

    Spring IOC 컨테이너

    Spring 컨테이너

    • Spring Bean과 수명 주기를 관리함

    • Java 클래스 + Config 파일을 만들면 IOC 컨테이너가 런타임시스템을 만듦

    • Spring Container의 인풋 : Java 클래스(Person, Address)와 설정 파일(HelloWorldConfiguration)

    • Spring Container의 아웃풋 : Ready System

      런타임하면 IOC 컨테이너가 런타임 시스템을 만들고 모든 Bean을 관리하는 것이다.

    • Spring Context, Spring Container, IOC 컨테이너 모두 동일한 것을 의미함. 클래스의 인풋을 가지고 실행되는 시스템을 만드는 것을 의미함.

    • Spring 컨테이너에 대해 이야기할 때 논의의 대상이 되는 두 가지 IOC 컨테이너

      • Bean Factory - 기본 Spring 컨테이너

      • Application Context - 엔터프라이즈 전용 기능이 있는 고급 Spring 컨테이너 (가장 자주 사용)

        • REST API

        • 웹 서비스 등에 사용

Java Bean, POJO, Spring Bean 살펴보기

  • Java Bean - 세 가지 제약을 준수하는 클래스(EJB에서 만든 것이 Java Bean / 요즘은 사용하지 않음)

    • no args contructor

    • getter / setter

    • java.io.Serializable을 구현해야 함

  • POJO - 모든 자바 객체는 POJO(Plain Old Java Object)

  • Spring Bean은 Spring이 관리하는 모든 자바 객체

    • IOC 컨테이너가 관리하는 모든 자바 객체

Bean 자동 연결 살펴보기 - 기본 및 한정자

Spring이 관리하는 Bean 프레임워크를 모두 나열하려면 어떻게 해야 할까?

context.getBeanDefinitionNames()를 사용한다. 해당 함수의 반환 타입은 String[] 이다. 이를 함수형 프로그래밍으로 구현하여 모든 Bean의 이름을 나열하고도록 해보자.

    Arrays.stream(context.getBeanDefinitionNames())
                .forEach(System.out::println);

    /*
    org.springframework.context.annotation.internalConfigurationAnnotationProcessor
    org.springframework.context.annotation.internalAutowiredAnnotationProcessor
    org.springframework.context.annotation.internalCommonAnnotationProcessor
    org.springframework.context.event.internalEventListenerProcessor
    org.springframework.context.event.internalEventListenerFactory
    helloWorldConfiguration
    name
    age
    */

빈 우선순위 부여

일치하는 후보가 여러 개인 시나리오에서는 Spring에서 후보(candidate)가 여러 개라는 예외를 출력함

    System.out.println(context.getBean(Address.class)); // Address Bean이 여러 개일 경우
    public Person person1(String name, int age, Address address) { }
    public Person person1(String name, int age, Address address) { }

다음과 두 경우 똑같은 에러가 발생한다.

    No qualifying bean of type 'com.in28minutes.learnspringframework.Address' available: expected single matching bean but found 2: address2,address3

해결방법은 다음과 같다.

1. Primary를 사용해 우선순위 조정

    @Bean(name = "address2")
    @Primary
    public Address address() {
        var address = new Address("한국", "인천 광역시");
        return address;
    }

2. Qualifier 사용

한정자를 지정해 활용한다.

    @Bean(name = "address3")
    @Qualifier("address3qualifier")
    public Address address3() {
        var address = new Address("미국", "뉴욕");
        return address;
    }
    @Bean
    public Person person5Quailifier(String name, int age, @Qualifier("address3qualifier") Address address) {
        return new Person(name, age, address);
    }

try-with-resources 를 이용해 컨텍스트 닫기

    public class App02HelloWorldSpring {
        public static void main(String[] args) {
            try(var context
                        = new AnnotationConfigApplicationContext
                        (HelloWorldConfiguration.class);) {
                System.out.println(context.getBean("name"));
                System.out.println(context.getBean("age"));
                System.out.println(context.getBean("person"));
                System.out.println(context.getBean("person2MethodCall"));
                System.out.println(context.getBean("person4Parameters"));
                System.out.println(context.getBean(Address.class));
                System.out.println(context.getBean(Person.class));
                System.out.println(context.getBean("person5Qualifier"));

                Arrays.stream(context.getBeanDefinitionNames())
                        .forEach(System.out::println);

            }
        }
    }

배운 내용을 바탕으로 직접 구현해보기

    @Configuration
    public class GamingConfiguration {

        @Bean
        public GamingConsole game() {
            var game = new PacmanGame();
            return game;
        }

        @Bean
        public GameRunner gameRunner(GamingConsole game) {
            var gameRunner = new GameRunner(game);
            return gameRunner;
        }
    }
    public class App03GamingSpringBean {
        public static void main(String[] args) {
            try(var context = new AnnotationConfigApplicationContext(GamingConfiguration.class);) {

                context.getBean(GamingConsole.class).up();
                context.getBean(GameRunner.class).run();
            }
        }
    }
    public class GameRunner {
        private GamingConsole game;
        public GameRunner(GamingConsole game) {
            this.game = game;
        }

        public void run() {
            System.out.println("Running game : " + game);
            game.up();
            game.down();
            game.left();
            game.right();
        }
    }
    public interface GamingConsole {
        void up();
        void down();
        void left();
        void right();
    }

Spring Framework 이해하기

Bean을 수동으로 만들지 않고 Spring 프레임워크가 우리에게 Bean을 생성해줄 수 있다면 어떨까?

그렇게 하기 위해선 Configuration 파일과 App 파일을 결합해야 한다.

    @Configuration
    public class App03GamingSpringBeans {

        @Bean
        public GamingConsole game() {
            var game = new PacmanGame();
            return game;
        }

        @Bean
        public GameRunner gameRunner(GamingConsole game) {
            var gameRunner = new GameRunner(game);
            return gameRunner;
        }
        public static void main(String[] args) {
            try(var context = new AnnotationConfigApplicationContext(App03GamingSpringBeans.class);) {

                context.getBean(GamingConsole.class).up();
                context.getBean(GameRunner.class).run();
            }
        }
    }

이제 Pacman 게임 생성을 Spring에 요청해보자. 특정 클래스의 인스턴스 생성을 Spring에 요청하려면 클래스에 @Component 어노테이션을 추가해야 한다.

    @Component
    public class PacmanGame implements GamingConsole {
        ...
    }

추가해도 다음과 같이 Bean을 등록해주지 않으면 Spring은 특정 컴포넌트를 찾지 못한다.

    @Bean
    public GamingConsole game() {
        var game = new PacmanGame();
        return game;
    }

그렇다면, 저 Bean을 지웠을 때에도 실행이 되도록 하려면 어떻게 해야할까? 설정 파일에 @ComponentScan 을 붙이면 된다.

    @Configuration
    @ComponentScan
    public class App03GamingSpringBeans {
        ...
    }

개선 전 코드

    @Configuration
    class GamingConfiguration {

        @Bean
        public GamingConsole game() {
            var game = new PacmanGame();
            return game;
        }

        @Bean
        public GameRunner gameRunner(GamingConsole game) {
            var gameRunner = new GameRunner(game);
            return gameRunner;
        }
    }
    public class App03GamingSpringBean {
        public static void main(String[] args) {
            try(var context = new AnnotationConfigApplicationContext(com.in28minutes.learnspringframework.GamingConfiguration.class);) {

                context.getBean(GamingConsole.class).up();
                context.getBean(GameRunner.class).run();
            }
        }
    }

개선 후 코드

    @Configuration
    @ComponentScan
    public class App03GamingSpringBeans {
        public static void main(String[] args) {
            try(var context = new AnnotationConfigApplicationContext(App03GamingSpringBeans.class);) {

                context.getBean(GamingConsole.class).up();
                context.getBean(GameRunner.class).run();
            }
        }
    }

이렇듯 Spring은 객체를 관리하고 자동 auto-wiring을 할 뿐만 아니라 우리에게 객체를 생성해준다.

그런데 문제가 있다. GamingConsole을 implement하는 다른 클래스가 컴포넌트로 등록되어있다면 Spring은 무엇을 선택해야 할까?

Primary와 Qualifier 어노테이션 알아보기

Primary

@Primary 어노테이션으로 우선순위를 부여할 수 있다.

    @Component
    @Primary
    public class MarioGame implements GamingConsole {
        ...
    }

Qualifier

@Primary로 우선순위를 설정했더라도 @Qualifier로 우선순위를 설정할 수 있다.

    @Component
    @Qualifier("SuperContraGameQualifier")
    public class SuperContraGame implements GamingConsole{
        ...
    }
    @Component
    public class GameRunner {
        private GamingConsole game;
        **public GameRunner(@Qualifier("SuperContraGameQualifier") GamingConsole game) {**
            this.game = game;
        }

        public void run() {
            System.out.println("Running game : " + game);
            game.up();
            game.down();
            game.left();
            game.right();
        }
    }

결론

  • Spring Framework를 사용하여 Java 객체를 생성하고 관리할 수 있다.

  • Spring은 객체를 관리하고 자동 auto-wiring을 할 뿐만 아니라 우리에게 객체를 생성해준다.

  • Primary와 Qualifier 어노테이션으로 Spring Framework가 어떤 컴포넌트를 선택해야할지 우선순위를 설정할 수 있다.