상세 컨텐츠

본문 제목

[스프링1] 4장. 의존 자동 주입

22-23/22-23 Spring 1

by YUZ 유즈 2022. 11. 5. 10:00

본문

728x90

해당 포스트는 초보 웹 개발자를 위한 스프링 5 프로그래밍 입문 [최범균 저] 책 내용을 참고하였습니다.


 

❗@Autowired 애노테이션을 이용한 의존 자동 주입

📌@Autowired로 의존 자동 주입하기

(기존 config 클래스)

@Configuration
public class AppCtx {

	@Bean
	public MemberDao memberDao() {
		return new MemberDao();
	}
	
	@Bean
	public MemberRegisterService memberRegSvc() {
		return new MemberRegisterService(memberDao()); //의존 관계 직접 주입
	}
	
	@Bean
	public ChangePasswordService changePwdSvc() {
		ChangePasswordService pwdSvc = new ChangePasswordService();
		pwdSvc.setMemberDao(memberDao()); //의존 관계 직접 주입
		return pwdSvc;
	}
	
	@Bean
	public MemberPrinter memberPrinter() {
		return new MemberPrinter();
	}
	
	@Bean
	public MemberListPrinter listPrinter() {
		return new MemberListPrinter(memberDao(), memberPrinter()); //의존 관계 직접 주입
	}
	
	@Bean
	public MemberInfoPrinter infoPrinter() {
		MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
		infoPrinter.setMemberDao(memberDao()); //의존 관계 직접 주입
		infoPrinter.setPrinter(memberPrinter()); //의존 관계 직접 주입
		return infoPrinter;
	}
	
	@Bean
	public VersionPrinter versionPrinter() {
		VersionPrinter versionPrinter = new VersionPrinter();
		versionPrinter.setMajorVersion(5);
		versionPrinter.setMinorVersion(0);
		return versionPrinter;
	}
}

 

1. 필드에 @Autowired

public class ChangePasswordService {

	@Autowired
	private MemberDao memberDao; //의존 주입 대상

	public void changePassword(String email, String oldPwd, String newPwd) {
		Member member = memberDao.selectByEmail(email);
		if (member == null)
			throw new MemberNotFoundException();

		member.changePassword(oldPwd, newPwd);

		memberDao.update(member);
	}

	public void setMemberDao(MemberDao memberDao) { //setter 메소드 사용 없이 의존 주입 가능
		this.memberDao = memberDao;
	}

}

 

2. 메서드에 @Autowired

public class MemberInfoPrinter {

	private MemberDao memDao;
	private MemberPrinter printer;

	public void printMemberInfo(String email) {
		Member member = memDao.selectByEmail(email);
		if (member == null) {
			System.out.println("데이터 없음\n");
			return;
		}
		printer.print(member);
		System.out.println();
	}

	@Autowired
	public void setMemberDao(MemberDao memberDao) {
		this.memDao = memberDao;
	}

	@Autowired
	public void setPrinter(MemberPrinter printer) {
		this.printer = printer;
	}

}

 

3. 필드에 @Autowired + 기본생성자

public class MemberRegisterService {
	@Autowired
	private MemberDao memberDao;

	public MemberRegisterService() { //인자 없는 생성자 추가
	}
	
	public MemberRegisterService(MemberDao memberDao) {
		this.memberDao = memberDao;
	}

	public Long regist(RegisterRequest req) {
		Member member = memberDao.selectByEmail(req.getEmail());
		if (member != null) {
			throw new DuplicateMemberException("dup email " + req.getEmail());
		}
		Member newMember = new Member(
				req.getEmail(), req.getPassword(), req.getName(), 
				LocalDateTime.now());
		memberDao.insert(newMember);
		return newMember.getId();
	}
}

 

public class MemberListPrinter {

	private MemberDao memberDao;
	private MemberPrinter printer;

	public MemberListPrinter() { //인자 없는 생성자 추가
	}
	
	public MemberListPrinter(MemberDao memberDao, MemberPrinter printer) {
		this.memberDao = memberDao;
		this.printer = printer;
	}

	...

	@Autowired
	public void setMemberDao(MemberDao memberDao) {
		this.memberDao = memberDao;
	}
	
	@Autowired
	public void setMemberPrinter(MemberPrinter printer) {
		this.printer = printer;
	}
}

 

(@Autowired 적용 후 config)

@Configuration
public class AppCtx {

	@Bean
	public MemberDao memberDao() {
		return new MemberDao();
	}
	
	@Bean
	public MemberRegisterService memberRegSvc() {
		return new MemberRegisterService();
	}
	
	@Bean
	public ChangePasswordService changePwdSvc() {
		return new ChangePasswordService();
	}
	
	@Bean
	public MemberPrinter memberPrinter1() {
		return new MemberPrinter();
	}
	
	@Bean
	public MemberSummaryPrinter memberPrinter2() {
		return new MemberSummaryPrinter();
	}
	
	@Bean
	public MemberListPrinter listPrinter() {
		return new MemberListPrinter();
	}
	
	@Bean
	public MemberInfoPrinter infoPrinter() {
		MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
		return infoPrinter;
	}
	
	@Bean
	public VersionPrinter versionPrinter() {
		VersionPrinter versionPrinter = new VersionPrinter();
		versionPrinter.setMajorVersion(5);
		versionPrinter.setMinorVersion(0);
		return versionPrinter;
	}
}

 기존에 setter 메소드나 생성자를 통해 인자 직접 주입한 코드들을 생략할 수 있다. 그래서 config 클래스 코드가 매우 깔끔해진다.

 

📌의존 자동 주입 시 발생할 수 있는 에러

1.의존 대상이 빈으로 등록되지 않은 경우: NoSuchBeanDefinitionExceptions

2.일치하는 빈이 두 개 이상인 경우: NoUniqueBeanDefinitionExceptions

 

 

❗@Qualifier 애노테이션을 이용한 의존 객체 선택

📌@Qualifier 사용 방법

의존 자동 주입 시 일치하는 빈이 두 개 이상인 경우, @Qualifier 자동 주입할 빈을 지정해주어야 한다.

 

1. config 클래스에서 @Qualifier 한정값 지정

@Configuration
public class AppCtx {

	...
	
	@Bean
	@Qualifier("printer") //빈 한정값 지정
	public MemberPrinter memberPrinter1() {
		return new MemberPrinter();
	}
	...
}

2. @Autowired 애노테이션을 이용한 의존 자동 주입 대상으로 해당 한정값 빈으로 설정

public class MemberListPrinter {

	private MemberDao memberDao;
	private MemberPrinter printer;

	...
	
	@Autowired
	@Qualifier("printer") //해당 한정값 빈으로 의존관계 자동 주입 대상 지정
	public void setMemberPrinter(MemberPrinter printer) {
		this.printer = printer;
	}
}

 

📌빈 이름과 기본 한정자

config 클래스에 @Qualifier 지정을 하지 않은 경우엔 빈 이름이 메소드 이름이 된다.

@Configuration
public class AppCtx2{

	@Bean
	public MemberPrinter printer(){
		return new MemberPrinter();
	}

	@Bean
	@Qualifier("mprinter")
	public MemberPrinter print2(){
		return new MemberPrinter();
	}

	@Bean
	public MemberInfoPrinter2 infoPrinter(){
		MemberInfoPrinter2 infoPrinter = new MemberInfoPrinter2();
		return infoPrinter;
	}
}

 

또, @Autowired 애노테이션도 @Qualifer 애노테이션이 없으면 필드나 파라미터 이름을 한정자로 사용한다.

public class MemberPrinter2 {
	@Autowired
	private MemberPrinter printer;
}
빈 이름 @Qualifier 한정자
printer   printer
printer2 mprinter mprinter
infoPrinter   infoPrinter

따라서 위 표에 의하면 MemberPrinter2의 MemberPrinter타입 필드에는 printer()메소드로 등록된 빈이 의존 주입된다.

 

 

❗상위/하위 타입 관계와 자동 주입

MemberSummaryPrinter가 MemberPrinter를 상속하고 있다.

public class MemberSummaryPrinter extends MemberPrinter {

	@Override
	public void print(Member member) {
		System.out.printf(
				"회원 정보: 이메일=%s, 이름=%s\n", 
				member.getEmail(), member.getName());
	}

}

이 상태로 실행하면(기존에 config 파일과 MemberListPrinter, MemberInfoPrinter 에 작성한 @Qualifier 애노테이션 삭제 후 실행), 위에 언급한 일치하는 빈이 두 개 이상인 경우(NoUniqueBeanDefinitionExceptions)와 같은 에러가 발생한다.

 

@Configuration
public class AppCtx {
	...

	@Bean
	public MemberPrinter memberPrinter1() {
		return new MemberPrinter();
	}
	
	@Bean
	public MemberSummaryPrinter memberPrinter2() {
		return new MemberSummaryPrinter();
	}
	...
}

그 이유는 memberPrinter2의 타입 MemberSummaryPrinter 가 MemberPrinter를 상속하고 있기 때문이다. MemberListPrinter와 MemberInfoPrinter가 @Autowired를 통해서 MemberPrinter 타입의 빈 객체을 의존 자동 주입 하려고 하는데, 이때 MemberPrinter 타입 빈 탐색 결과로 memberPrinter1(MemberPrinter 타입)과 memberPrinter2(MemberPrinter를 상속한 MemberSummaryPrinter 타입) 2개의 빈이 나온다. 그리고 이 2개의 빈 중 무엇을 주입할지를 알 수 없어서 익셉션을 발생시키는 것이다.

 

그러면 이를 어떻게 해결할 수 있을까?

1. @Qualifier 사용

@Configuration
public class AppCtx {
	...
	@Bean
	@Qualifier("printer")
	public MemberPrinter memberPrinter1() {
		return new MemberPrinter();
	}
	...
}

 

public class MemberInfoPrinter {

	private MemberDao memDao;
	private MemberPrinter printer;

	@Autowired
	@Qualifier("printer")
	public void setPrinter(MemberPrinter printer) {
		this.printer = printer;
	}
	...
}

 

2. @Autowired 대상 인자 하위 클래스 지정

public class MemberListPrinter {

	private MemberDao memberDao;
	private MemberPrinter printer;
	
	@Autowired
	public void setMemberPrinter(MemberSummaryPrinter printer) {
		this.printer = printer;
	}
	...
}

 

이전 인프런 강의에 들은 바로는 구체화한 하위 클래스 타입으로 지정하는 것보단 @Qualifier를 사용하는 것이 더 좋은 방법!

 

❗@Autowired 애노테이션의 필수 여부

📌의존 자동 주입 대상이 빈이 아닌 경우(의존 자동 주입이 필수가 아닌 경우)

MemberPrinter 의 print 메소드 코드를 수정했다.

public class MemberPrinter {
	private DateTimeFormatter dateTimeFormatter;
	
	public MemberPrinter() {
		dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일");
	}
	
	public void print(Member member) {
		if (dateTimeFormatter == null) {
			System.out.printf(
					"회원 정보: 아이디=%d, 이메일=%s, 이름=%s, 등록일=%tF\n", 
					member.getId(), member.getEmail(),
					member.getName(), member.getRegisterDateTime());
		} else {
			System.out.printf(
					"회원 정보: 아이디=%d, 이메일=%s, 이름=%s, 등록일=%s\n", 
					member.getId(), member.getEmail(),
					member.getName(), 
					dateTimeFormatter.format(member.getRegisterDateTime()));
		}
	}
	
	@Autowired //
	public void setDateFormatter(DateTimeFormatter dateTimeFormatter) {
		this.dateTimeFormatter = dateTimeFormatter;
	}
	
}

print 메소드 내용을 살펴보면 dateTimeFormatter 필드 여부를 기준으로 if문을 작성하였다. 때문에 dateTimeFormatter가 지정되지 않아도 print 메소드가 실행되어야 한다는 의미이다. 즉, 자동 주입할 대상이 필수가 아니다.

그치만 우리가 지금까지 사용한 @Autowired는 빈에 등록되지 않는 경우에는 익셉션이 발생되었다. 만약 자동 주입이 필수가 아닌 경우엔 어떻게 해야할까?

 

1. @Autowired(required=false) : 자동주입 대상이 빈이 아닌 경우엔 메소드 실행X

@Autowired(required = false)
public void setDateFormatter(DateTimeFormatter dateTimeFormatter) {
	this.dateTimeFormatter = dateTimeFormatter;
}

 

2. java8의 Optional<> : 자동주입 대상이 빈이 아닌 경우 null(Optional.empty) 할당

@Autowired
public void setDateFormatter(Optional<DateTimeFormatter> formatterOpt) {
	if (formatterOpt.isPresent()) {
		this.dateTimeFormatter = formatterOpt.get();
	} else {
		this.dateTimeFormatter = null;
	}
}

 

3. @Nullable : 자동주입 대상이 빈이 아닌 경우 null 할당

	@Autowired
	public void setDateFormatter(@Nullable DateTimeFormatter dateTimeFormatter) {
		this.dateTimeFormatter = dateTimeFormatter;
	}

 

 

❗자동 주입과 명시적 의존 주입 간의 관계

@Configuration
public class AppCtx {
	...	
	@Bean
	public MemberInfoPrinter infoPrinter() {
		MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
		infoPrinter.setPrinter(memberPrinter2());
		return infoPrinter;
	}
	...
}

MemberInfoPrinter의 setter 메서드에 @Autowired로 memberPrinter1을 자동 주입한 상태에서 config 파일에서 memberPrinter2를 직접 주입했다.

 

어떻게 될까?

결과적으로 memberPrinter1이 주입된다.

즉, config 클래스에서 setter 메서드로 의존을 직접 주입해도 빈 클래스에 @Autowired 애노테이션이 있다면 자동 주입을 통해 일치하는 빈을 주입한다.

 

자동 주입을 하는 코드와 수동으로 주입하는 코드가 섞여 있으면 주입을 제대로 하지 않아서 NullPointerException이 발생했을 때 원인을 찾기 어렵다. 일관되게 사용하자!

 


1. ( @Autowired ) 애노테이션으로 의존 관계 자동 주입이 가능하다.

2. ( @Qualifier ) 애노테이션으로 빈 객체에 한정자를 지정할 수 있다.

3. @Qualifier 애노테이션 사용 없이 빈 수동 등록 시에는 빈 이름이 ( 메서드 ) 이름과 동일하다.

4. @Autowired로 의존 관계 자동 주입 시, NoUniqueBeanDefinitionException 이 발생하는 경우 ( @Qualifier ) 애노테이션을 이용해 해결할 수 있다.

5. @Autowired로 의존 관계 자동 주입 시, NoUniqueBeanDefinitionException 이 발생하는 경우 대상 인자 클래스 타입을 ( 하위 or 구체화된 ) 클래스로 지정하여 해결할 수 있다.

6. 빈으로 등록되지 않는 클래스 객체에 대해서 @Autowired를 사용하고자 한다면 @Autowired의 ( required ) 속성을 false로 지정하면 된다. 이때 메서드는 실행되지 않는다.

7. @Autowired 사용 시, java8 Optional<>을 이용하면 빈 등록이 안된 객체에 대해서 ( Optional.empty )가 할당되고, @Nullable을 사용하면 ( null )이 할당된다.


Spring 1

EDITOR: 로자

728x90

관련글 더보기