Spring Framework의 Bean을 이해해보자
느슨한 결합과 강한 결합 알아보기
강한 결합
해당 코드에서 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.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에 등록된 값을 이용해 새로운 객체 만들어보기
두 가지 방법이 있다.
메서드 호출
@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=인천 광역시]]
매개변수 설정
@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가 어떤 컴포넌트를 선택해야할지 우선순위를 설정할 수 있다.