강의/Java Spring Boot

Spring Framework를 사용하여 Java 객체(Bean)을 생성하기

studylida 2024. 7. 24. 21:00

저번 시간에 다 설명하지 못한 Question 5부터 이어서 설명하겠다.

Question 5: Spring is managing objects and performing auto-wiring

앞선 시간에는 우리가 Bean을 수동으로 생성했다.

그런데 만약 Bean을 수동으로 만들 필요가 없다면 어떨까?

즉, Spring 프레임워크가 우리 대신 Bean을 생성해줄 수 있다면 어떨까, 하는 질문인데

일단 질문에 답하기 전에 이를 위한 환경을 만들기 위해 Configuration 파일과 App 파일을 결합한다. 지금부터 import문은 생략하도록 하겠다.

package com.in28minutes.learnspringframework;

import....

@Configuration
class GamingConfiguration {

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

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

}

public class App03GamingSpringBeans {

    public static void main(String[] args) {

        try(var context = new AnnotationConfigApplicationContext(GamingConfiguration,class)) {

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

        }
    }

}

합쳐보았다. 그런데 이렇게 한 파일에 합쳐놓았다면 @Bean을 @Configuration에 작성하지 않고 Launch 클래스에 작성해도 되지 않을까? 한 번 해보자.

package com.in28minutes.learnspringframework;

import....

@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();

        }
    }

}

이제 Spring에 Bean을 생성해달라고 요청해볼 건데, 이를 위해서는 생성을 원하는 Bean에 @Component라는 어노테이션을 추가해야 한다. 이 어노테이션을 통해 스프링은 자신이 어떤 컴포넌트를 관리해야할지 알 수 있다.

PacmanGame 클래스에 @Component를 입력해보자.

package com.in28minutes.learn_spring_framework.game;

import....

@Component // Indicates that an annotated class is a "component". Such classes are considered as candidates for auto-detection when using annotation-based configuration and classpath scanning.
public class PacmanGame implements GamingConsole {

    public void up() {
        System.out.println("up");
    }

    public void down() {
        System.out.println("down");
    }

    public void left() {
        System.out.println("left");
    }

    public void right() {
        System.out.println("rigth");
    }
}

@Component 어노테이션 옆에 적혀있는 주석을 보면 Indicates that an annotated class is a "component". Such classes are considered as candidates for auto-detection when using annotation-based configuration and classpath scanning.라고 적혀있다. 이러한 클래스는 어노테이션 기반의 Configuration과 classpath scanning을 사용할 때, 자동 감지되는 후보에 포함된다.

이제 우리가 작성해놓은 GamingConsole Bean을 삭제하고, 실행해보자.

package com.in28minutes.learnspringframework;

import....

@Configuration
public class App03GamingSpringBeans {

    @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();

        }
    }

}

아마 오류가 발생할 것이다. NoSuchBeanDefinitionException이라는 예외를 발견할 수 있을텐데, 이는 Spring이 특정 컴포넌트를 찾지 못하고 있다는 것이다.

이상하지 않은가? 분명 방금 전에 PacmanGame 클래스에 @Component 어노테이션을 붙였을텐데 말이다.

이렇게 오류가 발생하는 이유는 Spring에게 PacmanGame을 어디서 찾아야 하는지 알려주지 않았기 때문이다.

Spring에게 컴포넌트를 어디서 찾아야할지 알려주기 위해서는 @ComponentScan이라는 어노테이션을 사용하여 할 수 있다.

package com.in28minutes.learnspringframework;

import....

@Configuration
@ComponentScan("com.in28minutes.learnspringframework.game") 
// package about game such as PacmanGame, GameRunner, etc.
public class App03GamingSpringBeans {

    @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();

        }
    }

}

이번에 위의 코드를 실행하면 성공적으로 실행이 이루어질 것이다.

즉, Spring이 PacmanGame 인스턴스를 생성하는 데 성공했다는 것이다. 자동 와이어링이 이루어진 것이다.

Spring이 PacmanGame 인스턴스를 생성하고, 이 인스턴스가 GameRunner에 와이어링 되었다. Parameter로 전송된 것이다.

그런데 생각해보면 GameRunner도 Bean이니까 Spring 보고 알아서 처리하라고 할 수 있지 않을까?

package com.in28minutes.learnspringframework.game;

import ....

@Component
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();

    }
}
package com.in28minutes.learnspringframework;

import....

@Configuration
@ComponentScan("com.in28minutes.learnspringframework.game") 
// package about game such as PacmanGame, GameRunner, etc.
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이 객체를 관리하고 자동 와이어링을 수행할 뿐만 아니라 객체를 생성함을 알 수 있다.

일단 이제 우리는 Bean에 대해 공부하고 있지 않으니 클래스의 이름을 App03GamingSpringBeans에서 GamingAppLauncherApplication으로 바꾸어보자.

그런데 우리가 Bean을 공부할 때, 하나의 인터페이스를 구현하는 여러 개의 클래스가 생기자, Spring이 어찌할지 모르고 오류를 발생시킨 적이 있다.

@Component에서는 어떨까? 마찬가지일거라는 생각이 들지만 일단 MarioGame에도 @Component 어노테이션을 붙여서 테스트해보자.

package com.in28minutes.learn_spring_framework.game;

import....

@Component
public class MarioGame implements GamingConsole{

    public void up() {
        System.out.println("Jump");
    }

    public void down() {
        System.out.println("Go into a hole");
    }

    public void left() {
        System.out.println("Go back");
    }

    public void right() {
        System.out.println("Accelerate");
    }
}

이렇게 하고 실행하면 NoUniqueBeanDefinitionExecption이 발생한다. 이를 위해서는 @Primary를 사용하거나, @Qualifier를 사용할 수 있다.

@Primary는 여러 후보가 단일 값 의존성을 자동 와이어링할 자격이 있는 경우, 해당 Bean에 우선권을 부여한다.

package com.in28minutes.learn_spring_framework.game;

import....

@Component
@Primary
public class MarioGame implements GamingConsole{

    public void up() {
        System.out.println("Jump");
    }

    public void down() {
        System.out.println("Go into a hole");
    }

    public void left() {
        System.out.println("Go back");
    }

    public void right() {
        System.out.println("Accelerate");
    }
}

@Qualifier는 해당 Bean에 이름을 부여하고, 이름을 통해 해당 Bean을 호출할 수 있게 한다.

package com.in28minutes.learn_spring_framework.game;

import....

@Component
@Qualifier("SuperContraGameQualifier")
public class SuperContreGame implements GamingConsole {
    public void up() {
        System.out.println("up");
    }

    public void down() {
        System.out.println("Sit down");
    }

    public void left() {
        System.out.println("Go back");
    }

    public void right() {
        System.out.println("Shoot a bullet");
    }
}
package com.in28minutes.learn_spring_framework.game;

import....

@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();
    }
}

@Primary와 @Qualifier는 이전 게시글에서 다루었으니 굳이 자세하게 다루진 않겠다.

혹시 헷갈릴까봐 덧붙이자면, @Primary는 자동 와이어링을 하는 경우 우선권을 부여하는 것이기 때문에, 만약 @Qualifier를 이용해 명시적으로 특정 Bean을 호출하겠다 하면, @Primary를 사용한 Bean이 아니라 @Qualifier를 통해 호출하려고 한 Bean이 호출된다.

이를 이해하기 위해서 아래의 코드를 보고 ComplexAlgorithm과 AnotherComplexAlgorithm은 어떤 정렬 알고리즘을 호출할지 생각해보자. 주석에 힌트가 적혀있다.

@Component @Primary
class QuickSort implement SortingAlgorithm {}

@Component
class BubbleSort implement SortingAlgorithm {}

@Component @Qualifier("RadixSortQualifier")
class RadixSort implement SortingAlgorithm {}

@Component
class ComplexAlgorithm
    @Autowired
    private SortingAlgorithm algorithm;
/*
If you use only @Autowired, you're asking for the best SortingAlgorithm
It doesn't matter what SortingAlgorithm you have.
If you have one SortingAlgorithm, give me that one.
If you have 10 SortingAlgorithms, we're asking for the best one.
*/

@Component
class AnotherComplexAlgorithm
    @Autowired @Qualifier("RadixSortQualifier")
    private SortingAlgorithm iWantToUseRadixSortOnly;
/*
If there is no @Qualifier("RadixSortQualifier").

In this case, you can use the name of the bean as a qualifier, as long as the first letter is lowercase.
*/

아래는 @Component와 @Bean을 비교한 표다.

Heading @Component @Bean
Where? Can be used on any Java class Typically used on method is Spring Configuration classes
Easy of use Very easy. Just add an annotation You wirte all the code.
Autowiring Yes. Field, Setter or Constructor Injection Yes - method call or method parameter
Who creates beans Spring Framework You write bean creation code
Recommended For Instantiating Beans for Your Own Application Code: @Component 1: Custom Business Logic
2: Instantiating Beans for 3rd-party libraries: @Bean