본문 바로가기

Dev/Spring Boot

[코드로 배우는 스프링 부트] 스프링과 빈의 의존관계

 본 포스팅은 인프런의 '스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술' 강의를 수강하며 정리한 내용입니다.

 

 지금까지 서비스와 리포지토리, 도메인을 생성하고, 처리하는 로직을 작성했습니다. 이번 포스팅에서는 화면 단을 작성하기 위해서 회원 컨트롤러가 회원서비스와 회원 리포지토리를 사용할 수 있게 의존관계를 준비할 것입니다.

 

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

1. 컴포넌트 스캔과 자동 의존관계 설정 ex. @Controller, @Service ...

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

 

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

 스프링이 시작될 때, @Controller, @Service, @Repository 와 같은 컴포넌트들을 스캔하고, 스프링에 빈으로 등록해줍니다. 그리고 @Autowired 어노테이션으로 의존 관계를 연결해주는 방식입니다. @Component 애노테이션이 있으면 스프링 빈으로 자동 등록됩니다. @Controller 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문입니다. @Component 를 포함하는 @Controller @Service @Repository도 스프링 빈으로 자동 등록됩니다.

 참고: 생성자에 @Autowired 를 사용하면 객체 생성 시점에 스프링 컨테이너에서 해당 스프링 빈을 찾아서 주입합니다. 생성자가 1개만 있으면 @Autowired 는 생략할 수 있다고 합니다.

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

 

Code

MemberController.java 파일 생성

package hello.hellospring.controller;
import org.springframework.stereotype.Controller;

    @Controller
    public class MemberController {

    }

스프링컨테이너는 프로젝트가 처음 시작될 때, @Controller 어노테이션이 붙어있으면 MemberController 객체를 생성해서 스프링에 넣어두고 관리합니다. 이것을 스프링 컨테이너에서 빈이 관리된다라고 표현합니다.

 이때, 컨트롤러 클래스가 서비스 클래스를 new MemberService(); 로 호출하지 않고, 스프링이 관리하는 스프링서비스 객체를 받아서 사용해야 합니다. 여러 컨트롤러들이 MemberService를 사용할 수 있는데, 여러개 new 할 필요 없이 공유하는 것이 좋기 때문입니다.

 그래서 컨트롤러의 생성자에 @Autowired 어노테이션을 붙이므로써, 스프링은 스프링이 저장하고 있는 memberService 객체를 컨트롤러가 생성될 때 가져와 줍니다. 즉, 생성자에 @Autowired 가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어주고, 이렇게 객체 의존관계를 외부에서 넣어주는 것을 DI (Dependency Injection), 의존성 주입이라 합니다. 이전 테스트에서는 개발자가 직접 주입했고, 여기서는 @Autowired에 의해 스프링이 주입해줍니다.

 

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;
    }

}

 

실행하면 아래와 같은 오류가 발생합니다.

Consider defining a bean of type 'hello.hellospring.service.MemberService' in your configuration.

 현재 MemberService 클래스는 현재 순수한 자바 클래스라 빈으로 등록되어 있지 않기 때문에 스프링이 알 수 없습니다. MemberService 클래스에 @Service 어노테이션을 붙여줍니다. 또, MemoryMemberRepository클래스에도 @Repository 어노테이션을 추가합니다.

 

아래처럼 memberController가 생성자에서 memberService 를 @Autowired를 통해 연결했던 것처럼, 서비스 클래스에서도 @Autowired 를 통해 리포지토리를 주입받도록 코드를 수정합니다.

 

반환형만 바꾼 사진입니다. 동작은 동일합니다.

 

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

회원 서비스와 회원 리포지토리의 @Service, @Repository, @Autowired 애노테이션을 제거하고 진행합니다. @Controller은 그대로 유지합니다. 아래 두 사진은 주석처리한 모습입니다.

 

 hello.hellospring 패키지 밑에 service 패키지 안에 SpringConfig.java 파일을 생성합니다. 클래스에 @Configuration 어노테이션을 붙이면, 스프링 프로젝트가 실행될 때, @Configuration 어노테이션을 읽고, 그 안에 @Bean 이 붙여진 것을 보고 스프링 빈에 등록시켜주는 방식으로 진행됩니다.

 

Code

package hello.hellospring.service;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {


    // @Bean : spring bean을 등록할 거라는 의미
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }

    @Bean

    // 반환형을 인터페이스 타입으로 작성함 : 추후 리포지토리 변경 용이
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }

}

 

 과거에는 위와 같이 자바 코드로 빈 등록 관련 설정을 하지 않고, XML 문서를 통해 작성했습니다. 그러나 최근에는 XML로 작성하지 않고, 거의 자바코드로 설정을 합니다. 또, DI에는 필드 주입, setter 주입, 생성자 주입 이렇게 3가지 방법이 있습니다. 의존관계가 실행중에 동적으로 변하는 경우는 거의 없으므로 생성자 주입을 권장합니다. 

 

1. 생성자 주입 방식 

 생성자를 통해서 MemberService가 MemberController에 주입이 됩니다.

 

2. 필드 주입 방식

 스프링이 시작될 때, 초기에만 설정되고 개발자가 동적으로 바꿀 수 없어서 추천하지 않는 방식입니다.

 

3. Setter 주입 방식

 setter은 나중에 호출됩니다. 이 방식은 MemberController 을 호출했을 때, set 메소드가 public으로 열려 있어야 합니다. 어플리케이션 로딩 시점에 한번 세팅되고 나면 이 멤버변수는 바꿀 일이 없는데 public으로 노출되게 된다는 점이 단점입니다.

 

 실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용합니다. 그리고 정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록합니다.(***) 예를들어, 회원 조회, 등록 예제를 이전 포스팅에서 다루었을때, 데이터베이스를 나중에 변경하기로 했었습니다. 그래서 인터페이스와 구현체로 코드를 작성해서 변경에 용이하도록 했습니다. 이렇게 변경에 용이하게 코드를 작성했기 때문에, 나중에 MemoryMemberRepository를 다른 repository 구현체로 바꿔치기를 할 때, 기존 서비스나 컨트롤러 코드를 하나도 변경하지 않고 바꿔치기를 할 수 있습니다. 이것이 여러 코드를 변경해야 하는 컴포넌트 스캔과 차별되는 자바 @Bean 으로 config 설정했을 때의 장점입니다. 물론 컴포넌트 스캔은 작성이 편한 장점이 있습니다.

 

 또 주의할 점은 @Autowired 를 통한 DI는 helloConroller , memberService 등과 같이 스프링이 관리하는 객체에서만 동작합니다. 스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않습니다. 만약, SpringConfig.java 파일에서 @Bean으로 관리되던 모든 메소드를 주석처리하면, MemberService.java 파일에서 @Autowired 어노테이션은 작동되지 않습니다. MemberService가 스프링에 등록되고 스프링이 관리를 해야 스프링이 @Autowired도 적용시켜줄 수 있는 것입니다. 컴포넌트 스캔방식에서는 이 역할을 @Service, @Repository가 하였습니다.

 

Code

package hello.hellospring.service;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {

    // @Bean : spring bean을 등록할 거라는 의미
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository(){
        return new AnotherMemoryMemberRepository(); // 이 부분만 수정하면 됩니다.
    }
}