의존이란?
DI('Dependency Injection’)의 약자로 우리말로는 ‘의존 주입’ 이라고 번역한다. 이 단어를 이해하려면 ‘의존’이 뭔지 알아야 한다. 다음 코드를 살펴보자.
import java.time.LocalDateTime;
public class MemberRegisterService{
private MemberDao memberDao = new MemberDao();
public void regist(RegisterRequest req){
Member member = memberDao.selectByEmail(req.getEmail.());
if(member != null){
// 이메일로 회원 데이터(Member 조회);
throw new DuplicateMemberException("dup email " + req.getEmail())
}
// 같은 이메일을 가진 회원이 존재하지 않으면 DB에 삽입
Member newMember = new Member(
req.getEmail(), req.getPassword(), req.getName(),
LocalDateTime. now());
memberDao.insert(newMember) ;
}
}
위 코드에서 눈여겨볼 점은 MeberRegisterService 클래스가 DB처리를 위해 MemberDao클래스의 메서드를 사용한다는 점이다. 회원 데이터가 존재하는지 확인하기 위해 MemberDao객체의 selectByEmail() 메서드를 실행하고, 회원 데이터를 DB에 삽입하기 위해 insert()메서드를 실행한다.
이렇게 한 클래스가 다른 클래스의 메서드를 실행할 때 이를 ‘의존’한다고 표현한다. 앞서 코드에서는MemberResisterService 클래스가 MemberDao클래스에 의존하고 있다.
📌 의존은 변경에 의해 영향을 받는 관계이다. 예를 들어 Insert 메서드를 변경한다면 MemberRegisterService클래스의 소스 코드도 변경해야 한다. 이렇게 변경에 따른 영향이 전파되는 관계를 의존 한다고 표현한다.
MemberRegisterService는 의존 대상인 MemberDao 객체를 직접 생성해서 필드에 할당한다. 쉬운 방법이긴 하지만, 유지보수 관점에서 문제점을 유발할 수 있다.
DI를 통한 의존 처리
DI(Dependency Injection, 의존 주입)는 의존하는 객체를 직접 생성하는 대신 의존 객체를 전달받는 방식을 사용한다. 소스를 확인해보자.
import java.time.LocalDateTime;
public class MemberRegisterService{
private MemberDao memberDao;
// 변경된 부분, 생성자를 통한 의존 주입
public MemberRegisterService(MemberDao memberDao){
this.memberDao = memberDao;
}
public void regist(RegisterRequest req){
Member member = memberDao.selectByEmail(req.getEmail.());
if(member != null){
// 이메일로 회원 데이터(Member 조회);
throw new DuplicateMemberException("dup email " + req.getEmail())
}
// 같은 이메일을 가진 회원이 존재하지 않으면 DB에 삽입
Member newMember = new Member(
req.getEmail(), req.getPassword(), req.getName(),
LocalDateTime. now());
memberDao.insert(newMember) ;
}
}
위 소스는 직접 객체를 생성하지 않고, 생성자를 통해 의존하고 있는 객체를 주입받았다. 의존 객체를 직접 구하지 않고 생성자를 통해서 전달 받기 때문에 이 코드는 DI(의존 주입)패턴을 따르고 있다.
MemberDao dao = new MemberDao();
MemberRegisterService svc = new MemberRegisterService(dao);
그런데 굳이 복잡하게 생성자를 통해 주입받을까? 이는 변경의 유연함 때문이다.
DI와 의존 객체 변경의 유연함
DB에서 데이터를 가져오는 시간이 길어 cache를 통해 받아오려고 한다. 그래서 개발자는 MemberDao를 상속받아 CachedMemberDao클래스를 하나 더 만들었다.
public class CachedMemberDao extends MemberDao {
)
이럴 경우, 직접 MemberDao객체를 생성해서 사용하고 있던 모든 클래스는 변경 부분이 생긴다.
public class MemberRegisterService{
private MemberDao memberDao = new CachedMemberDao; // MemberDao에서 변경
...
}
public class ChangePasswordService{
private MemberDao memberDao = new CachedMemberDao; // MemberDao에서 변경
...
}
생성자를 통한 주입 방법은 수정해야 될 부분이 직접 객체를 생성해서 주입받던 방법보다 적다.
public class MemberRegisterService{
private MemberDao memberDao;
// 변경된 부분, 생성자를 통한 의존 주입
public MemberRegisterService(MemberDao memberDao){
this.memberDao = memberDao;
}
...
}
public class ChangePasswordService{
private MemberDao memberDao;
// 변경된 부분, 생성자를 통한 의존 주입
public ChangePasswordService(MemberDao memberDao){
this.memberDao = memberDao;
}
...
}
MemberDao dao = new CachedMemberDao();
MemberRegisterService svc = new MemberRegisterService(dao);
ChangePasswordService pwdSvc = new ChangePasswordService(dao);
이렇게 변경할 코드가 한 곳으로 집중된다.
예제 프로젝트 만들기
- 회원 데이터 관련 클래스
- Member
- WrongIdPasswordException
- MemberDao
- 회원 가입 처리 관련 클래스
- DuplicateMemberException
- RegisterRequest
- MemberRegisterService
- 암호 변경 관련 클래스
- MemberNotFound
- ChangePasswordService
Member
package org.example.chap03;
import java.time.LocalDateTime;
@Getter @Setter // set은 id만 해주는데 소스가 너무 길어지니까 그냥 롬복으로 표시함
public class Member {
private Long id;
private String email;
private String password;
private String name;
private LocalDateTime registerDateTime;
public Member(String email, String password, String name, LocalDateTime registerDateTime) {
this.email = email;
this.password = password;
this.name = name;
this.registerDateTime = registerDateTime;
}
// 암호 변경 기능
public void changePassword(String oldPassword, String newPassword){
if(!password.equals(oldPassword))
throw new WrongIdPasswordException();
this.password = newPassword;
}
}
MemberDao
package org.example.chap03;
import java.util.HashMap ;
import java.util.Map;
public class MemberDao {
private static long nextld = 0;
private Map<String, Member> map = new HashMap<>();
public Member selectByEmail(String email) {
return map.get(email);
}
public void insert(Member member) {
member.setId(++nextld);
map.put(member.getEmail(), member);
}
public void update(Member member) {
map.put(member.getEmail(), member);
}
}
DuplicateMemberException
package org.example.chap03;
// 동일한 이메일을 갖고 있는 회원이 존재할 때, MemberRegisterService가 발생시키는 익셉션타입
public class DuplicateMemberException extends RuntimeException{
public DuplicateMemberException(String message){
super(message);
}
}
RegisterRequset
package org.example.chap03;
@Getter @Setter
// 회원가입을 처리할 떄 필요한 이메일, 암호, 이름 데이터를 담고있는 클래스
public class RegisterRequest {
private String email;
private String password;
private String confirmPassword;
private String name;
}
MemberRegisterService
package org.example.chap03;
import java.time.LocalDateTime;
public class MemberRegisterService{
private MemberDao memberDao;
// 변경된 부분, 생성자를 통한 의존 주입
public MemberRegisterService(MemberDao memberDao){
this.memberDao = memberDao;
}
public void regist(RegisterRequest req){
Member member = memberDao.selectByEmail(req.getEmail());
if(member != null){
// 이메일로 회원 데이터(Member 조회);
throw new DuplicateMemberException("dup email " + req.getEmail());
}
// 같은 이메일을 가진 회원이 존재하지 않으면 DB에 삽입
Member newMember = new Member(
req.getEmail(), req.getPassword(), req.getName(),
LocalDateTime. now());
memberDao.insert(newMember) ;
}
}
ChangePasswordService
package org.example.chap03;
public class ChangePasswordService {
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;
}
}
MemberNotFoundException
package org.example.chap03;
public class MemberNotFoundException extends RuntimeException {
}
객체 조립기
main 메서드에서 의존 대상 객체를 생성하고 주입하는 방법이 나쁘진 않다. 이 방법보다 더 나은 방법은 객체를 생성하고 의존 객체를 주입해주는 클래스를 따로 작성하는 것이다. 이 클래스를 객체를 조립한다하여 조립기라고도 표현한다.
package org.example.chap03.assembler;
import org.example.chap03.ChangePasswordService;
import org.example.chap03.MemberDao;
import org.example.chap03.MemberRegisterService;
// 객체 조립기
public class Assembler{
private MemberDao memberDao;
private MemberRegisterService regSvc;
private ChangePasswordService pwdSvc;
public Assembler() {
memberDao = new MemberDao();
regSvc = new MemberRegisterService(memberDao);
pwdSvc = new ChangePasswordService();
pwdSvc.setMemberDao(memberDao);
}
public MemberDao getMemberDao() {
return memberDao;
}
public MemberRegisterService getMemberRegisterService() {
return regSvc;
}
public ChangePasswordService getChangePasswordService() {
return pwdSvc;
}
}
조립기가 생성한 객체의 연결 관계
Assembler assembler = new Assembler();
ChangePasswordService changePwdSvc = assembler.getChangePasswordService();
changePwdSvc.changePassword("madvirus@madvirus.net", u1234', "newpwd");
정리하면 조립기는 객체를 생성하고 의존 객체를 주입하는 기능을 제공한다. 또한 특정 객체가 필요한 곳에 객체를 제공한다.
조립기 사용예제
package org.example.chap03.main;
import org.example.chap03.*;
import org.example.chap03.assembler.Assembler;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class MainForAssembler {
public static void main(String[] args) throws IOException {
// 콘솔에 입력받기 위해 System.in 을 이용해서 입력 받는다.
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while (true) {
System.out.println("명령어를 입력하세요.");
String command = br.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;
}
// 명령어를 잘못 입력했을 경우 도움말
printHelp();
}
}
// 의존 주입
private static Assembler assembler = new Assembler();
// 새로운 회원정보 생성
private static void processNewCommand(String[] arg) {
if (arg.length != 5) {
printHelp();
return;
}
MemberRegisterService regSvc = assembler.getMemberRegisterService();
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");
}
}
// 암호 변경 메서드
private static void processChangeCommand(String[] arg) {
if (arg.length != 4) {
printHelp();
return;
}
ChangePasswordService changePwdSvc = assembler.getChangePasswordService();
try {
changePwdSvc.changePassword(arg[1], arg[2], arg[3]);
System.out.println("암호를 변경했습니다. \\n");
} catch (MemberNotFoundException e) {
System.out.println("존재하지 않는 이메일입니다. \\n");
} catch (WrongIdPasswordException e) {
System.out.println("이메일과 암호가 일치하지 않습니다. \\n");
}
}
private static void printHelp() {
System.out.println();
System.out.println("잘못된 명령어 입니다. 아래 명령어 사용법을 확인하세요.");
System.out.println("명령어 사용법:");
System.out.println("new 이메일 이름 암호 암호확인");
System.out.println("change 이메일 현재비번 변경비번");
System.out.println();
}
}
Assembler 클래스의 생성자에서 필요한 객체를 생성하고 의존을 주입한다. 따라서 Assembler 객체를 생성하는 시점에서 사용할 객체가 모두 생성된다.
스프링의 DI 설정
의존, DI, 조립기에 대해 알아본 이유는 스프링이 DI를 지원하는 조립기이기 때문이다. 실제로 스프링은 조립기와 유사한 기능을 제공한다. 즉 스프링은 Assembler 의 생성자 코드처럼 필요한 객체를 생성하고 생성한 객체에 의존을 주입한다. 또, Assembler의 메서드처럼 객체를 제공하는 기능을 정의하고 있다.
스프링을 이용한 객체 조립과 사용
package org.example.chap03.main.config;
import org.example.chap03.*;
import org.example.chap03.MemberInfoPrinter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// Assembler 클래스와 비슷한 스프링의 조립기
// 스프링은 DI를 지원하는 조립기이다.
@Configuration
public class AppCtx {
@Bean
public MemberDao memberDao(){
return new MemberDao();
}
@Bean
public MemberRegisterService memberRegisterSvc(){
// 생성자를 통한 의존주입
return new MemberRegisterService(memberDao());
}
@Bean
public ChangePasswordService changePasswordSvc(){
ChangePasswordService pwdSvc = new ChangePasswordService();
// set메서드를 통한 의존 주입
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;
}
}
package org.example.chap03.main;
import org.example.chap03.*;
import org.example.chap03.assembler.Assembler;
import org.example.chap03.main.config.AppCtx;
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 MainForSpring {
private static ApplicationContext ctx = null;
public static void main(String[] args) throws IOException {
// 스프링 컨테이너를 생성한다. 조립기와 동일하게 객체를 생성하고 의존객체를 주입한다.
// 컨테이너는 설정 파일로부터 생성할 객체와 의존 주입 대상을 정한다.
ctx = new AnnotationConfigApplicationContext(AppCtx.class);
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while (true) {
System.out.println("명령어를 입력하세요.");
String command = br.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.equals("info ")){
processInfoCommand(command.split(" "));
continue;
}else if(command.equals("version")){
processVersionCommand();
continue;
}
// 명령어를 잘못 입력했을 경우 도움말
printHelp();
}
}
private static void processVersionCommand() {
VersionPrinter versionPrinter = ctx.getBean("versionPrinter", VersionPrinter.class);
versionPrinter.print();
}
private static void processInfoCommand(String[] arg) {
if(arg.length != 2){
printHelp();
return;
}
MemberInfoPrinter infoPrinter = ctx.getBean("infoPrinter", MemberInfoPrinter.class);
infoPrinter.printMemberInfo(arg[1]);
}
private static void processListCommand() {
MemberListPrinter listPrinter = ctx.getBean("listPrinter", MemberListPrinter.class);
listPrinter.printAll();
}
// 새로운 회원정보 생성
private static void processNewCommand(String[] arg) {
if (arg.length != 5) {
printHelp();
return;
}
MemberRegisterService regSvc = ctx.getBean("memberRegisterSvc", 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");
}
}
// 암호 변경 메서드
private static void processChangeCommand(String[] arg) {
if (arg.length != 4) {
printHelp();
return;
}
ChangePasswordService changePwdSvc = ctx.getBean("changePasswordSvc", ChangePasswordService.class);
try {
changePwdSvc.changePassword(arg[1], arg[2], arg[3]);
System.out.println("암호를 변경했습니다. \\n");
} catch (MemberNotFoundException e) {
System.out.println("존재하지 않는 이메일입니다. \\n");
} catch (WrongIdPasswordException e) {
System.out.println("이메일과 암호가 일치하지 않습니다. \\n");
}
}
private static void printHelp() {
System.out.println();
System.out.println("잘못된 명령어 입니다. 아래 명령어 사용법을 확인하세요.");
System.out.println("명령어 사용법:");
System.out.println("new 이메일 이름 암호 암호확인");
System.out.println("change 이메일 현재비번 변경비번");
System.out.println();
}
}
📌 AnnotationConfigApplicationContext를 사용해서 스프링 컨테이너를 생성한다. 스 프링 컨테이너는 Assembler와 동일하게 객체를 생성하고 의존 객체를 주입한다. getBean메서드를 사용하여 스프링 컨테이너로부터 이름이 "memberRegisterSvc“인 빈 객체를 구한다.
DI방식: 생성자 방식
private MemberDao membe Dao;
//생성자틀 통해 의존 객체틀 주입 받음
public MemberRegisterService(Membe Dao memberDao) {
//주입 받은 객체를 필드에 할당
this.memberDao = memberDao;
}
@Bean
public MemberDao memberDao() {
return new MemberDao() ;
}
@Bean
public MemberRegisterService memberRegSvc() {
return new MemberRegisterService(memberDao()) ,
}
생성자에 전달할 의존 객체가 두 개 이상이어도 동일한 방식으로 주입하면 된다.
package org.example.chap03;
import java.util.Collection;
public class MemberListPrinter {
private MemberDao memberDao;
private MemberPrinter printer;
// 생성자 주입 방식
public MemberListPrinter(MemberDao memberDao, MemberPrinter printer){
this.memberDao = memberDao;
this.printer = printer;
}
public void printAll(){
Collection<Member> members = memberDao.selectAll();
members.forEach(m -> printer.print(m));
}
}
@Bean
public MemberPrinter memberPrinter(){
return new MemberPrinter();
}
@Bean
public MemberListPrinter listPrinter(){
return new MemberListPrinter(memberDao(), memberPrinter());
}
DI 방식2 : Setter 메서드 방식
생성자 외에 세터 메서드를 이용해서 객체를 주입받기도 한다. 일반적인 세터(setter) 메서드는 자바빈 규칙에 따라 다음과 같이 작성한다.
- 메서드 이름이 set으로 시작한다.
- set 뒤에 첫 글자는 대문자로 시작한다.
- 파라미터가 1 개이다.
- 리턴 타입이 void 이다 .
package org.example.chap03;
import org.example.chap03.Member;
import org.example.chap03.MemberDao;
import org.example.chap03.MemberPrinter;
/*
* 설정 메서드 주입 방식
* : 세터 메서드이름을 통해 어떤 의존 객체가 주입되는지 알 수 있다.
* */
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();
}
public void setMemberDao(MemberDao memberDao){
this.memDao = memberDao;
}
public void setPrinter(MemberPrinter printer){
this.printer = printer;
}
}
@Bean
public MemberInfoPrinter infoPrinter(){
MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
infoPrinter.setMemberDao(memberDao());
infoPrinter.setPrinter(memberPrinter());
return infoPrinter;
}
기본 데이터 타입 값 설정
이 코드는 두 개의 int 타입 값을 세터 메서드로 전달받는다.
package org.example.chap03;
public class VersionPrinter {
private int majorVersion;
private int minorVersion;
public void print(){
System.out.printf("이 프로그램의 버전은 %d.%d입니다.\\n\\n", majorVersion, minorVersion);
}
public void setMajorVersion(int majorVersion){
this.majorVersion = majorVersion;
}
public void setMinorVersion(int minorVersion){
this.minorVersion = minorVersion;
}
}
@Bean
public VersionPrinter versionPrinter(){
VersionPrinter versionPrinter = new VersionPrinter();
versionPrinter.setMajorVersion(5);
versionPrinter.setMinorVersion(0);
return versionPrinter;
}
int, long과 같은 기본 데이터 타입과 String 타입의 값은 일반 코드처럼 값을 설정하면 된다
@Configuration 설정 클래스의 @Bean 설정과 싱글톤
스프링 컨데이너가 생성한 빈은 싱글톤 객체이다. 스프링 컨데이너는 @Bean이 붙은 메서드 에 대해 한 개의 객체만 생성한다. 이는 한 번 생성한 객체를 보관했다가 이후에도 동일한 객체를 리턴하겠다는 말이다.
두 개 이상의 설정 파일 사용하기
개발을 하다보면 수백여개 이상의 빈을 설정하게 된다. 때문에 한 개의 클래스 파일에 설정하는 것보다 영역 별로 설정 파일을 나무면 관리하기 편해진다.
package org.example.chap03.main.config;
import org.example.chap03.MemberDao;
import org.example.chap03.MemberPrinter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConf1 {
@Bean
public MemberDao memberDao(){
return new MemberDao();
}
@Bean
public MemberPrinter memberPrinter(){
return new MemberPrinter();
}
}
package org.example.chap03.main.config;
import org.example.chap03.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConf2 {
@Autowired
private MemberDao memberDao;
@Autowired
private MemberPrinter memberPrinter;
@Bean
public MemberRegisterService memberRegisterSvc(){
//@Autowired 애노테이션이 붙은 필드이므로 바로 사용하면 됨.
return new MemberRegisterService(**memberDao**);
}
@Bean
public ChangePasswordService changePasswordSvc(){
ChangePasswordService pwdSvc = new ChangePasswordService();
pwdSvc.setMemberDao(**memberDao**);
return pwdSvc;
}
@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;
}
}
@Autowired 애노테이션은 스프링의 자동 주입 기능을 위한 것이다. 해당 타입의 빈을 찾아서 필드에 할당한다. AppConf1 에 MemberDao 타입의 빈을 설정했으므로 AppConf2 클래스의 memberDao 필드는 AppConf1 클래스에서 설정한 빈이 할당된다.
//두 설정을 사용하겠다면, 컨테이너 생성자에 파라미터 두개를 넣어주면 된다.
ctx = new AnnotationConfigApplicationContext(AppConf1 .class, AppConf2.class):
@Configuration 애노테이션, 빈, @Autowired 애노테이션
public class MemberlnfoPrinter {
@Autowired
private MemberDao memDao;
@Autowired
private MemberPrinter printer;
public void printMemberlnfo(String email) {
Member member = memDao.selectByEmail(email);
if (member == null) {
System.out.println( 데 이 터 없음\\n );
return;
}
printer.print(member);
System.out.println();
}
… 세터 생략
}
@Autowired 애노테이션을 의존 주입 대상에 붙이면 스프링 설정 클래스의 @Bean 메서드 에서 의존 주입을 위한 코드를 작성하지 않아도 된다.
@Bean
public MemberlnfoPrinter infoPrinter() {
MemberlnfoPrinter infoPrinter = new MemberlnfoPrinter();
//세터 메서드 를 사용해서 의존 을 하지 않아도
//스프링 컨테이너가 Autowired 롤 붙인 필드에
//자동으로 해당 타입의 빈 객체를 주입
return infoPrinter;
}
AppConf2.java 클래스를 다시 보자. 거기서는 설정에 @Autowired 애노테이션을 사용했다.
@Configuration
public class AppConf2 {
@Autowired
private MemberDao memberDao;
@Autowired
private MemberPrinter memberPrinter;
스프링 컨테이너는 설정 클래스에서 사용한 @Autowired 에 대해서도 자동 주입을 처리한다. 실제로 스프링은 @Configuration 애노테이션이 붙은 설정 클래스를 내부적으로 스프링 빈으로 등록한다. 그리고 다른 빈과 마찬가지로 @Autowired가 붙은 대상에 대해 알맞은 빈을 자동으로 주입한다.
즉 스프링 컨테이너는 AppConf2 객체를 빈으로 등록하고. @Autowired 애노테이션이 붙은 두 필드_memberDao와 memberPrinter- 에 해당 타입의 빈 객체를 주입한다.
@lmport 애노테이션 사용
두 개 이상의 설정 파일을 사용하는 또 다른 방법은 @Import 애노데이션을 사용하는 것이다. @Import 애노테이션은 함께 사용할 설정를 지정한다.
package org.example.chap03.main.config;
import org.example.chap03.MemberDao;
import org.example.chap03.MemberPrinter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import(AppConf2.class)
public class AppConfImport {
@Bean
public MemberDao memberDao(){
return new MemberDao();
}
@Bean
public MemberPrinter memberPrinter(){
return new MemberPrinter();
}
}
public static void main(String [ ] args) throws IOException {
ctx = new AnnotationConflgApplicationContext(AppConflmport.class).
… 생략
}
@Import 애노테이션은 다음과 이 열을 이용해서 두 개 이상의 설정 클래스도 지정할 수 있다.
@Configuration
@Import( { AppConf1 .class, AppC:onf2.class } )
public class AppConflmport {
}
📌 MainForSpring에서 AppConflmport를 사용하도록 수정했다면, 이후 다른 설정 클래스를 추가해도 MainForSpring을 수정할 필요가 없다. Import를 사용해서 포함한 설정 클래스 가 다시 @Import를 사용할 수 있다. 이렇게 하면 설정 클래스를 변경해도 AnnotationConf igApplicationContex 를 생성하는 코드는 최상위 설정 클래스 한 개만 사용하면 된다.
getBean() 메서드 사용
메서드에 전달한 빈이름이 잘못되었을 때 출력되는 에러메시지
Exception in thread 11 main11 org . springframework. beans . factory. NoSuchBeanDefi nitionException :
No bean named 'versionPrinte 2' available
메서드에 전달한 타입이 잘못되었을 때 출력되는 에러 메시지
Exception in thread "main"
org.springframework.beans.factory.BeanNotOfRequiredTypeException:
Bean named 'listPrinter' is expected to be of type 'spring.VersionPrinter' but was
actually of type 'spring . MemberlistPrinter'
빈 이름을 지정하지 않고도 빈을 구할 수 있다.
VersionPrinter versionPrinter = ctx.getBean(MemberPrinter.class)
이때 해당 타입의 빈 객체가 한 개만 존재한다면 해당 빈을 구해서 리턴한다. 해당 타입의 빈 객체가 존재하지 않으면 다음과 같은 익셉션이 발생한다.
Exception in thread "main"
org.springframework.beans.factory.NoSuchBeanDefinitionException:
No qualifying bean of type 'spring.MemberPrinter' available
같은 타입의 빈 객체가 두개 이상 존재할 때 발생하는 에러 메시지
Exception in thread "main"
org . springframework. beans . factory . NoUniqueBeanDen n itionException :
No qualifying bean of type 'spring.VersionPrinter' available: expected single
matching bean but found 2: versionPrinter,oldVersionPrinter
주입 대상 객체를 모두 빈 객체로 설정해야 하나?
@Configuration
public class AppCtxNoMemberPrinterBean {
private MemberPrinter printer = new MemberPrinter(); // 빈 이 아님
··· 생략
@Bean
public MemberlistPrinter listPrinter() {
return new MemberlistPrinter(memberDao() , printer) ;
}
@Bean
public MemberlnfoPrinter infoPrinter() {
MemberlnfoPrinter infoPrinter = new MemberlnfoPrinterO ;
infoPrinter.setMemberDao(memberDao());
infoPrinter.setPrinter(printer) ;
return infoPrinter;
}
… 생략
}
MemberPrinter를 빈으로 등록하지 않았음에도 MemberlistPrinter 객체와 MemberlnfoPrinter 객체는 정상적으로 작동한다.
객체를 스프링 빈으로 등록할 때와 등록하지 않을 때의 차이는 스프링 컨테이너가 객체를 관리하는지 여부이다.
최근에는 의존 자동 주입 기능을 프로젝트 전반에 걸쳐 사용하는 추세이기 때문에 의존주입 대상은 스프링 빈으로 등록하는 것이 보통이다.
.
.
.
.
@Autowired는 지양해야 된다고 알고 있다. 다음챕터에서 다룰 내용이 @Autowired를 이용한 의존 자동 주입이라는데,, 흐음....
'Spring > 스프링5 프로그래밍 입문' 카테고리의 다른 글
Chapter7. AOP 프로그래밍 (0) | 2023.02.22 |
---|---|
Chapter6. 빈 라이프사이클과 범위 (0) | 2023.02.21 |
Chapter5. 컴포넌트 스캔 (0) | 2023.02.21 |
Chapter4. 의존 자동 주입 (0) | 2023.02.20 |
Chapter2. 스프링 시작하기 (0) | 2023.02.18 |