[스프링2] 3장. 스프링 DI
이번 장의 키워드
# 의존 주입(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. 빈칸, 선택 문제
- 의존 주입(DI)은 (세터 메서드) 방식과 (생성자) 방식이 있다.
- 객체를 main메서드에서 주입하는 것보다 의존 객체를 주입하기 위한 클래스를 따로 작성하는 것이 코드 유지보수에 더 효율적이다. 이때 의존 객체를 주입하기 위한 이 클래스를 (조립기)(이)라고 한다.
- (@Configuration) 애노테이션을 사용하여 설정 클래스로 설정하고 (@Bean) 애노테이션을 사용하여 스프링 빈 객체를 등록하는 메서드로 설정한다.
- 스프링 컨테이너를 생성하면 (getBean()) 메서드르 사용하여 빈 객체를 구할 수 있다.
- 스프링 빈이 생성한 빈 객체는 ( 싱글톤 ) 객체이다.
- 두 개 이상의 설정 파일을 사용하고 싶을 때 사용하는 방법은 총 2가지가 있다. 첫 번째는 ( @Autowired ) 애노테이션을 사용하여 자동 주입하는 방식, 두 번째는 ( @Import ) 애노테이션을 사용하여 함께 사용할 설정 클래스를 지정할 수 있다.
- 다음 코드 중, 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