Java Category/Spring

[Spring 입문] 스프링 빈과 의존관계

ReBugs 2024. 1. 22.

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


아래의 글에 연관이 있는 포스팅입니다.

2024.01.21 - [Java Category/Spring] - [Spring 입문] 회원 관리 예제 - 백엔드 개발

회원 가입하고 그 결과를 html을 통해 화면에 띄우려고 하는데, 그러러면 Controller와 View template가 필요하다.

MemberController를 만들어서 사용해야 하는데, MemberController가 MemberService를 통해서 회원 가입을 하고, 데이터 조회를 해야 한다.

이런 것을 "MemberController가 MemberService를 의존한다" 라고 표현한다.

따라서 회원 컨트롤러가 회원서비스와 회원 리포지토리를 사용할 수 있도록 의존관계를 설정하는 것을 의존성 주입(Dependency Injection)이라고 한다.

스프링 빈과 의존성 주입과의 관계
의존성 주입(Dependency Injection, DI)과 스프링 빈과의 관계는 밀접하게 연관되어 있습다.
스프링은 IoC(Inversion of Control) 컨테이너를 제공하여 객체의 생명주기와 의존성을 관리한다.
그 중에서도 DI는 스프링에서 객체 간의 의존성을 해결하고 관리하기 위한 주요 기능 중 하나이다.

스프링 빈(Bean)
스프링 컨테이너가 관리하는 자바 객체를 뜻하며, 하나 이상의 빈(Bean)을 관리한다.
빈은 인스턴스화된 객체를 의미하며, 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 한다.
스프링 빈은 IoC 컨테이너에 의해 생성, 관리되며, 스프링 빈을 만들려면 해당 클래스에 @Component, @Service, @Repository, @Controller 등과 같은 어노테이션을 부여하거나 XML로 설정하는 방식도 있지만 최근에는 잘 사용하지 않는다.

의존성 주입(Dependency Injection)
의존성 주입은 한 객체가 다른 객체에 의존할 때, 그 의존성을 외부에서 주입하는 디자인 패턴이다.
스프링에서는 주로 생성자 주입, 필드 주입, 메서드 주입 등의 방식으로 의존성을 주입한다.
주입 받을 객체는 스프링에서 관리되는 빈이어야 한다.

@Service // 스프링 빈으로 등록
public class MyService {

    private final MyRepository myRepository;

    // 생성자 주입
    @Autowired
    public MyService(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    // ...
}​

위의 코드에서 MyService는 @Service 어노테이션으로 스프링 빈으로 등록되었고, MyRepository 타입의 의존성이 생성자 주입을 통해 주입되었다.
스프링 IoC 컨테이너는 MyService를 생성할 때 필요한 MyRepository 빈을 찾아서 주입한다.

이렇게 함으로써 스프링은 개발자가 객체 간의 의존성을 관리하고, 런타임에 의존성을 주입함으로써 코드의 유연성과 테스트 용이성을 높일 수 있도록 도와준다.

Inversion of Control (IoC)
소프트웨어 디자인 패턴 중 하나로, 제어의 역전이라고도 불린다.
이 패턴은 프로그램의 흐름이 개발자가 작성한 코드가 아니라 외부 컨테이너에 의해 결정된다는 개념을 기반으로 한다.
일반적으로 프로그램의 제어 흐름은 개발자가 코드를 작성하여 정의한다.
그러나 IoC에서는 제어 흐름의 일부 또는 전부가 외부의 프레임워크나 컨테이너에 의해 관리되고 결정된다.
이는 주로 의존성 주입(Dependency Injection)이라는 형태로 나타난다.
IoC의 핵심 아이디어는 개발자가 코드를 작성할 때가 아니라, 프레임워크 또는 컨테이너가 애플리케이션의 객체들을 생성하고 관리할 때이다. 이렇게 하면 객체 간의 결합도가 낮아져 유연성과 확장성이 증가하며, 테스트하기 쉬운 코드를 작성할 수 있게 된다.
Spring 프레임워크는 IoC의 원칙을 따르는 대표적인 프레임워크 중 하나이다.
Spring IoC 컨테이너는 객체의 생성, 초기화, 소멸, 의존성 주입 등을 관리하고, 애플리케이션의 제어 흐름을 개발자에게서 컨테이너로 역전시킨다.
이는 코드의 모듈화와 재사용성을 높이며, 유지보수가 용이한 시스템을 구축하는데 도움이 된다.

 

일단은 컨트롤러를 추가하자

package hello.hellospring.controller;
import org.springframework.stereotype.Controller;
@Controller
public class MemberController {

}
@Controller
spring이 실행될 때, spring container가 생긴다.
그 때, @Controller이 적혀있다면 spring container에 해당 객체를 넣어두고, spring이 관리를 한다.
이후 Controller가 필요할 때 container에서 지정된 Controller를 사용한다.
이를 "spring container에서 spring bean이 관리된다" 라고 표현한다.

 

스프링 빈을 등록하는 방법은 아래와 같이 2가지 방법이 있다.

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

 

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

Autowired을 이용하여 MemberController와 MemberService 연결

MemberController가 MemberService를 사용해야 하는데, new 키워드를 이용하여 MemberService 객체를 생성하여 사용할 수도 있다.
하지만 MemberService를 spring container에 등록을 해서 spring이 관리를 하도록 해야한다. spring container에 등록하면 하나의 객체만 등록이 된다. (singleton 패턴)
이렇게 해야 다른 Controller에서도 MemberService를 가져와 사용할 수 있다.

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

MemberService를 spring container에 연결하는 방법은 아래와 같다.

  1. MemberController 클래스에서 memberService 클릭 - Alt+Enter - Add Constructor Parameter를 선택한다.
  2. 생성자에 @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;
    }
}

 

여기까지 했다면 MemberService를 spring container에 연결은 했지만, memberService가 아직 스프링 빈으로 등록되어있지 않다.

@Autowired 어노테이션이 있다면 spring container에서 해당 객체를 찾은 후 연결시켜준다.

그러려면 MemberService가 spring container에 스프링 빈으로 등록이 되어 있어야 한다.

하지만 MemberService는 순수한 java class이기 때문에 spring bean으로 등록되어 있지 않다.

 

어노테이션을 통해 Spring Container에 등록하기

@Service와 @Repository 어노테이션을 통해서  spring container에 등록해야 한다.
먼저 MemberService 클래스에 가서 @Service 어노테이션을 붙여준다.
그리고 MemoryMemberRepository에는 @Repository 어노테이션을 붙여준다.
여기까지 하면 MemberController, MemberService, MemoryMemberRepository가 spring container에 등록된다.

추가적으로 MemberService의 생성자를 살펴보면, MemberRepository를 할당받는데 @Autowired를 통해 spring container에 등록된 MemoryMemberRepository 객체를 연결시켜주어야 한다.

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

Controller, Service, Repository는 정형화 되어 있는 패턴이다.

Controller를 통해서 외부 요청을 받고, Service에서 비지니스 로직을 만들고, Repository에서 데이터를 저장하는 패턴이다.

 

MemberController

@Controller
public class MemberController {
    private final MemberService memberService;

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

 

MemberService

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    ...
}

 

MemoryMemberRepository

@Repository
public class MemoryMemberRepository implements MemberRepository{
    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;
    ...
}

 

컴포넌트 스캔

@Component 어노테이션이 있으면 스프링 빈으로 자동 등록된다.

@Controller 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문이다.

@Component 를 포함하는 다음 어노테이션도 스프링 빈으로 자동 등록된다

  • @Controller
  • @Service
  • @Repository
컴포넌트 스캔 시, 객체는 기본적으로 싱글톤(singleton)으로 등록하고, 하나의 객체를 공유한다.
컴포넌트 스캔은 기본적으로 main 메서드가 있는 package의 하위 package들만을 스캔하고 등록한다.

즉, Component와 관련된 어노테이션이 있으면, 해당 객체를 모두 하나씩 생성해서 spring container에 등록을 하고, @Autowired를 통해 객체들을 연결해준다.

 

자바 코드로 직접 스프링 빈 등록

컨트롤러는 아래와 같이 되어있어야 한다.

@Controller
public class MemberController {
    private final MemberService memberService;
    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}

 

service 패키지에 SpringConfig 클래스를 생성하고, @Configuration 어노테이션을 추가한다.



여기서 @Configuration은 설정 파일을 만들거나 Bean을 등록하기 위한 어노테이션이다.

@Bean을 사용하는 클래스의 경우 반드시 @Configuration을 같이 사용해야 한다.

@Configuration
public class SpringConfig {

}

 

SpringConfig 클래스에 다음과 같은 코드를 작성한다.

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

@Bean은 spring bean으로 등록한다는 뜻이다.
이렇게 하면 @Configuration에서 @Bean을 읽고, spring bean에 등록하라는 뜻이라고 인식한다.
그리고 return 값을 통해 받은 MemberService를 spring bean에 등록을 해준다.

MemberService의 생성자는 MemberRepository 인터페이스 변수를 매개변수로 받아야 한다.
따라서 MemberRepository인터페이스 타입의 구현 객체를 반환하는 함수도 spring bean에 등록해야 한다.

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

이제 MemoryMemberRepository(인터페이스 구현 객체)를 반환하는 함수 memberService를 생성했다.

다음으로 MemberService의 생성자를 수정해야 한다.

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

이렇게 하면MemberService의 생성자로 전달한다.
최종적으로 spring bean에 정상적으로 MemberService 가 등록된다.

 

@Configuration
public class SpringConfig {

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

    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
}
정리
1. @Configuration 어노테이션을 통해서 @Bean 어노테이션이 있는 것(memberService(), memberRepository())들을  spring bean에 등록하게 된다.
2. 컨트롤러의 생성자가 MemberService 객체를 요구한다.
3. memberRepository() 메소드에 의해서 구현 객체가 리턴되고, 리턴된 객체를 memberService()의 리턴값으로 전달하므로써 생성자에 @Autowired 어노테이션으로 연결한 것과 동일한 효과가 난다.
4. memberService()가 MemberService 객체를 컨트롤러의 생성자에 전달하므로써 최종적으로 스프링 컨테이너에 아래와 같은 스프링 빈들이 등록된다.

 

의존성 주입(DI)의 세 가지 방법

Field 주입

멤버 변수의 앞에 @Autowired 어노테이션을 붙인 모양이다. 
하지만 이 방법을 사용하면 IntelliJ에서도 다른 방법을 추천하며, 별로 추천하지 않는 방법이다.
필드 주입은 spring이 처음 실행할 때에만 spring container에서 받아와서 넣어주고, 중간에 값을 바꿀 수 있는 방법이 없다.

@Autowired private MemberService memberService;

 

Setter 주입

이 방법의 단점은 setter가 public으로 설정되어 있기 때문에, 누군가 MemberController를 호출 했을 때 의도치 않게 MemberService를 수정할 수 있다는 점이다.
spring bean은 대부분 한 번 설정되고 나면, 이후에 수정할 일이 거의 없는데 public으로 노출되어 있어 문제가 생길 가능성이 있다.

@Controller
public class MemberController {
    private MemberService memberService;

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

 

Constructor 주입

최근에는 생성자(Constructor)를 이용한 주입을 권장한다.
생성자를 이용한 주입을 하게 되면, 처음 어플리케이션이 조립된다고 표현하는 기점(spring bean이 세팅되는 시점)에 Autowired를 통해서 한 번 할당되고 끝난다.
조립 시점에 생성자를 통해 한 번만 할당을하고 마무리가 되기 때문에 public으로 설정되어 있는 setter보다 안전하다.

@Controller
public class MemberController {
    private final MemberService memberService;

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

 

의존 관계가 Runtime 중에 동적으로 변하는 경우는 거의 없으므로(사실 아예 없다) 생성자 주입을 권장한다.
만일 정말 의존 관계를 바꿔야 하는 상황이라면, SpringConfig 파일을 수정하고 서버를 다시 올리는 것이 바람직하다.
실무에서는 주로 정형화된 Controller, Service, Repository 같은 코드는 컴포넌트 스캔을 사용한다.
정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정(SpringConfig)를 통해 spring bean으로 등록한다.

댓글