의존 자동 주입

HeeSeong·2021년 7월 27일
0
post-thumbnail

의존 자동 주입


@Autowired 애노테이션을 이용


  • ChangePasswordService.java
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 애노테이션이 붙어 있으면 스프링이 해당 타입의 빈 객체를 찾아서 필드에 할당한다.


  • AppCtx.java
@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이 발생하게 된다.


  • MemberInfoPrinter.java
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 애노테이션을 붙였다.


  • AppCtx.java
@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() 메서드를 주석처리 했다.


  • AppCtx.java
@Configuration
public class AppCtx {

//	@Bean
//	public MemberDao memberDao() {
//		return new MemberDao();
//	}
	
	@Bean
	public MemberRegisterService memberRegSvc() {
		return new MemberRegisterService();
	}
 	... 중략   
}
  • MainForSpring.java
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 애노테이션을 사용한다. 이걸로 자동 주입 대상 빈을 한정할 수 있다.

@Qualifier 애노테이션은 두개의 위치에서 사용 가능하다. 첫번째는 @Bean 애노테이션을 붙인 빈 설정 메서드이다.


  • AppCtx.java
@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 애노테이션에서 자동 주입할 빈을 한정할 때 사용한다.


  • MemberListPrinter.java
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 붙인 메서드 명)을 한정자로 지정한다.


상위/하위 타입 관계


  • MemberSummaryPrinter.java
public class MemberSummaryPrinter extends MemberPrinter {

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

이 클래스는 MemberPrinter 클래스를 상속한 클래스다.


  • AppCtx.java
@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 타입 빈은 한 개만 존재하므로 자동 주입할 대상이 두 개 이상이어서 발생하는 문제를 피할 수 있다.

profile
끊임없이 성장하고 싶은 개발자

0개의 댓글