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) {
this.memberDao = memberDao;
}
}
자동 주입 기능을 사용하는 것은 매우 간단하다. 의존을 주입할 대상에 @Autowired 애노테이션을 붙이기만 하면 된다. memberDao 필드에 @Autowired 애노테이션을 붙였다. 이러면 설정 클래스에서 의존을 주입하지 않아도 된다. 필드에 @Autowired 애노테이션이 붙어 있으면 스프링이 해당 타입의 빈 객체를 찾아서 필드에 할당한다.
@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();
}
}
@Autowired 애노테이션을 memberDao 필드에 붙였으므로 AppCtx 클래스의 @Bean 설정 메서드에서 의존을 주입하는 코드를 삭제하면 된다. setMemberDao()를 호출해서 MemberDao 빈 객체를 주입하지 않아도 스프링이 MemberDao 타입의 빈 객체를 주입하기 때문이다.
만약 @Autowired 애노테이션을 설정한 필드에 알맞은 빈 객체가 주입되지 않았다면 ChangePasswordService의 memberDao 필드는 null일 것이다. 그러면 암호 변경 기능을 실행할 때 NullPointerException이 발생하게 된다.
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;
}
}
@Autowired 애노테이션은 메서드에도 붙일 수 있다. 두개의 세터 메서드에 @Autowired 애노테이션을 붙였다.
@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 memberPrinter() {
return new MemberPrinter();
}
@Bean
public MemberInfoPrinter infoPrinter() {
MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
return infoPrinter;
}
}
MemberInfoPrinter 객체의 두 세터 메서드를 호출하지 않도록 수정했다. 빈 객체의 메서드에 @Autowired 애노테이션을 붙이면 스프링은 해당 메서드를 호출한다. 이때 메서드 파라미터 타입에 해당하는 빈 객체를 찾아 인자로 주입한다.
@Autowired 애노테이션을 적용한 대상에 일치하는 빈이 없으면 어떻게 될까? 설정 클래스의 memberDao() 메서드를 주석처리 했다.
@Configuration
public class AppCtx {
// @Bean
// public MemberDao memberDao() {
// return new MemberDao();
// }
@Bean
public MemberRegisterService memberRegSvc() {
return new MemberRegisterService();
}
... 중략
}
public class MainForSpring {
private static ApplicationContext ctx = null;
public static void main(String[] args) throws IOException {
// 스프링 컨테이너 생성
ctx = new AnnotationConfigApplicationContext(AppCtx.class);
BufferedReader reader =
new BufferedReader(new InputStreamReader(System.in));
while (true) {
System.out.println("명령어를 입력하세요:");
String command = reader.readLine();
if (command.equalsIgnoreCase("exit")) {
System.out.println("종료합니다.");
break;
}
if (command.startsWith("new ")) {
processNewCommand(command.split(" "));
continue;
} else if (command.startsWith("change ")) {
processChangeCommand(command.split(" "));
continue;
} else if (command.equals("list")) {
processListCommand();
continue;
} else if (command.startsWith("info ")) {
processInfoCommand(command.split(" "));
continue;
} else if (command.equals("version")) {
processVersionCommand();
continue;
}
printHelp();
}
}
private static void processNewCommand(String[] arg) {
if (arg.length != 5) {
printHelp();
return;
}
MemberRegisterService regSvc =
ctx.getBean("memberRegSvc", MemberRegisterService.class);
RegisterRequest req = new RegisterRequest();
req.setEmail(arg[1]);
req.setName(arg[2]);
req.setPassword(arg[3]);
req.setConfirmPassword(arg[4]);
if (!req.isPasswordEqualToConfirmPassword()) {
System.out.println("암호와 확인이 일치하지 않습니다.\n");
return;
}
try {
regSvc.regist(req);
System.out.println("등록했습니다.\n");
} catch (DuplicateMemberException e) {
System.out.println("이미 존재하는 이메일입니다.\n");
}
}
... 중략
}
이 상태에서 MainForSpring을 실행하면 익셉션이 발생하면서 제대로 실행되지 않는다. 에러를 보면 'memberRegSvc' 빈을 생성하는데 에러가 발생했다. 'memberDao' 필드에 대한 의존을 충족하지 않는다는 내용이 나오고 적용할 수 있는 'MemberDao' 타입의 빈이 없다는 내용이 나온다.
이 에러 메세지는 @Autowired 애노테이션을 붙인 MemberRegisterService의 memberDao 필드에 주입할 MemberDao 빈이 존재하지 않아 에러가 발생했다는 사실을 알려준다.
반대로 @Autowired 애노테이션을 붙인 주입 대상에 일치하는 빈이 두개 이상이면 어떻게 될까? memberPrinter에 해당하는 빈 설정 메서드를 주석으로 막고 memberPrinter1(), memberPrinter2()의 빈 설정을 추가했다.
//@Bean
//public MemberPrinter memberPrinter() {
// return new MemberPrinter();
//}
@Bean
public MemberPrinter memberPrinter1() {
return new MemberPrinter();
}
@Bean
public MemberPrinter memberPrinter2() {
return new MemberPrinter();
}
MainForSpring을 실행하면 MemberPrinter 타입의 빈을 한정할 수 없는데, 해당 타입 빈이 한 개가 아니라 이름이 두 개인 빈을 발견했다는 사실을 알려준다.
자동 주입을 하려면 해당 타입을 가진 빈이 어떤 빈인지 정확하게 한정할 수 있어야 하는데 MemberPrinter 타입의 빈이 두 개여서 어떤 빈을 자동 주입 대상으로 선택해야 할지 한정할 수 없다. 이때 스프링은 자동 주입에 실패하고 Exception을 발생시킨다.
자동 주입 가능한 빈이 두 개 이상이면 자동 주입할 빈을 지정할 수 있는 방법이 필요하다. 이때 @Qualifier 애노테이션을 사용한다. 이걸로 자동 주입 대상 빈을 한정할 수 있다.
@Qualifier 애노테이션은 두개의 위치에서 사용 가능하다. 첫번째는 @Bean 애노테이션을 붙인 빈 설정 메서드이다.
@Configuration
public class AppCtx {
... 중략
@Bean
@Qualifier("printer")
public MemberPrinter memberPrinter1() {
return new MemberPrinter();
}
@Bean
@Qualifier("summaryPrinter")
public MemberSummaryPrinter memberPrinter2() {
return new MemberSummaryPrinter();
}
}
memberPrinter1() 메서드에 "printer" 값을 갖는 @Qualifier 애노테이션을 붙였다. 이 설정은 해당 빈의 한정 값으로 "printer"를 지정한다. 이렇게 지정한 한정 값은 @Autowired 애노테이션에서 자동 주입할 빈을 한정할 때 사용한다.
public class MemberListPrinter {
private MemberDao memberDao;
private MemberPrinter printer;
@Autowired
@Qualifier("printer)
public void setMemberPrinter(MemberSummaryPrinter printer) {
this.printer = printer;
}
}
setMemberPrinter() 메서드에 @Autowired 애노테이션을 붙였으므로 MemberPrinter 타입의 빈을 자동 주입한다. 이때 @Qualifier 애노테이션의 값이 "printer" 이므로 한정 값이 "printer"인 빈을 의존 주입 후보로 사용한다. 그래서 앞서 스프링 설정 클래스에서 @Qualifier 애노테이션의 값으로 "printer"를 준 MemberPrinter 타입의 빈을 자동 주입 대상으로 사용한다.
빈 설정에 @Qualifier 애노테이션이 없으면 빈의 이름(@Bean 붙인 메서드 명)을 한정자로 지정한다.
public class MemberSummaryPrinter extends MemberPrinter {
@Override
public void print(Member member) {
System.out.printf(
"회원 정보: 이메일=%s, 이름=%s\n",
member.getEmail(), member.getName());
}
}
이 클래스는 MemberPrinter 클래스를 상속한 클래스다.
@Configuration
public class AppCtx {
@Bean
public MemberPrinter memberPrinter1() {
return new MemberPrinter();
}
@Bean
public MemberSummaryPrinter memberPrinter2() {
return new MemberSummaryPrinter();
}
}
MemberListPrinter, MemberInfoPrinter 클래스의 세터 메서드에 붙인 @Qualifier 애노테이션을 삭제했다. 다시 MainForSpring을 실행하면 같은 타입 빈을 두 개 설정하고 @Qualifier 애노테이션을 붙이지 않았을 때와 동일한 Exception이 발생한다.
memberPrinter2 빈을 MemberSummaryPrinter 타입으로 변경했음에도 같은 에러가 발생하는 이유는 MemberSummaryPrinter 클래스가 MemberPrinter 클래스를 상속했기 때문이다.
MemberSummaryPrinter 클래스는 MemberPrinter 타입에도 할당할 수 있으므로, 스프링 컨테이너는 MemberPrinter 타입 빈을 자동 주입해야 하는 @Autowired 애노테이션을 만나면 1,2 타입 빈 중에서 어떤 빈을 주입해야 할지 알 수 없다.
해결 방법은 둘다 각각 @Qualifier 한정자를 다르게 설정해주어 붙여주거나, MemberListPrinter가 MemberSummaryPrinter를 사용하도록 수정하는 것이다. MemberSummaryPrinter 타입 빈은 한 개만 존재하므로 자동 주입할 대상이 두 개 이상이어서 발생하는 문제를 피할 수 있다.