4. 스프링 빈과 의존관계

doriskim·2024년 2월 25일
0

스프링 빈을 등록하는 2가지 방법

1. 컴포넌트 스캔과 자동 의존관계 설정
2. 자바 코드로 직접 스프링 빈 등록하기

1. 컴포넌트 스캔과 자동 의존관계 설정

스프링빈을 등록하고 의존관계를 어떻게 설정하는지 알아보자.

지금까지 멤버 객체, 서비스, 리포지토리를 만들었다.
이제 회원 가입하고 그 결과를 HTML로 화면에 붙이고자 한다.
이때 컨트롤러, 뷰 템플릿이 필요하다.

멤버 컨트롤러를 만들어보자.이때 멤버 컨트롤러는 멤버 서비스와 리포지토리를 의존한다고 표현한다.
(멤버 컨트롤러가 멤버 서비스, 리포지토리를 통해서 회원 가입과 데이터를 조회할 수 있어야 한다는 의미)

MemberController.java

package hello.hellospring.controller;

import org.springframework.stereotype.Controller;

@Controller
public class MemberController {
    
}

이렇게까지만 만들어놓으면 스프링 내부에서 어떤 일이 발생할까.

스프링은 처음에 스프링 컨테이너가 생긴다.
처음에 코드에 @Controller 애노테이션이 있으면 스프링은 이 MemberController 객체를 생성해 들고 있는다.
이를스프링 컨테이너에서 스프링빈이 관리된다고 표현하다. 컨트롤러 객체를 생성해서 스프링에 넣어두고 스프링이 관리를 할 수 있다는 의미이다.

MemberController.java

 package hello.hellospring.controller;

import hello.hellospring.service.MemberService;
import org.springframework.stereotype.Controller;

@Controller
public  class MemberController {
    private final MemberService memberService = new MemberService();
}

멤버 서비스를 가져다 쓰기위해 memberService변수를 선언할 때 위처럼 new를 사용하면 멤버 컨트롤러 말고 다른 컨트롤러들이 멤버 서비스를 가져다 쓸 수 있게되는 문제점이 발생한다.
그리고 memberService는 여러 개의 인스턴스를 생성할 필요 없이 하나만 생성해서 공통으로 쓰면 된다.

그러므로 스프링 컨테이너에 등록을 하고 사용하는 방법을 사용하자.(스프링 컨테이너는 딱 하나만 등록이 된다.)
방법은 아래와 같이 생성자를 @Autowired로 연결해주면 된다.

MemberController.java

package hello.hellospring.controller;

import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public  class MemberController {

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}

스프링은 처음에 MemberController를 생성할때 생성자를 호출한다. 그런데 생성자에 @Autowired라고 되어있으면 스프링이 스프링컨테이너에 있는 memberService를 가져다가 연결을 시켜준다.

그러나 위와 같이 작성하고 코드를 돌리면 다음이 MemberService를 찾을 수 없다고 오류가 발생한다.

그 이유는 MemberService 코드가 순수한 자바 코드로서만 짜여져 있어 스프링이 안을 수 있는 방법이 없기 때문이다.
다음과 같이 @Service 애노테이션을 추가해주어야 스프링이 스프링빈을 생성해 관리할 수 있다.

repository도 마찬가지이다. 다음과 같이 @Repository 애노테이션을 추가해준다.

여기까지 하면 스프링 컨테이너에서 Controller, Service, Repository 스프링 빈이 생성된다.
(Controller를 통해서 외부요청을 받고 Service에서 비즈니스 로직을 만들고 Repository에서 데이터를 저장하는 이 패턴은 정형화된 패턴이니 알아두자.)

다음과 같이 연결해주기 위해 MemberService생성자에도 @Autowired를 추가한다.(MemberController, MemberService 생성자에 @Autowired가 추가된 상황임)

생성자에 @Autowired 가 있으면 스프링은 연관된 객체를 스프링 컨테이너에서 찾아 넣어준다.
이렇게 객체 의존관계를 외부에서 넣어주는 것을 DI (Dependency Injection), 의존성 주입이라 한다.
참고로 생성자가 1개만 있으면 @Autowired 는 생략할 수 있다.


1. 컴포넌트 스캔과 자동 의존관계 설정방법은 @Component을 사용해 컴포넌트 스캔을 해서 스프링빈으로 자동 등록하는 방법이다. 위에서 설명한 @Controller, @Service, @Repository를 사용하는 방법도 1번 방법이다.

사실 @Controller, @Service, @Repository위치에 대신 @Component애노테이션을 사용하면 컴포넌트 스캔이 된다. @Controller, @Service, @Repository를 사용해도 컴포넌트 스캔이 되는 이유는 이 애노테이션들 내부에 @Component이 포함되어 있기 때문이다.

그리고 @Autowired로 연결해 사용한다.


그렇다면 아무데나 @Component가 있어도 될까?
다음과 같이 패키지와 코드를 추가해보았다.

그러나 위 코드는 HelloSpringApplication을 실행을 해도 동작하지 않는데

코드를 실행시키면 hello의 hellospring 패키지를 포함해서 하위패키지들은 자동으로 스프링이 뒤져서 스프링빈으로 등록한다. 하지만 패키지가 동일한 레벨이거나 하위 패키지가 아닌 애들은 스프링빈으로 컴포넌트스캔하지 않아 위 demo는 등록되지 않는다.
(설정을 추가해주면 가능할 수 있지만 기본적으론 불가능)


※참고
스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다.(유일하게 하나만 등록해서 공유한다.) 따라서 같은 스프링 빈이면 모두 같은 인스턴스다. 설정으로 싱글톤이 아니게 설정할 수 있지 만, 특별한 경우를 제외하면 대부분 싱글톤을 사용한다.

2. 자바 코드로 직접 스프링 빈 등록하기

위 코드에서 MemberService의 @Service, @Autowired 그리고 MemoryMemberRepository의 @Repository를 삭제하고 직접 스프링빈을 등록해주자. (Controller는 그대로 놔둔다.)

@Bean을 이용하면 스프링이 이를 인식해 스프링빈에 등록해준다.

package hello.hellospring;

import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
}

위 코드에서 MemberService와 MemberRepository를 둘 다 스프링빈에 등록을 한다. 그러면서 new MemberService(memberRepository())를 생성할 때 스프링빈에 등록되어있는 MemberRepository를 memberService에 넣어준다.

Controller는 스프링이 관리해야하기때문에 어쩔 수 없이 컴포넌트 스캔 방식을 이용한다.

그러면 마찬가지로 스프링 컨네이너에 다음과 같은 구조가 완성된다.

[정상적으로 실행되는 모습]

[DI의 3가지 방법]
1. 생성자 주입
다음과 같이 생성자를 통해서 주입이 되면 생성자 주입이라고 한다.

  1. 필드 주입
    생성자를 빼고 필드에다 @Autowired를 이용하는 방법이다.
    단점: 생성 이후 중간에 뭔가를 바꿀 수 있는 방법이 아예 없다.

  2. setter 주입
    setter에 @Autowired를 이용하는 방법이다.
    생성은 생성대로 되고 setter는 나중에 호출이 돼서 memberService에 들어오는 방법이다.
    단점: 누군가가 MemberController를 호출했을때 이게 public으로 열려있어 아무 개발자나 호출할 수 있다.

의존관계가 실행중에 동적으로 변하는 경우는 거의 없으므로 생성자 주입을 권장한다. 애플리케이션을 로딩할 때 처음에 '애플리케이션이 조립된다'고 표현을 하는데, 조립될 때 한 번 세팅된 다음, 다음에 변경할 수 없게해야한다.


참고

  • 실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용한다.

  • 그리고 정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다.

이게 무슨말이냐하면, 이번에 설정한 회원 관리 비즈니스 모델 예제에서 '아직 데이터 저장소가 선정되지 않았다'고 가정했다.
아직 데이터 저장소가 선정되지 않아 일단 메모리로 만들고 나중에 교체할 예정이었기 때문에 인터페이스 설계를 하고 구현체로 memoryMemberRepository를 사용했었다.

이후 memoryMemberRepository를 데이터베이스에 실제 연결하는 Repository로 바꿔치기 할 때 기존 코드를 하나도 손대지 않고 바꾸고자 한다.
나중에 데이터베이스를 연결할 때 아래 코드만 변경해주면 된다.

컴포넌트 스캔을 하면 여러 코드를 바꿔야하지만 자바 코드로 직접 스프링빈을 등록하는 방식은 여기만 고치면 된다.

package hello.hellospring;
 import hello.hellospring.repository.MemberRepository;
 import hello.hellospring.repository.MemoryMemberRepository;
 import hello.hellospring.service.MemberService;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 @Configuration
 public class SpringConfig {
     @Bean
     public MemberService memberService() {
         return new MemberService(memberRepository());
     }
     
     @Bean
     public MemberRepository memberRepository() {
     	//Before
        //return new MemoryMemberRepository(); 
        
     	//After
        return new DbMemoryMemberRepository(); 
     }
}

주의
@Autowired 를 통한 DI는 helloController, memberService 등과 같이 스프링이 관리하는 객체에서만 동작한다. 스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다.

현재 config에선 memberService, memberRepository스프링빈이 관리되고 있다. 근데 만약 config에서 @Bean에 해당되는 코드를 모두 지우면 @Autowired는 작동하지 않는다.

또 new를 이용해 다음과 같이 내가 직접 객체를 생산하는 경우도 @Autowired는 동작하지 않는다.

public static void main(String[] args){
	this.memberRepository = memberRepository;
}

즉 객체가 스프링 컨테이너에 등록이 된 것들만 @Autowired가 동작한다

0개의 댓글