본문 바로가기
개발/스프링

[Spring] Bean 등록 및 의존 관계 설정하기

by 카펀 2022. 3. 9.

개요

Spring에서는 객체를 bean (빈)이라는 이름으로 부릅니다.

이러한 bean은 Spring이 시작할 때 Spring container에 등록되어, Spring에 의해 관리되고 쓰여집니다.

 

앞서 스프링의 MVC 구조에 의해 언급했었는데요.

View에 원하는 내용을 그리기 위해서는 Controller를 작성해야 합니다.

Controller는 Service에 작성된 메소드를 호출하여, 요구되는 역할을 수행합니다.

Service는 Repository에 접근하여, 데이터를 읽어 오거나, 저장 및 수정합니다.

 

만약 특정 사이트의 상품 주문 기능이 있다면, Controller는 View를 이용해 화면을 나타내는 역할을 합니다.

Controller는 Service를 이용해서 실제 주문을 수행하고, Service는 Repository를 통해 데이터를 조회 또는 저장합니다.

 

이렇게 Controller, Service, Repository는 서로 의존 관계에 있다고 할 수 있는데요.

이걸 Spring이 알아서 파악해 주는 것은 아니고, '각각 이러한 관계에 있다' 고 개발자가 나타내 주어야 합니다.

개발자가 표시만 해 주면, 나머지는 Spring이 필요할 때 알아서 호출해 주는 것이죠.

 

Spring bean을 등록하는 방법은 크게 두 가지가 있습니다.

  1. 어노테이션 이용
  2. 직접 등록하기

 

1. 어노테이션 이용

우선 Controller를 생성합니다.

아래와 같이 클라스를 만들고, 위에 @Controller 어노테이션을 붙여 줍니다.

 

@Controller
public class PurchaseController {

}

 

위 코드를 보면 Controller 내에 구현된 기능은 하나도 없지만, Spring은 이 클라스를 컨트롤러로 인식합니다.

위에 붙은 @Controller 어노테이션 덕분입니다.

Spring 최초 실행 시, Spring Container가 어노테이션을 확인하고, MemberController 객체를 생성하여 해당 클라스를 컨테이너에 추가해 줍니다.

이후 Spring이 관리를 해 주는 것이죠. 이것을 'Spring Bean이 관리된다'고 표현합니다.

 

Controller가 Service를 사용할 수 있도록 추가해 줍니다.

 

직접 생성

@Controller
public class PurchaseController {

    private final PurchaseService purchaseService = new PurchaseService();

}

 

Spring Container에서 연결

@Controller
public class PurchaseController {

    private final PurchaseService purchaseService;

    //스프링 컨테이너에 등록
    @Autowired
    public PurchaseService(PurchaseService purchaseService) {
        this.purchaseService = purchaseService;
    }
}

상단처럼 새 Service 객체를 생성해도 되지만, Spring을 사용하는 이상, Spring이 관리해 주도록 설정하는 것이 바람직합니다.

Spring Container에 등록하고, 필요할 때 컨테이너로부터 받아서 사용하는 것이죠.

이렇게 하면 여러 번 생성되는 일 없이, 한 번만 생성되어 여러 번 사용하게 됩니다. 여러 번 사용하도록 Spring이 관리해 주는 것이고요.

 

하단처럼 @Autowired 어노테이션을 사용하여 의존 관계를 설정할 수 있습니다.

Autowired란, 자동으로 연결이 설정되었다는 뜻입니다.

이 뜻 그대로, 위 어노테이션을 사용하면, Spring이 Controller와 Service 사이의 연결 관계를 자동으로 설정해 줍니다.

 

정확히는 @Autowired 어노테이션을 발견하면, Spring은 컨테이너에서 해당 Service를 찾습니다.

이 때 Spring이 찾아지려면, 당연히 Service 역시 Spring Container에 등록이 되어 있어야겠죠?

@Service
public class PurchaseService {

    private final PurchaseService purchaseService;

    @Autowired
    public PurchaseService(PurchaseService purchaseService) {
    this.purchaseService = purchaseService;
}

Service는 @Service 어노테이션을 통해 Container에 등록시켜 줄 수 있습니다.

 

자, 앞서 Service는 Repository를 통해 데이터를 저장하거나 읽어 온다고 했습니다.

그렇다면 Service와 Repository 사이에도 의존 관계를 설정해 주어야 합니다.

 

Service에도 위처럼 생성자를 통해 Repository를 추가해 줍니다.

마찬가지로 생성자 위에는 @Autowired 어노테이션을 붙여 주어야 합니다.

@Repository
public class MemoryPurchaseRepository implements PurchaseRepository {
	...
}

 

그리고 Repository 역시 마찬가지로 @Repository 어노테이션을 사용하여 Spring Container에 등록시켜 줍니다.

 

위와 같이 어노테이션을 이용하여 등록하는 방식을 'Component 스캔 방식' 이라고 합니다.

Component는 '구성 요소'라는 뜻을 가지고 있는데요.

 

@Controller, @Service, @Repository의 구현체입니다.

보시면 전부 위에 @Component라는 어노테이션이 붙어 있습니다.

Spring은 시작될 때 @Component 및 관련 어노테이션 (Controller, Service, Repository)이 붙어 있는 클라스를 탐색하여, Spring Container에 추가합니다.

이렇게 추가된 Spring bean을 @Autowired를 통해 연결해서 사용하는 것이죠.

 

그렇다면 Spring은 어디까지 탐색을 하고 Spring Container에 추가를 할까요?

 

Spring Boot를 시작하는 @SpringBootApplication 어노테이션이 붙어 있는 Application 클라스를 보겠습니다.

 

위에 package com.tistory.katfun.intro; 라는 코드가 보이시나요?

저 패키지 및 해당 패키지의 하위 패키지 내에 있는 클라스들에 대해서만 탐색을 합니다.

즉 com.tistory.katfun.intro 바깥에 있는 클라스에 대해서는 기본적으로는 스캔을 하지 않습니다.

(@SpringBootApplication 어노테이션의 구현체를 보면 @ComponentScan이라는 어노테이션에 정의되어 있습니다.)

 

더불어, Spring Container에 등록되는 각 Spring bean은 '싱글 톤'으로 등록됩니다.

이게 무슨 말이냐 하면, 각 bean마다 '유일하게 하나만 등록해서 공유한다'는 뜻입니다.

따라서 같은 Spring bean이 여러 번 등록되어도 등록은 하나만 되고, 같은 spring bean이면 모두 같은 인스턴스입니다.

물론 이것 역시 싱글 톤이 아니도록 설정할 수 있지만, 사실상 그럴 일은 없다고 볼 수 있습니다.

 

2. 직접 등록하기 (Java 코드 이용)

앞에서 컴포넌트 스캔을 이용하여 Spring Bean을 Spring Container에 등록했는데요.

이렇게 하지 않고, 직접 코드를 작성하여 등록하는 방법 역시 존재합니다.

 

등록하는 방법은 XML, Java 등 다양한 방법이 존재합니다.

최근에는 XML보다는 Java로 작성하는 추세이므로, 여기서는 Java 코드로 작성하는 방법을 소개하도록 하겠습니다.

 

Controller, Service, Repository는 1번에서의 상황과 같다고 가정하겠습니다.

SpringConfig이라는 클라스를 만들고, 아래와 같이 작성합니다.

 

@Configuration
public class SpringConfig {

    @Bean
    public PurchaseService purchaseService() {
        return new PurchaseService(purchaseRepository());
    }

    @Bean
    public PurchaseRepository purchaseRepository() {
        return new MemoryPurchaseRepository();
    }
}

 

Controller만은 앞의 경우와 마찬가지로 @Controller 어노테이션을 붙여 줘야 합니다.

따라서 생성자 역시 마찬가지로 @Autowired 어노테이션을 붙여 줘야 합니다.

 

@Configuration은 Bean을 수동으로 등록하는 것을 의미하는 어노테이션입니다.

내부에는 @Bean 어노테이션을 붙인 메소드마다, 각각 개별 Spring Bean을 등록하는 내용을 작성합니다.

 

PurchaseRepository는 의존하는 다른 bean이 없습니다.

따라서 별도의 인자 없이 생성해 주면 됩니다.

PurchaseService는 PurchaseRepository에 의존하는 bean입니다.

따라서 인자에 purchaseRepository()를 넣어 주면, Spring이 시작하면서 등록된 purchaseRepository를 purchaseService 생성 시에 넣어 줍니다 (@Autowired와 비슷).

 

각 방법의 장점

Component 스캔 방식은 구현이 훨씬 간편합니다. 특정 설정 파일을 만들 필요 없이, 각 클라스의 앞에 적절한 어노테이션을 붙여 주면 되고, 코드의 가독성도 상승합니다.

@Controller가 붙어 있으면 컨트롤러임을 쉽게 알아볼 수 있겠죠.

이런 이유 탓에, 실무에서는 주로 정형화된 Controller, Service, Repository는 컴포넌트 스캔 방식으로 구현합니다.

 

직접 Bean을 등록하는 방식은 실무에서는 주로 정형화되지 않은 Controller, Service, Repository의 경우, 또는 상황에 따라 구현 클라스를 변경해야 하는 경우 사용합니다.

만약 Repository의 내용을 변경해야 하는 경우라면, Component 스캔 방식은 여러 곳의 코드를 수정해 주어야 합니다.

하지만 직접 Bean을 등록하는 방식은, 앞서 본 것처럼 별도의 설정 파일을 둡니다. 때문에 설정 파일만 수정해 주면 간단하게 구현 클라스를 변경할 수 있습니다.

 

위 두 경우를 모두 알고 이해하고, 필요에 따라 적합한 방법을 사용하는 것이 좋겠습니다.

 

기타

관련된 내용이지만 위에서 언급하지 않은 내용을 추가로 작성하였습니다.

 

Spring을 사용하면, 웬만한 객체는 전부 Spring bean으로 등록해서 쓰는 것이 좋습니다. (관련 내용을 자세하게 별도 포스팅으로 작성 예정)

 

Spring container에 등록할 때 사용되는 자료구조는 map입니다. 덕분에 빠른 탐색이 가능합니다.

(Hashmap - O(1), Treemap - O(log n). Spring이 정확히 어떤 map을 사용하는지는 추후 내용을 추가하도록 하겠습니다.

 

*이 글은 인프런 - 김영한 님의 '스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술' 강의를 보고 정리한 내용입니다.

댓글