이 프로젝트의 개발 환경
- 개발 언어 및 개발 환경
- OpenJDK 17
- 기타 환경
- macOS Sonoma 14.1.1
- IntelliJ IDEA 2020.3 Ultimate Edition
예제에 앞서 이 블로그의 문서: Chapter 03B. 여러 개의 @Configuration 설정 클래스 등록하기를 선행합니다.
이전 예제에서는 스프링의 의존 주입(DI, Dependency Injection)에 대해서 살펴봤습니다.
설정 클래스 AppContext는 주입할 의존 대상을 생성자 또는 메서드를 사용하여 DI 했습니다.
@Configuration
public class AppConttext {
@Bean
public MemberDao memberDao() {
return new MemberDao();
}
@Bean
public ChangePasswordService changePwdSvc() {
ChangePasswordService pwdSvc = new ChangePasswordService();
pwdSvc.setMemberDao(memberDao());
return pwdSvc;
}
}
자동 주입은 설정 클래스에서 DI 코드를 직접 작성하지 않고 스프링의 기능을 사용합니다.
예제 프로젝트 생성
sp5-chap04 프로젝트를 생성하고 chap04 패키지를 추가합니다.
이전 프로젝트의 chap03 패키지의 소스 파일을 복사하여 사용합니다.
tree
.
├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── src
└── main
├── java
│ ├── AppContext1.java
│ ├── AppContext2.java
│ ├── ChangePasswordService.java
│ ├── Main.java
│ ├── Member.java
│ ├── MemberDao.java
│ ├── MemberListPrinter.java
│ ├── MemberPrinter.java
│ ├── MemberRegisterService.java
│ └── RegisterRequest.java
└── resources
자동 주입을 위한 @Autowire
자동 주입은 주입 대상에 @Autowire 어노테이션을 등록합니다.
자동 주입은 @Autowire @Resource @Inject 어노테이션을 사용합니다.
세 가지 어노테이션이 모두 유효하지만 이 예제에서는 @Autowire을 사용합니다.
ChangePasswordService 클래스의 DI 코드를 다음과 같이 수정합니다.
import org.springframework.beans.factory.annotation.Autowired;
public class ChangePasswordService
{
@Autowired
private MemberDao memberDao;
public void changePassword(String email,
String oldPassword,
String newPassword)
{
Member member = memberDao.selectByEmail(email);
if (null == member)
throw new RuntimeException();
member.changePassword(oldPassword, newPassword);
memberDao.update(member);
}
@Deprecated
public void setMemberDao(MemberDao memberDao)
{
this.memberDao = memberDao;
}
}
코드 | 비고 | |
MemberDao 필드에 이 어노테이션은 설정 클래스에서 DI 하지 않더라도 자동 주입됩니다. |
||
MemberDao 필드는 자동 주입되므로 이 함수는 더 이상 사용하지 않습니다. |
AppContext 클래스의 DI 코드는 다음과 같이 수정합니다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppContext
{
@Bean
public ChangePasswordService changePwdSvc()
{
ChangePasswordService svc = new ChangePasswordService();
// svc.setMemberDao(memberDao);
return svc;
}
}
코드 | 비고 | |
@Autowired 어노테이션은 자동 주입하려는 필드 뿐만 아닐라 메소드에서도 사용 할 수 있습니다.
테스트를 위해서 MemberInfoPrinter 클래스를 생성합니다.
import org.springframework.beans.factory.annotation.Autowired;
public class MemberInfoPrinter
{
private MemberDao memberDao;
private MemberPrinter memberPrinter;
public void printMemberInfo(String email)
{
Member member = memberDao.selectByEmail(email);
if (null == member)
throw new NullPointerException();
memberPrinter.print(member);
System.out.println();
}
@Autowired
public void setMemberDao(MemberDao memberDao)
{
this.memberDao = memberDao;
}
@Autowired
public void setPrinter(MemberPrinter printer)
{
this.memberPrinter = printer;
}
}
코드 | 비고 | |
두 Setter에서 자동 주입을 실행합니다. |
MemberInfoPrinter를 Bean으로 지정합니다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppContext
{
@Bean
public MemberInfoPrinter infoPrinter()
{
return new MemberInfoPrinter();
}
}
코드 | 비고 | |
테스트 코드를 실행하기 위해 Main 클래스를 다음과 같이 수정합니다.
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main
{
private static ApplicationContext ctx = null;
public static void main(String[] args) throws IOException
{
ctx = new AnnotationConfigApplicationContext(AppContext.class);
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
while (true) {
(... 중략 ...)
else if (command.startsWith("info"))
{
processInfoCommand(command.split("[ ]+"));
continue;
}
else
{
printHelp();
}
}
}
private static void processInfoCommand(String[] args)
{
if (2 != args.length)
{
printHelp();
return;
}
MemberInfoPrinter infoPrinter = ctx.getBean("infoPrinter", MemberInfoPrinter.class);
infoPrinter.printMemberInfo(args[1]);
}
}
코드 | 비고 | |
콘솔에서 |
||
애플리케이션을 실행하고 새로 추가한 info 명령줄의 정상 동작을 확인합니다.
예제에서는 AppContext에서 MemberInfoPrinte를 Bean으로 등록하고 Bean 객체의 필드를 자동 주입합니다.
스프링은 Bean 객체의 Setter 메소드에 @Autowired 어노테이션을 등록하면 Bean 객체가 생성되는 시점에 해당 메소드를 호출합니다.
그리고 @Autowired 메소드의 파라미터 타입에 해당하는 Bean 객체를 자동으로 중비합니다.
나머지 다른 Bean 객체에서도 DI 코드를 자동 주입으로 수정합니다.
MemberRegisterService 클래스에서는 MemberDao 필드에 자동 주입을 실행하고 기본 생성자를 추가합니다.
import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDateTime;
public class MemberRegisterService
{
@Autowired
private MemberDao memberDao;
public MemberRegisterService()
{
}
public MemberRegisterService(MemberDao memberDao)
{
this.memberDao = memberDao;
}
}
코드 | 비고 | |
기본 생성자를 새로 추가합니다. |
@Autowired는 Setter 메소드 뿐만 아니라 Bean 객체의 필드에서도 동작합니다.
스프링은 Bean 객체의 필드에서 자동 주입이 필요한 경우 필드 타입과 일치하는 Bean 객체를 설정 파일에서 찾고 자동 주입합니다.
MemberListPrinter 클래스는 다음과 같이 수정합니다.
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Collection;
public class MemberListPrinter
{
private MemberDao memberDao;
private MemberPrinter memberPrinter;
public MemberListPrinter()
{
}
@Autowired
public void setMemberDao(MemberDao memberDao)
{
this.memberDao = memberDao;
}
@Autowired
public void setMemberPrinter(MemberPrinter memberPrinter)
{
this.memberPrinter = memberPrinter;
}
}
코드 | 비고 | |
기존 생성자를 삭제하고 기본 생성자를 새로 추가합니다. | ||
자동 주입을 위한 Setter를 추가합니다. |
기존 DI 코드를 모두 자동 주입으로 수정했으므로 AppContext에서 더 이상 DI를 처리하지 않습니다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppContext
{
@Bean
public MemberDao memberDao()
{
return new MemberDao();
}
@Bean
public MemberPrinter memberPrinter()
{
return new MemberPrinter();
}
@Bean
public MemberRegisterService memberRegSvc()
{
return new MemberRegisterService();
}
@Bean
public ChangePasswordService changePwdSvc()
{
return new ChangePasswordService();
}
@Bean
public MemberListPrinter listPrinter()
{
return new MemberListPrinter();
}
@Bean
public MemberInfoPrinter infoPrinter()
{
return new MemberInfoPrinter();
}
}
일치하는 Bean이 없는 경우
자동 주입은 필드의 타입 또는 Setter 메소드의 인자 타입에 의존합니다.
만약 자동 주입을 위한 Bean이 설정 클래스에 등록되어 있지 않으면 UnsatisfiedDependencyException이 발생합니다.
AppContext 클래스에서 다음과 같이 MemberDao()를 주석 처리하고 애플리케이션을 실행합니다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppContext
{
// @Bean
// public MemberDao memberDao()
// {
// return new MemberDao();
// }
}
에러 메시지는 Spring 버전에 따라서 차이가 있을 수 있습니다. Spring 5.3.2 버전에서는 다음 오류가 발생합니다.
org.springframework.context.support.AbstractApplicationContext refresh
경고: Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'memberRegSvc': Unsatisfied dependency expressed through field 'memberDao'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'MemberDao' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'memberRegSvc': Unsatisfied dependency expressed through field 'memberDao'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'MemberDao' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
@Autowired 어노테이션으로 자동 주입하려는 Bean 객체 타입이 설정 클래스에 여러 개 등록되어 있으면 어떤 일이 발생할까요?
설정 클래스 AppContext에서 memberPrinter를 두 개의 Bean으로 등록합니다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppContext
{
@Bean
public MemberPrinter memberPrinter()
{
return new MemberPrinter();
}
@Bean
public MemberPrinter memberPrinterSeconds()
{
return new MemberPrinter();
}
}
애플리케이션을 실행하면 expected single matching bean but found 2 오류가 발생합니다.
자동 주입하려는 Bean이 설정 클래스에 여러 개 등록되어 있어 한정 할 수 없다는 오류입니다.
No qualifying bean of type 'MemberPrinter' available: expected single matching bean but found 2: memberPrinter,memberPrinterSeconds
자동 의존 주입 Bean을 선택하기 위한 @Qualifier
이전 테스트처럼 자동 주입이 가능한 Bean이 여러 개인 경우 Spring은 어떤 Bean을 사용할지 한정 할 수 없습니다.
Spring이 자동 주입에서 어떤 Bean을 선택할지를 알려주려면 @Qualifier을 사용합니다.
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppContext
{
@Bean
@Qualifier("printer")
public MemberPrinter memberPrinter()
{
return new MemberPrinter();
}
@Bean
public MemberPrinter memberPrinterSeconds()
{
return new MemberPrinter();
}
}
코드 | 비고 | |
설정 클래스에서 Bean을 등록하며 |
@Qualifier로 지정한 Bean의 한정자는 자동 주입을 실행하는 위치에서 지정합니다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import java.util.Collection;
public class MemberListPrinter
{
@Autowired
@Qualifier("printer")
public void setMemberPrinter(MemberPrinter memberPrinter)
{
this.memberPrinter = memberPrinter;
}
}
코드 | 비고 | |
자동 주입을 위한 메소드에서 |
이제 Spring이 자동 주입으로 어떤 Bean을 사용해야하는지 한정 할 수 있습니다. 애플리케이션이 오류 없이 정상 실행됩니다.
설정 클래스에서 Bean을 등록할 때 @Qualifier를 사용하지 않으면 디폴트로 Bean 이름을 한정자로 사용합니다.
예제의 AppContext에서 memberPrinterSeconds는 MemberPrinter에 대한 Bean 이름입니다.
자동 주입이 필요한 코드에서 Bean 이름을 한정자로 지정하면 애플리케이션이 정상 동작하는 것을 확인 할 수 있습니다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import java.util.Collection;
public class MemberListPrinter
{
@Autowired
@Qualifier("memberPrinterSeconds")
public void setMemberPrinter(MemberPrinter memberPrinter)
{
this.memberPrinter = memberPrinter;
}
}
상위-하위 타입 관계의 자동 주입
이전 예제에서 자동 주입이 가능한 Bean이 여러 개인 경우 Spring은 어떤 Bean을 사용할지 한정 할 수 없는 문제를 확인했습니다.
그렇다면 Bean으로 등록되는 클래스를 상속하는 클래스를 Bean으로 동시 등록했을 때는 어떤 결과가 나올까요?
새로운 클래스 MemberSummaryPrinter를 생성하고 MemberPrinter 클래스를 상속합니다.
public class MemberSummaryPrinter extends MemberPrinter
{
@Override
public void print(Member member)
{
System.out.printf("회원 정보: 이메일=%s, 이름=%s\n",
member.getEmail(), member.getName());
}
}
설정 클래스 AppContext에서 memberPrinterSeconds()의 리턴 타입을 수정합니다.
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppContext
{
@Bean
public MemberPrinter memberPrinter()
{
return new MemberPrinter();
}
@Bean
public MemberSummaryPrinter memberPrinterSeconds()
{
return new MemberSummaryPrinter();
}
}
코드 | 비고 | |
그리고 @Qualifier를 사용해 지정자를 사용 중인 코드 MemberInfoPrinter MemberListPrinter를 주석처리합니다.
애플리케이션을 실행하면 최초의 문제처럼 expected single matching bean but found 2 오류가 발생합니다.
Bean으로 등록된 클래스가 서로 다름에도 불구하고 상속 관계에 있을 경우 Spring은 자동 주입을 위해 어떤 Bean을 사용할 지 한정하지 못합니다.
상속 관계의 클래스 중 어떤 클래스를 자동 주입하기를 원하는지 Spring이 판단할 수 없기 때문입니다.
이 경우에도 @Qualifier 어노테이션을 등록하여 사용자가 자동 주입을 원하는 Bean을 한정해야 합니다.
선택적인(Optional) 자동 주입
어떤 비즈니스 로직에선느 자동 주입을 필요로 하는 객체가 선택적(Optional)일 수 있습니다.
테스트를 위해서 MemberPrinter 클래스를 다음과 같이 수정합니다.
import org.springframework.beans.factory.annotation.Autowired;
import java.time.format.DateTimeFormatter;
public class MemberPrinter
{
private DateTimeFormatter dateTimeFormatter;
public void print(Member member)
{
if (null == dateTimeFormatter)
{
System.out.printf("회원 정보: 아이디=%d, 이메일=%s, 이름=%s, 등록일=%tF\n",
member.getId(),
member.getEmail(),
member.getName(),
member.getRegisterDateTime());
}
else
{
System.out.printf("회원 정보: 아이디=%d, 이메일=%s, 이름=%s, 등록일=%tF\n",
member.getId(),
member.getEmail(),
member.getName(),
dateTimeFormatter.format(member.getRegisterDateTime()));
}
}
@Autowired
public void setDateTimeFormatter(DateTimeFormatter dateTimeFormatter)
{
this.dateTimeFormatter = dateTimeFormatter;
}
}
코드 | 비고 | |
비즈니스 로직에서 |
||
예시의 코드는 DateTimeFormatter는 Nullable합니다.
하지만 자동 주입으로 설정 되어 있기 때문에 Error creating bean with name 'memberPrinter' 오류가 발생합니다.
자동 주입 대상이 Nullable한 경우 requred = false 옵션을 사용합니다.
@Autowired(required = false)
Spring은 자동 주입 단계에서 설정 클래스에서 일치하는 Bean을 찾지 못하더도 오류를 발생시키지 않습니다.
Spring 5부터는 required = false와 동일한 기능을 자바 8의 java.util.Optional로 대체 할 수 있습니다.
import org.springframework.beans.factory.annotation.Autowired;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
public class MemberPrinter
{
private DateTimeFormatter dateTimeFormatter;
@Autowired
public void setDateTimeFormatter(Optional<DateTimeFormatter> dateTimeFormatter)
{
if (dateTimeFormatter.isPresent())
this.dateTimeFormatter = dateTimeFormatter.get();
else
this.dateTimeFormatter = null;
}
}
또 다른 방법은 Spring의 @Nullable을 사용하는 것입니다.
이 방법은 Optional과 동일한 결과를 갖으며 코드를 더 간결하게 만듭니다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
import java.time.format.DateTimeFormatter;
public class MemberPrinter
{
private DateTimeFormatter dateTimeFormatter;
@Autowired
public void setDateTimeFormatter(@Nullable DateTimeFormatter dateTimeFormatter)
{
this.dateTimeFormatter = dateTimeFormatter;
}
}
이번에는 생성자에서 자동 의존 주입 대상을 초기화하고 requred = false 옵션을 활성화합니다.
import org.springframework.beans.factory.annotation.Autowired;
import java.time.format.DateTimeFormatter;
public class MemberPrinter
{
private DateTimeFormatter dateTimeFormatter;
public MemberPrinter()
{
dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일");
}
@Autowired(required = false)
public void setDateTimeFormatter(DateTimeFormatter dateTimeFormatter)
{
System.out.println("@Autowired(required = false) invoked");
this.dateTimeFormatter = dateTimeFormatter;
}
}
코드 | 비고 | |
이 코드 라인을 실행되지 않습니다. |
애플리케이션을 실행하면 생성자에서 할당된 DateTimeFormatter이 Setter로 인해 Null로 덮어씌우지 않습니다.
Spring에서 required = false 옵션을 지정하면 자동 할당에서 Bean을 찾지 못했을 때 Setter가 호출되지 않습니다.
이번에는 required = false 대신 @Nullable로 실행하고 이전과 비교합니다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
import java.time.format.DateTimeFormatter;
public class MemberPrinter
{
private DateTimeFormatter dateTimeFormatter;
public MemberPrinter()
{
dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일");
}
@Autowired
public void setDateTimeFormatter(@Nullable DateTimeFormatter dateTimeFormatter)
{
System.out.println("@Nullable invoked");
this.dateTimeFormatter = dateTimeFormatter;
}
}
코드 | 비고 | |
이 코드 라인은 실행되며 |
생성자에서 할당된 DateTimeFormatter는 Setter가 실행됨에 따라서 Null로 덮어씌워집니다.
Spring은 @Nullable로 지정된 자동 할당에서 Bean을 찾지 못했을 때 Setter의 인자를 Null로 호출합니다.
자동 의존 주입과 명시적 의존 주입의 우선 순위
자동 의존 주입으로 지정되었으나 코드에서 DI를 명시하면 어떤 일이 발생할까요?
테스트를 위해서 설정 클래스 AppContext를 다음과 같이 수정합니다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppContext
{
@Bean
public MemberPrinter memberPrinter()
{
return new MemberPrinter();
}
@Bean
public MemberSummaryPrinter memberPrinterSeconds()
{
return new MemberSummaryPrinter();
}
@Bean
public MemberInfoPrinter infoPrinter()
{
MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
infoPrinter.setPrinter(memberPrinterSeconds());
return infoPrinter;
}
}
코드 | 비고 | |
의존 주입을 명시적으로 실행하며 |
MemberInfoPrinter 클래스는 다음과 같습니다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
public class MemberInfoPrinter
{
@Autowired
@Qualifier("memberPrinter")
public void setPrinter(MemberPrinter printer)
{
this.memberPrinter = printer;
}
}
코드 | 비고 | |
Setter는 DI에서 단, 자동 주입은 |
애플리케이션을 실행하고 info 명령문을 실행합니다.
명령어를 입력하세요:
$ info hello
회원 정보: 아이디=1, 이메일=hello, 이름=hello, 등록일=2023-11-27
명시적인 DI를 사용해 MemberSummaryPrinter를 할당하였으나 실제로는 MemberPrinter가 할당되었습니다.
Spring의 자동 주입이 더 높은 우선 순위로 처리되고 있습니다.
사실 DI와 자동 주입을 동시에 사용하는 것은 적절하지 않습니다. 어떤 Bean 객체가 실제로 할당 되었는지를 명확하게 알기 어려워 가독성이 떨어진다는 문제가 있습니다.
실제 작성되는 코드에서는 특정 Bean 객체 할당을 위해서 DI 또는 자동 주입을 일관성 있게 사용하는 것이 적합합니다.
정리 및 복습
자동 의존 주입을 사용하면 DI를 위한 많은 코드를 생략 할 수 있습니다.- 자동 의존 주입을 위한 어노테이션은
@Autowired@Resource@Inject가 있습니다. - @Autowired로
자동 주입하는 객체가 설정 클래스에서 Bean으로 등록되어있지 않으면 Spring은 오류를 발생시킵니다. - @Autowired로
자동 주입하는 객체가 설정 클래스에서 여러 개의 Bean으로 등록되어 있으면 Spring은 오류를 발생시킵니다. - 여러 개의 Bean 중에서 자동 주입하려는 객체를 선택하려면
@Qualifier를 사용합니다. - @Qualifier는
자동 주입하려는 Bean 객체에 대한 지정자입니다. - 상속 관계에서도 Spring은 자동 주입으로 어떤 Bean을 사용해야하는지 판단하지 못합니다.
상속 관계 역시 자동 주입을 위해 @Qualifier로 지정해야 합니다. - 자동 주입이 필수적으로 실행되어야 하지 않으면
@Autowired(required=false)를 사용합니다. 또는@Nullable과 자바 8의Optional을 사용할 수 있습니다. - 설정 클래스에서
자동 주입하려는 Bean을 찾지 못했을 때 requred=false는 메소드가 실행되지 않습니다. - 반면
@Nullable로 지정된 메소드는 자동 주입하려는 Bean을 찾지 못했을 때 인자를 Null로 전달하며 실행됩니다. 명시적인 DI와 자동 주입이 동시 실행되는 코드에서 자동 주입이 우선시됩니다.
'Java > Spring' 카테고리의 다른 글
Spring 5 입문: Chapter 05.컴포넌트 스캔 (0) | 2023.11.28 |
---|---|
자동 의존 주입을 위한 @Autowired @Resource @Inject 차이 (0) | 2023.11.27 |
@Configuration 어노테이션 개념과 특징 (0) | 2023.11.23 |
Spring 5 입문: Chapter 03B. 여러 개의 @Configuration 설정 클래스 등록하기 (0) | 2023.11.23 |
Spring 5 입문: Chapter 03A. Spring 의존 주입(DI, Dependency Injection) (0) | 2023.11.22 |