Spring, Apache, Java

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

cwchoiit 2023. 10. 16. 09:20
728x90
반응형
SMALL
728x90
반응형
SMALL

Spring을 사용하면 중요하게 알아두어야 할 것이 '컴포넌트 스캔'이란 단어다. 이게 무엇인지 공부한 내용을 작성해보고자 한다.

우선 Spring 코드를 보면 이런 어노테이션이 많이 보인다.

@Service
@RequiredArgsConstructor
public class FileStoreImpl implements FileStore {}
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/file")
public class FileController {}

@Service, @RestController, @Controller, @Component, @Repository 등 자주 보이는 어노테이션이 있는데 이게 무얼 의미하는지 알아야한다.

 

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

Spring이 아닌 기존의 자바 코드를 생각해보면, 어떤 임의의 클래스에서 다른 클래스의 객체를 불러올 때 이러한 방식으로 코드를 작성한다.

public class FileController {
	private final FileService fileService = new FileService();
}

해당 클래스에서 'new'를 사용해서 인스턴스화 하곤했는데, 이러한 객체를 불러오는 것을 스프링은 대신 작업해준다. 더 많은 이점을 가지고. 그것이 컴포넌트 스캔과 자동 의존관계 설정이다.

 

그럼 어떻게 하는지 알아보자.

@Service
public class FileService {

}

위 코드처럼 @Service라는 어노테이션을 특정 클래스에 추가하면 스프링은 스프링이 띄워질 때 이 클래스를 스프링 컨테이너에 유일하게 하나의 객체로 만들어 보관한다. 그래서 이 클래스가 스프링이 관리하는 하나의 컴포넌트가 되고 그 컴포넌트를 스프링이 띄워질 때 모두 찾아내는 걸 컴포넌트 스캔이라고 하는데 왜 컴포넌트일까? @Service 어노테이션 안으로 들어가보면 이와 같다. 

/*
 * Copyright 2002-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.stereotype;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;

/**
 * Indicates that an annotated class is a "Service", originally defined by Domain-Driven
 * Design (Evans, 2003) as "an operation offered as an interface that stands alone in the
 * model, with no encapsulated state."
 *
 * <p>May also indicate that a class is a "Business Service Facade" (in the Core J2EE
 * patterns sense), or something similar. This annotation is a general-purpose stereotype
 * and individual teams may narrow their semantics and use as appropriate.
 *
 * <p>This annotation serves as a specialization of {@link Component @Component},
 * allowing for implementation classes to be autodetected through classpath scanning.
 *
 * @author Juergen Hoeller
 * @since 2.5
 * @see Component
 * @see Repository
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {

	/**
	 * The value may indicate a suggestion for a logical component name,
	 * to be turned into a Spring bean in case of an autodetected component.
	 * @return the suggested component name, if any (or empty String otherwise)
	 */
	@AliasFor(annotation = Component.class)
	String value() default "";

}

실제로 @Service 어노테이션에 들어와보면 @Component 라는 어노테이션이 있다. 이 때문에 @Service는 @Component 어노테이션을 붙인것과 같이 컴포넌트 스캔이 가능한 것.

 

그럼 스프링이 띄워질 때 스프링 컨테이너에 자동으로 컴포넌트들을 다 찾아서 등록해주면 이제 우리는 그 객체를 불러다가 사용만 하면 된다. 그 방식이 다음과 같은 방식이다.

@Controller
public class FileController {
	private final FileService;
    
    @Autowired
    public FileController(FileService fileService) {
    	this.fileService = fileService;
    }
}

이 코드를 보면 FileService를 final로 선언하고 초기화 하지 않은 상태에서 생성자에서 FileService를 초기화 해주는데, @Autowired라는 어노테이션이 보인다. 이 어노테이션은 스프링이 자동으로 스프링 컨테이너에서 보관하고 있는 FileService라는 객체를 이 클래스에 주입해준다는 의미다. 이게 자동 의존관계 설정이다.

 

그리고 저렇게 생성자가 딱 하나만 존재하는 경우 @Autowired 어노테이션은 생략할 수 있다. 

아래 코드가 그 예시이고, 이렇게 작성해도 스프링이 FileService를 자동 주입해준다. @Autowired가 생략된 것.

@Controller
public class FileController {
	private final FileService;
    
    public FileController(FileService fileService) {
    	this.fileService = fileService;
    }
}
참고로 @Controller 어노테이션 역시 스프링이 스프링 컨테이너에 객체로 보관해주는 컴포넌트 스캔이 일어난다. 이 @Controller 역시 들어가보면 @Component 라는 어노테이션이 들어있다.

요즘은 @Autowired 보다 더 간단하게 스프링한테 의존성 주입을 맡길 수 있는데 그 코드는 다음과 같다.

@Controller
@RequiredArgsConstructor
public class FileController {
	private final FileService;
    
}

여기서는 생성자도 필요없다. @RequiredArgsConstructor라는 어노테이션을 붙이면 이 어노테이션이 이 클래스가 반드시 가져야하는 인스턴스를 자동으로 주입해준다. 위에보다도 코드가 더 간결해졌다. 이렇게 스프링이 대신 의존관계를 주입해주면 가지는 여러 이점이 있지만 우선 그 중 하나는 유일하게 하나의 객체만을 만들어 사용하는 싱글톤 패턴 방식으로 동작하게 해준다. 

 

그럼 컴포넌트 스캔은 아무곳에 어떤 클래스나 상관없이 @Component라는 어노테이션만 붙으면 가능할까? 그렇지 않다.

어떻게 스프링이 스프링 컨테이너에 객체를 보관하냐면 스프링이 띄워질 때 가장 최초의 시작점인 @SpringBootApplication 어노테이션이 붙은 메인 클래스를 보자.

package com.example.server;

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

@SpringBootApplication
public class ServerApplication {

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

}

이 클래스를 보면 패키지가 com.example.server로 되어 있다. 스프링은 기본으로 이 패키지 하위에 있는 모든 패키지들만을 찾아 컴포넌트 스캔을 진행한다. 물론 다른 패키지에 있어도 컴포넌트 스캔을 설정하는 방법은 있지만 기본으로 이 메인 클래스의 패키지 하위에 있는 모든 패키지 안에서만 컴포넌트 스캔이 일어난다. 

 

 

 

이렇게 사전에 스프링이 제공해주는 어노테이션인 @Service, @Controller와 같은 어노테이션을 사용해서 스프링 컨테이너에 객체를 등록하는 방법이 있고 직접 자바 코드를 이용해서 등록하는 방법도 있다. 

 

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

public class FileService {
	private final FileRepository fileRepository;
    
    public FileService(FileRepository fileRepository) {
    	this.fileRepository = fileRepository;
    }
}

이러한 FileService 클래스가 있다고 하면, 이 클래스는 생성자로 FileRepository를 받아야한다. 그리고 스프링 컨테이너에 등록하는 어노테이션도 없다. 이제 이 녀석을 자바 코드로 직접 등록해보자. 

 

 

그 방법은 Configuration 클래스를 하나 새로 만들고 @Configuration 어노테이션을 추가해주자. 다음 코드와 같다.

@Configuration
public class SpringConfig {
	
    @Bean
    public FileService fileService() {
    	return new FileService(fileRepository());
    }
    
    @Bean
    public FileRepository fileRepository() {
    	return new FileRepository();
    }
}

이렇게 @Configuration 어노테이션을 추가하면 스프링한테 "스프링을 띄우면 이 파일을 확인해서 내가 @Bean 어노테이션을 붙인 녀석들 모두를 스프링 컨테이너에 등록해줘 !" 라고 하는것이다. 

 

그리고 FileService()는 FileRepository를 생성자로 받아야 하니 FileRepository 또한 스프링 빈으로 등록을 하고 등록된 객체를 불러다가 FileService에 넣어준다. 

 

 

이렇게 자바 코드로 직접 스프링 빈을 등록을 하면 이제 위에서와 같이 의존관계를 위한 의존성 주입이 가능해지는데 의존성 주입(Dependency Injection)을 하는 방법에는 크게 3가지가 있다. 필드 주입, setter 주입, 생성자 주입(위에서 봤던 것).

 

필드 주입, setter 주입은 이제 거의 사용하지 않는다. 아니 아예 사용하지 않을수도 있다. 그래서 어떻게 하는지만 보고 넘어갈 생각이다.

// 필드 주입
public class FileService {
	@Autowired private FileRepository fileRepository;
}
// setter 주입
public class FileService {
	private FileRepository fileRepository;
    
	@Autowired
    public void setFileRepository(FileRepository fileRepository) {
    	this.fileRepository = fileRepository;
    }
}

필드 주입과 setter 주입은 이렇게 생겨먹었다. 아 물론 필드 주입같은 경우 테스트 코드에서는 종종 쓰이곤한다. 왜냐하면 테스트는 가장 끝단에 있는거니까 그냥 편하게 쓸 수 있는게 가장 좋은거라고 생각하자.

 

728x90
반응형
LIST