상세 컨텐츠

본문 제목

[스프링2] 3장. 스프링 DI

22-23/22-23 Spring 2

by YUZ 유즈 2022. 10. 6. 10:32

본문

728x90

이번 장의 키워드

# 의존 주입(DI)

# 조립기 

# 스프링 빈(Bean) 

# 싱글톤 

# Autowired 애노테이션

 

프로젝트 생성 및 실행 > 생략


의존 주입 DI(Dependency Injection)

  • 의존(dependency): 객체 간의 의존
  • 변경에 의해 영향을 받는 관계
  • ex) MemberDao의 메서드의 이름을 수정하면 MemberDao의 메서드를 사용하고 있는 MemberRegisterService 클래스의 소스 코드도 변경된다.
public class MemberRegisterService {
	...
    
	public Long regist(RegisterRequest req) {
		Member member = memberDao.selectByEmail(req.getEmail()); // memberDao 객체의 메서드 호출
       	...
		Member newMember = new Member(
				req.getEmail(), req.getPassword(), req.getName(), 
				LocalDateTime.now());
		memberDao.insert(newMember); // memberDao 객체의 메서드 호출
		
        return member.getId();
	}
}
  • MemberRegisterService가 MemberDao 객체의 selectByEmail을 호출
  • MemberRegisterService가 MemberDao에 클래스에 의존한다.

 

1. DI(Dependency Injection)를 통한 의존 처리

// 객체를 직접 생성하는 방식
	public class MemberRegisterService() {
		private MemberDao memberDao = new MemberDao();
	}
	

// 의존 객체를 전달받는 방식
	private MemberDao memberDao;

	public MemberRegisterService(MemberDao memberDao) {
		this.memberDao = memberDao;
	}

 

2. DI와 의존 객체 변경의 유연함

가정: MemberDao 클래스가 회원 데이터를 DB에 저장

  • 회원 데이터의 조회를 위해 캐시를 적용해야 한다.
  • ChachedMemberDao 클래스 생성

# 캐시: 데이터 값을 복사해놓은 임시 장소, 조회 속도 향상을 위해 캐시를 사용한다.

// 객체 의존 주입을 사용했을 때
	//MemberDao memberDao = new MemberDao();
    MemberDao memberDao = new CachedMemberDao(); // 이 부분만 바꿔주면 된다.
    MemberRegisterService memberRegisterService = new MemberRegisterService(memberDao);
    ChangePasswordService service = new ChangePasswordService(memberDao);


// new를 사용하여 객체를 생성했을 때
// new를 사용하여 객체를 생성한 코드를 모두 수정해야 한다.

// MemberRegisterService.java
public class MemberRegisterService{
	MemberDao memberDao = new CachedMemberDao();
}

// ChangePasswordService.java
public class ChangePasswordService{
	MemberDao memberDao = new CachedMemberDao();
}

 

DI 방식에 대해서

  • 생성자 방식
  • 세터 메서드 방식

1. 생성자 방식

public class MemberRegisterService {
	private MemberDao memberDao;

    // 생성자를 통해 의존 객체 주입
	public MemberRegisterService(MemberDao memberDao) {
		this.memberDao = memberDao; // 객체 필드에 할당
	}
    
    public Long regist(RegisterRequest req) {
	...
    
    return newMember.getId(); // 주입받은 객체의 메서드 사용
    
}

 

2. 세터 메서드 방식

세터 메서드의 규칙

  • 메서드 이름이 set으로 시작
  • set 뒤에 첫 글자는 대문자로 시작
  • 파라미터가 1개
  • 리턴 타입이 void
public class MemberInfoPrinter {

	private MemberDao memberDao;
	private MemberPrinter printer;
	...

	// setter
	public void setMemberDao(MemberDao memberDao) {
		this.memDao = memberDao;
	}
	
   	 // setter
	public void setPrinter(MemberPrinter printer) {
		this.printer = printer;
	}

}

 

객체 조립기(Assembler)

의존 객체를 주입하다 = 서로 다른 객체를 조립하다

  • 객체를 생성하고 의존 객체를 주입하는 기능
  • 특정 객체가 필요한 곳에 객체를 제공
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); // 의존 객체 주입
	}
    
    	// MemberDao 객체가 필요한 곳에 객체를 제공하는 메서드
	public MemberDao getMemberDao() {
		return memberDao;
	}

	// MemberRegisterService 객체가 필요한 곳에 객체를 제공하는 메서드
	public MemberRegisterService getMemberRegisterService() {
		return regSvc; 
	}

	// ChangePasswordService 객체가 필요한 곳에 객체를 제공하는 메서드
	public ChangePasswordService getChangePasswordService() {
		return pwdSvc;
	}

}

 

1. Assembler을 사용하는 코드

main에서 Assembler 클래스를 호출하여 객체를 생성한다.

public class MainForAssembler {

	private static Assembler assembler = new Assembler();

	private static void processNewCommand(String[] arg) {
		if (arg.length != 5) {
			printHelp();
			return;
		}
        // Assembler 클래스에 있는 getMemberRegisterService 메서드 호출
		MemberRegisterService regSvc = assembler.getMemberRegisterService();
        
        	try {
			regSvc.regist(req); // 객체가 갖는 regist 메서드 호출
			System.out.println("등록했습니다.\n");
            
		} catch (DuplicateMemberException e) {
			System.out.println("이미 존재하는 이메일입니다.\n");
		}
}

 

스프링을 이용한 객체 조립: 스프링 빈(Bean)

스프링은 "DI를 지원하는 조립기"

 

1. 위에서 구현한 Assembler와 다른 점은 무엇일까요?

  • Assembler: MemberDao와 같은 특정 타입의 클래스만 생성
  • Spring: 범용 조립기
@Configuration // 스프링 설정 클래스로 설정
public class AppCtx {

	@Bean 
	public MemberDao memberDao() {
		return new MemberDao();
	}
	
	@Bean
	public MemberRegisterService memberRegSvc() {
		return new MemberRegisterService(memberDao()); // memberDao 객체 주입
  }
  • @Configuration: 스프링 설정 클래스
  • @Bean: 해당 메서드가 생성한 객체, 각 메서드마다 하나의 빈(Bean) 객체를 생성

2. 스프링 컨테이너 생성하기

  • 객체를 생성, 의존 객체를 주입하는 것은 스프링 컨테이너이다.
  • 설정 클래스(AnnotationConfigApplicationContext)를 이용하여 컨테이너를 생성해야 한다.
  • 컨테이너를 생성하면 getBean()을 이용하여 사용할 객체를 구할 수 있다.
	private static ApplicationContext ctx = null;
	
	public static void main(String[] args) throws IOException {
    	// 스프링 컨테이너 생성
		ctx = new AnnotationConfigApplicationContext(AppCtx.class);
        
        // 빈 객체 구하기
        MemberRegisterService regSvc = 
				ctx.getBean("memberRegSvc", MemberRegisterService.class);
                // getBean("메서드 name", "메서드 반환 type")
  	}

3. getBean() 메서드 사용 시 발생할 수 있는 오류

ctx.getBean("Printer", Printer.class);

// situation 1
@Bean 
public VersionPrinter Printer(){ }


// situation 2
ctx.getBean(Printer.class);

@Bean 
public Printer Printer1(){ }
@Bean 
public Printer Printer2(){ }

[ sit1 ] 실제 타입과 getBean() 메서드에 지정한 타입이 다른 경우

 

[ sit2 ] 빈 이름을 지정하지 않았을 때, 같은 타입의 빈 객체가 2개 이상 존재할 때

 ✏️ 빈 이름을 지정하지 않고 타입만으로 빈을 구할 수 있다.

 ✏️ 해당 타입의 빈이 1개만 존재할 때 해당 빈을 구해서 리턴한다. 오류 발생X

@Configuration 설정 클래스의 @Bean 설정과 싱글톤

@Configuration
public class AppCtx {

	@Bean
	public MemberDao memberDao() {
		return new MemberDao(); // MemberDao 객체 생성
	}
	
	@Bean
	public MemberRegisterService memberRegSvc() {
		return new MemberRegisterService(memberDao()); // MemberDao 메서드 실행
	}
	
	@Bean
	public ChangePasswordService changePwdSvc() {
		ChangePasswordService pwdSvc = new ChangePasswordService();
		pwdSvc.setMemberDao(memberDao()); // MemberDao 메서드 실행
		return pwdSvc;
	}
  }
  • memberRegisterService에서 생성한 MemberRegisterService 객체, ChangePasswordService에서 생성한 changePasswordService 객체는 서로 다른 memberDao 객체를 사용하는 것인가?

스프링 컨테이너가 생성한 빈(Bean)은 싱글톤 객체이다.

 = 다른 설정 메서드에서 memberDao()를 n번 호출하여도 항상 같은 객체를 리턴

한 번 생성한 객체를 보관했다가 이후에 동일한 객체 리턴(교재 92p 참고)

 

1. 생성자 vs 세터 메서드

생성자 방식

  • 빈 객체를 생성하는 시점에 모든 의존 객체 주입
  • 생성자의 파라미터 수가 많을 경우(=주입할 객체가 많을 경우), 어떤 객체를 생성하는지 확인하려면 생성자 코드 확인 필요

세터 메서드 방식

  • 세터 메서드 이름을 통해 어떤 의존 객체가 주입하는지 알 수 있음
  • 세터 메서드를 사용하여 의존 객체를 전달하지 않아도 빈 객체 생성, NullPointerException 발생

 

2. 주입 대상 객체를 모두 빈 객체로 설정해야 할까요?

주입할 객체가 꼭 스프링 빈일 필요는 없다.

@Configuration
public class AppCtxNoMemberPrinterBean {
	private MemberPrinter printer = new MemberPrinter(); // 스프링 빈이 아니다.
	
	@Bean
	public MemberDao memberDao() {
		return new MemberDao();
	}
	
	@Bean
	public MemberListPrinter listPrinter() {
		return new MemberListPrinter(memberDao(), printer); // 일반 객체 주입
	}
	
	@Bean
	public MemberInfoPrinter infoPrinter() {
		MemberInfoPrinter infoPrinter = new MemberInfoPrinter(); // 스프링 빈이 아니다.
		infoPrinter.setMemberDao(memberDao());
		infoPrinter.setPrinter(printer); // 일반 객체 주입
		return infoPrinter;
	}

}

// 메인.js
MemberPrinter printer = ctx.getBean(MemberPrinter.class);
// 스프링 빈으로 등록되어 있지 않아 에러 발생

 

두 개 이상의 설정 파일 사용하기

영역별로 설정 파일을 나누어 관리하면 스프링 빈을 관리하기 용이하다.

 

1. @Autowired 애노테이션

  • 스프링의 자동 주입 기능을 수행
  • 해당 타입의 빈을 찾아서 필드에 할당
  • 스프링 빈에 의존하는 다른 빈을 자동으로 주입하고 싶을 때 사용

 

  • @Configuration 애노테이션이 붙은 설정 클래스를 스프링 빈으로 등록
    • ✏️ @Configuration 애노테이션이 붙은 설정 클래스는 내부적으로 스프링 빈에 등록한다.
  • @Autowired 애노테이션이 붙은 필드에 해당하는 타입의 빈 객체 주입
// MainForSpring2.java 
ctx = new AnnotationConfigApplicationContext(AppConf1.class, AppConf2.class);
// 인자에 설정 클래스를 넣어 전달해준다.

// AppConf1.java
@Configuration
public class AppConf1 {

	@Bean
	public MemberDao memberDao() {
		return new MemberDao();
	}
	
	@Bean
	public MemberPrinter memberPrinter() {
		return new MemberPrinter();
	}
	
}

// AppConf2.java
@Configuration
public class AppConf2 {
	@Autowired // 스프링의 자동 주입 기능
	private MemberDao memberDao;
    // 스프링 컨테이너가 MemberDao 타입의 빈을 memberDao 필드에 할당
	@Autowired
	private MemberPrinter memberPrinter;
	
	@Bean
	public MemberRegisterService memberRegSvc() {
		return new MemberRegisterService(memberDao);
	}
	
	@Bean
	public ChangePasswordService changePwdSvc() {
		ChangePasswordService pwdSvc = new ChangePasswordService();
		pwdSvc.setMemberDao(memberDao);
		return pwdSvc;
	}
 }

 

 

2. @Import 애노테이션 

  • @Import 애노테이션으로 지정한 AppConf2를 설정 클래스로 함께 사용한다.
  • @Configuration으로 애노테이션을 사용하여 AppConf2를 설정 클래스로 지정할 필요가 없다.
  • 배열을 사용하여 2개 이상의 설정 클래스 지정 가능
// MainForImport.java
ctx = new AnnotationConfigApplicationContext(AppConfImport.class);

// AppConfImport.java
@Configuration
@Import(AppConf2.class) // AppConf2 설정도 함께 사용
@Import({AppConf1.class, AppConf2.class}) // 2개 이상의 설정 클래스 지정
public class AppConfImport. {

	@Bean
	public MemberDao memberDao() {
		return new MemberDao();
	}
	
	@Bean
	public MemberPrinter memberPrinter() {
		return new MemberPrinter();
	}
}

 


QUIZ

1. 빈칸, 선택 문제

  1. 의존 주입(DI)은 (세터 메서드) 방식과 (생성자) 방식이 있다.
  2. 객체를 main메서드에서 주입하는 것보다 의존 객체를 주입하기 위한 클래스를 따로 작성하는 것이 코드 유지보수에 더 효율적이다. 이때 의존 객체를 주입하기 위한 이 클래스를 (조립기)(이)라고 한다.
  3. (@Configuration) 애노테이션을 사용하여 설정 클래스로 설정하고 (@Bean) 애노테이션을 사용하여 스프링 빈 객체를 등록하는 메서드로 설정한다.
  4. 스프링 컨테이너를 생성하면 (getBean()) 메서드르 사용하여 빈 객체를 구할 수 있다.
  5. 스프링 빈이 생성한 빈 객체는 ( 싱글톤 ) 객체이다.
  6. 두 개 이상의 설정 파일을 사용하고 싶을 때 사용하는 방법은 총 2가지가 있다. 첫 번째는 ( @Autowired ) 애노테이션을 사용하여 자동 주입하는 방식, 두 번째는 ( @Import ) 애노테이션을 사용하여 함께 사용할 설정 클래스를 지정할 수 있다.
  7. 다음 코드 중, getBean()을 사용하여 빈 객체를 불러오는 데 오류가 발생할 수 있는 코드를 골라주세요.
// AppCtxNoMemberPrinterBean 클래스
@Configuration
public class AppCtxNoMemberPrinterBean {
	private MemberPrinter printer = new MemberPrinter();
	
	@Bean
	public MemberDao memberDao1() {
		return new MemberDao();
	}
    
   	 @Bean
	public MemberDao memberDao2() {
		return new MemberDao();
	}
	
	@Bean
	public MemberListPrinter listPrinter() {
		return new MemberListPrinter(memberDao(), printer);
	}
	
	@Bean
	public MemberInfoPrinter infoPrinter() {
		MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
		infoPrinter.setMemberDao(memberDao());
		infoPrinter.setPrinter(printer); // 일반 객체 주입
		return infoPrinter;
	}

}

// 메인.js
MemberPrinter printer = ctx.getBean(MemberInfoPrinter.class); // 1
MemberPrinter printer = ctx.getBean("memberDao1", MemberInfoPrinter.class); // 2
MemberPrinter printer = ctx.getBean("listPrinter", MemberListPrinter.class); // 3
MemberPrinter printer = ctx.getBean(MemberDao.class); // 4

정답: 2번, 4번

 

2. 코드 문제

1. 스프링 컨테이너를 생성하고 빈 객체를 불러오는 것 까지의 코드를 작성해보자.

// AppCtxNoMemberPrinterBean.class
@Configuration
public class AppCtxNoMemberPrinterBean {
	
	@Bean
	public MemberDao memberDao() {
		return new MemberDao();
	}

}

// 메인.java

// 스프링 컨테이너 생성, 2장에서 사용한 AnnotationConfigApplicationContext 클래스를 이용해서
// 스프링 컨테이너를 생성할 수 있다.
ApplicatonContext ctx = 
// 스프링 빈 객체 얻기, getBean()을 사용하여 MemberDao 타입의 스프링 빈 객체를 얻는다.


// 정답
new AnnotationConfigApplicationContext(AppCtxNoMemberPrinterBean.class);
MemberDao 객체 이름 = ctx.getBean("memberDao", MemberDao.class);

 

2.  2개 이상의 설정 파일을 사용한 스프링 컨테이너를 생성하는 코드를 작성해봅시다. @Autowired 애노테이션을 사용합니다. 

// AppConf1.java
@Configuration
public class AppConf1 {

	@Bean
	public MemberDao memberDao() {
		return new MemberDao();
	}
	
	@Bean
	public MemberPrinter memberPrinter() {
		return new MemberPrinter();
	}
	
}

// AppConf2.java
@Configuration
public class AppConf2 {

	private MemberDao memberDao;

	private MemberPrinter memberPrinter;
	
	@Bean
	public MemberRegisterService memberRegSvc() {
		return new MemberRegisterService(memberDao);
	}
	
	@Bean
	public ChangePasswordService changePwdSvc() {
		ChangePasswordService pwdSvc = new ChangePasswordService();
		pwdSvc.setMemberPrinter(memberPrinter);
		return pwdSvc;
	}
 }
 
 // MainForSpring2.java 
ctx = new AnnotationConfigApplicationContext(/*빈칸을 채워주세요*/);

Corner Spring #2

Editor : Otcr

 

728x90

관련글 더보기