[Spring Boot] 컴포넌트 스캔과 의존 관계 자동 주입, @SpringBootApplication , RestController

dejeong·2024년 10월 7일

DBMS

목록 보기
6/10
post-thumbnail

컴포넌트 스캔(Component Scan)

빈(Bean)으로 등록할 클래스를 찾는 과정이다. 스프링의 컴포넌트 스캔을 사용하면 자동으로 클래스를 탐색하고 빈으로 등록한다.

@SpringBootApplication //@ComponentScan

컴포넌트 스캔을 사용하지 않고 빈 등록

@Bean 을 사용하는 예시

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

@Configuration // 이 클래스가 스프링 설정 클래스임을 나타
public class MyConfiguration {

    @Bean // 해당 메서드가 빈을 생성하는 메서드임을 나타냄, 빈이 여러 개라면 @Bean 메서드도 여러 개 정의해주어야 한다.
    public MyBean myBean() {
        return new MyBean();
    }
}

XML을 사용하는 예시

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="myBean" class="com.example.MyBean"/>
</beans>

bean> 은 스프링 빈을 등록하는데 사용, id 속성은 빈의 고유 식별자이며, class 속성은 해당 빈을 생성하는 클래스의 경로를 나타낸다. 빈이 여러 개라면 도 여러 개 명시해주어야 한다.

컴포넌트 스캔을 사용하여 빈을 등

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

@Configuration
@ComponentScan(basePackages = "com.example")
public class MyConfiguration {

}

@ComponentScan은 지정된 패키지 (com.example) 및 하위 패키지에서 @Component, @Service 등의 애너테이션이 지정된 클래스를 탐색하여 자동으로 스프링 빈으로 등록 한다.

컴포넌트 스캔 대상

컴포넌트 스캔의 대상이 되며 자동으로 빈으로 등록되는 어노테이션으로 클래스의 용도에 따라 달라진다. @Service, @Repository, @Controller, @Configuration은 내부적으로 @Component를 사용하고 있다.

  • @Component: 일반적인 스프링 빈
  • @Service: 비즈니스 로직을 담당하는 빈
  • @Repository: 데이터 액세스를 담당하는 빈
  • @Controller: Spring MVC 컨트롤러
  • @Configuration: 스프링 설정 정보
import org.springframework.stereotype.Service;

@Service
public class UserService {
    public void addUser() {
    }
}
import org.springframework.stereotype.Repository;

@Repository
public class UserRepository {
    public void saveUser() {
    }
}
import org.springframework.stereotype.Controller;
import com.example.demo.service.UserService;

@Controller
public class UserController {
    
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    public void addUser() {
        userService.addUser();
    }
}

컴포넌트 스캔 옵션

옵션을 사용하여 스캔 대상을 제어할 수 있다.

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

@Configuration
@ComponentScan(basePackages = "com.example")
public class MyConfiguration {

}
  • basePackages : 컴포넌트 스캔을 수행할 패키지의 범위 지정, 만약 “com.example”로 지정했다면 “com.example”을 포함한 하위 클래스들이 스캔 대상이 된다.
  • basePackageClasses : 특정 클래스가 속한 패키지부터 컴포넌트 스캔을 시작
  • includeFilters : 스캔할 대상 추가
  • excludeFilters : 스캔에서 제외할 대상을 지정
/*
	includeFilters와 excludeFilters 옵션을 사용
	
	@MyComponent 애너테이션이 붙은 클래스를 스캔 대상으로 추가하고, 
	“Test”로 끝나는 클래스를 스캔 대상에서 제외
*/

@Configuration
@ComponentScan(
        basePackages = "com.example",
        includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyComponent.class),
        excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*Test"))
public class Config {

}

동일한 클래스가 includeFilters와 excludeFilters에 모두 해당하는 경우 excludeFilters가 우선 순위를 가잔다. excludeFilters에 해당하는 클래스들은 최종적으로 스캔 대상에서 제외된다.

컴포넌트 스캔으로 등록되는 빈의 이름

기본적으로 해당 클래스의 이름을 사용하지만, 명시적으로 빈의 이름을 지정할 수도 있다.

  • 기본 규칙 : 클래스 이름의 첫 글자를 소문자로 변환하여 사용(UserService → userService)
  • 명시적 지정 : @Component, @Service, @Repository, @Controller 등의 애너테이션을 사용하여 명시적으로 빈의 이름을 지정 가능
  • 동일한 이름의 빈이 이미 존재할 경우 : 빈의 이름을 명시적으로 지정 (ex. @Service("hello")) 하여 스프링 에러를 회피할 수 있다.

스프링 부트에서의 컴포넌트 스캔

@SpringBootApplication 어노테이션이 붙은 클래스의 패키지와 하위 패키지를 컴포넌트 스캔 대상으로 지정한다. 해당 어노테이션은 메인 클래스에서 사용한다.

@SpringBootApplication 어노테이션은 내부적으로 @ComponentScan을 포함하고 있지만, 필요에 따라 스캔 대상을 추가하거나 변경해야 할 때에는 @ComponentScan 어노테이션을 직접 사용하여 스캔 대상을 지정할 수 있다.

// MyApplication 클래스의 패키지와 그 하위 패키지를 컴포넌트 스캔 대상으로 지정

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

의존 관계 자동 주입

의존 관계(Dependency)란 내가 변하면 상대방에게도 영향을 끼치는 관계를 의미한다.

// Car 클래스와 Engine 클래스가 의존 관계
public class Car {
    private Engine engine;

    public Car() {
        this.engine = new Engine();
    }
}

의존 관계 주입(Dependency Injection)

의존 객체를 직접 생성하는 것이 아니라 외부에서 해당 의존 객체를 주입받는 것

// 의존 객체를 직접 생성하는 방법
public class Car {
    private Engine engine;

    public Car() {
        // 의존 객체를 직접 생성
        this.engine = new Engine();
    }
}
// 의존 객체를 주입받는 방법
public class Car {
    private Engine engine;

    // 생성자를 통해 의존 객체를 주입받음
    public Car(Engine engine) {
        this.engine = engine;
    }
}

생서자를 통해 의존 객체를 주입받는 방법은 어떤 엔진을 사용할지 결정하는 권한을 외부에 위임하는 것과 같다. 스프링에는 빈(Bean)이라는 객체들이 있고 빈도 서로가 서로를 의존하고 있다. 빈의 의존 관계를 스프링에서 관리하는데, 이 때 의존 관계 주입(Dependency Injection) 방식을 사용하고 있으며, 스프링과 빈의 의존 관계가 이상하다면 오류를 발생시키기도 한다.

의존 관계 자동주입

일반적으로 불변 보장, 테스트 용이성, 순환 참조 방지를 위해 생성자 주입 방식을 권장한다.

  • 생성자 주입 : 생성자를 통해 객체를 주입, 여러 개의 생성자가 있는 경우 스프링에서는 @Autowired 어노테이션이 붙은 생성자를 사용하며, 생성자가 여러 개가 아니라면 해당 어노테이션을 생략할 수 있다.
public class MyClass {
    private MyDependency myDependency;

    // @Autowired가 없는 생성자
    public MyClass(String message) {
        System.out.println("문자열을 받는 생성자 호출: " + message);
    }

    // @Autowired가 있는 생성자
    @Autowired
    public MyClass(MyDependency myDependency) {
        this.myDependency = myDependency;
        System.out.println("의존 객체를 받는 생성자 호출");
    }
}
package com.estsoft.springprojecttest.controller;

import com.estsoft.springprojecttest.interf.InterDependencyService;
import com.estsoft.springprojecttest.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
//    @Autowired
    private HelloService helloService; // Dependency Injection (DI)
    private InterDependencyService interDependencyService;

    public HelloController(HelloService helloService, InterDependencyService interDependencyService) { // 생성자 주입 Autowired 지우고 작성, 위 코드랑 의도하는 바는 같음
        this.helloService = helloService;
        this.interDependencyService = interDependencyService;
    }
    @GetMapping("/hello") // 특정 패턴이 왔을 때 처리할 수 있도록
    // http://localhost:8080/hello?param=jo
    public String hello(@RequestParam(value = "param", defaultValue = "Spring") String param){
        // 객체 직접 생성, 호출
//        HelloService helloService = new HelloService();
//        return helloService.printHello(param);
        interDependencyService.printMethod();

        // Spring 에게 제어권 맡기기(DI 사용해서)
       return helloService.printHello(param);
    }
}
  • 필드 주입 : 멤버 변수에 @Autowired 어노테이션을 추가하면 자동으로 의존 관계가 주입된다.
public class MyClass {
    @Autowired
    private MyDependency myDependency;
}
  • Setter 주입
public class MyClass {
    private MyDependency myDependency;

    @Autowired
    public void setMyDependency(MyDependency myDependency) {
        this.myDependency = myDependency;
    }
}

불변 보장

  • 한 번 의존 관계 주입이 되면 프로그램이 종료되기 전까지 변경되지 않아야 안전하다.
  • Setter 주입 방식의 경우 setter 메서드를 통해 주입된 객체를 수정할 수 있기 때문에 적합하지 않다.
  • final 키워드와 함께 사용하면 주입된 객체가 불변함을 완벽하게 보장할 수 있다. 필드, Setter 방식은 생성자 호출 이후에 의존 관계를 주입하는 방식이므로 final 키워드를 사용할 수 없다.
public class MyClass {
    // final 키워드 사용
    private final MyDependency myDependency;

    public MyClass(MyDependency myDependency) {
        this.myDependency = myDependency;
    }
}

테스트 용이성

  • 필드, Setter 방식은 @Autowired 애너테이션을 사용하여 의존성을 주입하지만, @Autowired는 스프링 프레임워크에서만 사용하는 어노테이션이기 때문에 스프링이 아닌 다른 곳에서 객체를 활용하기 어렵다.

순환 참조 방지

  • 순환 참조란 A 객체가 B 객체를 참조하고, B 객체가 다시 A 객체를 참조하는 상황으로 두 객체가 서로를 무한히 호출한다면 프로그램이 무한 루프에 빠질 수 있으며 비정상 종료될 수 있다.
  • 필드, Setter 방식은 프로그램 구동 시점에는 의존 객체를 null로 유지하다가 객체를 실제로 사용하는 시점에 주입함으로 실제 객체를 사용하기 전까지는 스프링에서 순환 참조를 감지하지 못한다.
  • 생성자 방식은 의존 객체가 주입되는 시점이 해당 객체가 생성될 때임으로, 스프링이 빈을 등록하려면 생성자를 필수적으로 호출해야하므로 프로그램 구동 시점에 순환 참조를 감지할 수 있다. 실제로 생성자 방식을 사용하면 스프링에서 순환 참조를 감지하고 The dependencies of some of the beans in the application context form a cycle 와 같은 에러를 발생시킨다.

@SpringBootApplication

스프링 부트 애플리케이션을 구성하기 위한 어노테이션

  • Auto-configuration: 스프링 부트는 애플리케이션의 의존성을 기반으로 필요한 빈(Bean)을 자동으로 구성한다.
  • Component Scanning: 스프링 부트 애플리케이션의 패키지를 스캔하여, 빈으로 등록할 수 있는 클래스를 찾는다.
  • Main method: 스프링 부트 애플리케이션의 시작점으로 메인 메소드를 제공한다.
@SpringBootApplication
public class SpringBootDeveloperApplication {
	public static void main(String[] args) {
		SpringApplication.run(SpringBootDeveloperApplication.class, args);
	}
}

자바의 main() 메서드와 같은 역할을 한다. 이 어노테이션을 추가하면 스프링 부트 사용에 필요한 기본 설정을 해준다. SpringApplication.run() 메서드는 애플리케이션을 실행하는데, 이 메서드의 첫 번째 인수는 스프링 부트 애플리케이션의 메인 클래스로 사용할 클래스, 두 번째 인수는 커맨드 라인의 인수들을 전달한다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration  // 스프링 부트 관련 설정
@ComponentScan(...)
@EnableAutoConfiguration  // 자동으로 등록된 빈을 읽고 등록 
public @interface SpringBootApplication {
	...
}

@ComponentScan

사용자가 등록한 빈을 읽고 등록하는 어노테이션으로 @Component라는 애노테이션을 가진 클래스들을 찾아 빈으로 등록해주는 역할을 하지만, 모든 빈에 @Component만 사용하는건 아니다. 해당 어노테이션을 감싸는 어노테이션이 있으며 실제 개발을 하면 용도에 따라 아래와 같은 어노테이션으 사용한다.

어노테이션명설명
@Configuration설정 파일 등록
@RepositoryORM 매핑
@Controller, @RestController라우터
@Service비즈니스 로직

@EnableAutoConfiguration

스프링 부트에서 자동 구성을 활성화하는 어노테이션으로 스프링 부트 서버가 실행될 때 스프링 부트의 메타 파일을 읽고 정의된 설정들을 자동으로 구성하는 역할을 한다. spring.factories 파일에 클래스들이 모두 @EnableAutoConfiguration을 사용할 때 자동 설정된다.


RestController

라우터 역할을 하는 어노테이션으로 라우터란 HTTP 요청과 메서드를 연결하는 장치를 말한다. 해당 어노테이션이 있어야 클라이언트의 요청에 맞는 메서드를 실행할 수 있다.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
	@GetMapping("/hello")   // GET /hello 요청이 들어오면 test() 메서드 실행
	public String test() {
		return "Nice to meet you";
	}
}

HelloController를 라우터로 지정해 /hello 라는 HTTP GET요청이 왔을 때 test() 메소드를 실행하도록 구성한다.

@Controller 어노테이션에서 @Component 어노테이션을 가지고 있었기 때문에 @Controller 어노테이션이 @ComponentScan을 통해 빈으로 등록된다. @Configuration, @Repository, @Service 어노테이션 모두 @Component 어노테이션을 가지고 있다. → 빈이 무슨 역할을 하는지 명확하게 구분하기 위해 다른 이름으로 덮어두었다.

interface 타입으로 의존성 주입(DI)할 때 구현체 지정하는 방법

  • @Qualifier로 어떤 구현체를 가르키고 있는지 선언(빈 이름 지정)
  • 실행시키고 싶은 구현체에 @Primary 를 붙여준다. 생성자 주입을 할 때 해당 어노테이션을 보고 최우선이라는 것으로 생각 후 주입시켜줌
package com.estsoft.springprojecttest.interf;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service
public class InterDependencyService {
    private final Inter inter;

    // interface 타입으로 의존성 주입(DI)할 때 구현체 지정하는 방법 2가지
    // (단, interface의 구현체가 하나일 경우에는 구현체 지정하지 않아도 DI 가능)
    // 1. @Qualifier ("빈 이름(구현체) 지정") ex.@Qualifier("interImplA")
    // 2, @Primaty

    public InterDependencyService(@Qualifier("interImplA") Inter inter) {
        this.inter = inter;
    }

    public void printMethod() {
        inter.method();
    }
}
package com.estsoft.springprojecttest.interf;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

@Primary
@Service
public class InterlmplA implements Inter{
   @Override
    public void method(){
       System.out.println("Hi A");
   }
}
package com.estsoft.springprojecttest.interf;

import org.springframework.stereotype.Service;

@Service
public class InterlmplB implements Inter{
    @Override
    public void method(){
        System.out.println("Hi B");
    }
}
profile
룰루

0개의 댓글