Spring 입문 Chapter 4-2. 스프링 빈과 의존관계 : 자바 코드로 직접 스프링 빈 등록하기
이 글은 인프런에 있는 김영한님의 "스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술" 강의를 듣고 정리한 필기입니다.
⛅️ 코드로 직접 등록하기 전 상태 되돌리기
이전 시간에는 컴포넌트 스캔(Component scan)을 통해서 Controller, Service, Repository를 spring bean에 등록하고,
Autowired를 통해 bean들을 연결했다.
이번 시간에는 코드로 직접 Service, Repository를 spring bean에 등록해볼 것인데, 그러려면 우선 컴포넌트 스캔으로 등록하기 위해 적었던 어노테이션을 제거해야 한다.
1. MemberService와 MemberRepository의 @Service, @Repository 그리고 @Autowired 어노테이션을 제거한다.
2. 이 때, MemberController는 어노테이션을 제거하지 않는다. spring container에 저장하고 Controller로 관리해야 하기 때문이다.
이 상태에서 코드를 실행하면 컴포넌트 스캔이 되지 않아서 memberService가 spring bean에 등록되지 않을 것이다.
따라서 memberService을 찾을 수 없다는 오류가 발생하게 된다.
이제 자바 코드를 작성하여 직접 spring bean을 등록할 준비가 된 것이다.
⛅️ @Bean을 통해 직접 spring bean 등록하기
service 패키지에 SpringConfig 클래스를 생성하고, @Configuration 어노테이션을 추가한다.
여기서 @Configuration은 설정 파일을 만들거나 Bean을 등록하기 위한 어노테이션이다. @Bean을 사용하는 클래스의 경우 반드시 @Configuration을 같이 사용해야 한다.
@Configuration
public class SpringConfig {
}
[MemberService를 spring bean에 등록하기]
SpringConfig 클래스에 다음과 같은 코드를 작성한다.
@Bean
public MemberService memberService() {
return new MemberService();
}
@Bean은 spring bean으로 등록할 것이라는 뜻이다.
이렇게 하면 spring이 뜰 때, @Configuration에서 @Bean을 읽고, "어? spring bean에 등록하라는 뜻이네?"하고 인식을 할 것이다.
그리고 return 값을 통해 받은 MemberService를 spring bean에 등록을 해줄 것이다.
그런데 return 줄은 보면 생성자쪽에 빨간 줄이 떠있음을 볼 수 있다.
MemberService의 생성자는 MemberRepository를 매개변수로 받아야 하는데, 아무것도 전달해주지 않았기 때문이다.
하지만 현재 SpringConfig 클래스에서는 MemberRepository를 전달할 수 있는 방법이 없다.
우선 MemberRepository도 spring bean에 등록하기로 했으므로, MemberRepository도 등록하자.
[MemberRepository 를 spring bean에 등록하기]
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
이전에 비지니스 요구 사항 정리할 때, DB가 현재 정해지지 않은 상태이므로 우선 메모리 기반 Repository를 만들고 이후에 교체하기로 했었다.
MemberRepository라는 인터페이스를 선언하고, MemoryMemberRepositroy라는 구현체가 MemberRepository를 상속받아서 필요한 기능을 구현하기로 했었다. 따라서 return 값으로 MemoryMemberRepositroy를 전달한다.
MemberService와 마찬가지로 @Bean을 통해 spring bean에 등록한다. 다만 생성자에 매개 변수를 받지 않으므로 빨간 줄이 뜨지 않는다.
[MemberService 생성자 보완하기]
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
MemoryMemberRepository를 반환하는 함수 memberService를 생성했기 때문에, MemberService의 생성자로 전달한다.
이렇게 하면 spring bean에 정상적으로 MemberService에 등록될 것이다.
[정리]
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
}
이렇게 하면 spring이 뜰 때, @Bean 어노테이션을 보고 MemberService와 MemberRepository를 spring bean에 등록한다.
동시에 생성자 밑의 memberRepository() 메서드를 호출하여, spring beandp 등록되어 있는 MemberRepository를 연결해준다. Autowired와 비슷한 느낌이다.
그렇게 해서 Controller, Service, Repository를 위 그림처럼 연결된다.
Controller는 spring이 관리하기 때문에, 컴포넌트 스캔을 통해 spring container에 올라가고 또 컴포넌트 스캔을 이용하기 때문에 Autowired로 받는 것이 대부분이다.
여기까지가 자바 코드로 직접 스프링 빈을 등록하는 방법이다.
⛅️ DI의 세 가지 방법 (Field, Setter, Constructor)
DI(Dependency Injection)에는 세 가지 방법이 있는데, Field 주입, Setter 주입, Constructor 주입 이렇게 세 가지이다.
우리가 지금까지 사용한 방법은 Constructor 주입으로써, 생성자를 통해서 전달받는 방법을 말한다.
첫 번째로 Field 주입은 다음과 같은 형태이다.
멤버 변수의 앞에 @Autowired 어노테이션을 붙인 모양이다.
하지만 이 방법을 사용하면 IntelliJ에서도 다른 방법을 추천하며, 별로 추천하지 않는 방법이다.
필드 주입은 spring이 처음 뜰 때에만 spring container에서 받아와서 넣어주고, 중간에 값을 바꿀 수 있는 방법이 없다.
@Autowired private MemberService memberService;
두 번째로는 Setter 주입으로 다음과 같은 형태를 띄고 있다.
@Controller
public class MemberController {
private MemberService memberService;
@Autowired
public void setMemberService(MemberService memberService) {
this.memberService = memberService;
}
}
생성(Construct)는 생성대로 진행이 되고, Setter는 이후에 호출이 되서 spring container에서 불러오는 방식이다.
이 방법의 단점은 setter가 public으로 설정되어 있기 때문에, 누군가 MemberController를 호출 했을 때 의도치 않게 MemberService를 수정할 수 있다는 점이다.
spring bean은 대부분 한 번 설정되고 나면, 이후에 수정할 일이 거의 없는데 public으로 노출되어 있어 문제가 생길 가능성이 있다.
마지막으로는 Constrcutor 주입으로 다음과 같은 형태를 띄고 있다.
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
최근에는 생성자(Constructor)를 이용한 주입을 권장한다.
생성자를 이용한 주입을 하게 되면, 처음 어플리케이션이 조립된다고 표현하는 기점(spring bean이 세팅되는 시점)에 Autowired를 통해서 한 번 할당되고 끝난다.
조립 시점에 생성자를 통해 한 번만 할당을하고 마무리가 되기 때문에 public으로 설정되어 있는 setter보다 안전하다.
의존 관계가 Runtime 중에 동적으로 변하는 경우는 거의 없으므로(사실 아예 없다) 생성자 주입을 권장한다.
만일 정말 의존 관계를 바꿔야 하는 상황이라면, SpringConfig 파일을 수정하고 서버를 다시 올리는 것이 바람직하다.
실무에서는 주로 정형화된 Controller, Service, Repository 같은 코드는 컴포넌트 스캔을 사용한다.
정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정(SpringConfig)를 통해 spring bean으로 등록한다.
상황에 따라 구현 클래스를 변경해야하는 경우?
MemberRepository를 MemoryMemberRepository 구현체로 구현한 상태이다. 하지만 DB가 선정되면 DbMemberRepository와 같이 다른 구현체로 변경해야할 것이다. 이러한 경우를 말한다.
만일 Database를 연결한 DbMemberRepositroy로 변경한다고 하면, 직접 설정 파일을 운영할 때에는 SpringConfig 클래스에서 return 값을 DbMemberRepository로 변경만 하면 된다.
이런 점이 코드로 직접 빈을 등록할 때의 장점이다.
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
return new DbMemberRepository();
}
}