해당 포스트는 초보 웹 개발자를 위한 스프링 5 프로그래밍 입문 [최범균 저] 책 내용을 참고하였습니다.
(기존 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 자동 주입할 빈을 지정해주어야 한다.
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를 사용하는 것이 더 좋은 방법!
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: 로자
[스프링1] 6장. 빈 라이프 사이클과 범위 (0) | 2022.11.05 |
---|---|
[스프링1] 5장. 컴포넌트 스캔 (0) | 2022.11.05 |
[스프링 1] 3장.스프링 DI (0) | 2022.10.27 |
[스프링 1] 2장.스프링 시작하기 (0) | 2022.10.27 |
[스프링1] 빈 생명주기 콜백, 빈 스코프 (0) | 2022.10.21 |