한 클래스가 다른 클래스의 메서드를 실행할 때 이를 '의존'한다고 표현한다.
package spring;
import java.time.LocalDateTime;
public class MemberRegisterService {
private MemberDao memberDao = new MemberDao();
public Long regist(RegisterRequest req) {
Member member = memberDao.selectByEmail(req.getEmail());
if (member != null) {
throw new DuplicateMemberException("dup email " + req.getEmail());
}
Member newMember = new Member(
req.getEmail(), req.getPassword(), req.getName(),
LocalDateTime.now());
memberDao.insert(newMember);
return newMember.getId();
}
}
DI = Dependency Injection = 의존 주입 : 의존하는 객체를 직접 생성하는 대신 의존 객체를 전달 받는 방식이다.
package spring;
import java.time.LocalDateTime;
public class MemberRegisterService {
private MemberDao memberDao;
public MemberRegisterService(MemberDao memberDao) {
this.memberDao = memberDao;
}
public Long regist(RegisterRequest req) {
Member member = memberDao.selectByEmail(req.getEmail());
if (member != null) {
throw new DuplicateMemberException("dup email " + req.getEmail());
}
Member newMember = new Member(
req.getEmail(), req.getPassword(), req.getName(),
LocalDateTime.now());
memberDao.insert(newMember);
return newMember.getId();
}
}
DI를 사용하면 A객체를 사용하는 클래스가 세 개여도 변경할 곳은 의존 주입 대상이 되는 객체를 생성하는 코드 한 곳 뿐이다.
의존 객체를 직접 생성했던 방식을 사용하면 변경할 코드가 의존하는 객체를 생성하는 곳마다 변경해주어야 하는 불편함이 있다.
회원정보에 캐시를 사용하기 위해, MemberDao클래스에 상속받은 CachedMemberDao클래스를 만들었다.
기존의 MemberDao객체를 CachedMemberDao로 변경해야 하는 상황
❔ 변경 전
public class MemberRegisterService {
private MemberDao memberDao = new MemberDao();
...
}
public class ChangePasswordService {
prviate MemberDao memberDao = new MemberDao();
...
}
❕ 변경 후
public class MemberRegisterService {
private MemberDao memberDao = new CachedMemberDao();
...
}
public class ChangePasswordService {
prviate MemberDao memberDao = new CachedMemberDao();
...
}
❔ 변경 전
MemberDao memberDao = new MemberDao();
MemberRegisterService rgSc = new MemberRegisterService(memberDao);
ChangePasswordService pwSc = new ChangePasswordService(memberDao);
❕ 변경 후
MemberDao memberDao = new CachedMemberDao();
MemberRegisterService rgSc = new MemberRegisterService(memberDao);
ChangePasswordService pwSc = new ChangePasswordService(memberDao);
의존 객체를 주입한다는 것은 서로 다른 두 객체를 조립한다고 생각할 수 있는데, 이런 의미에서 이 클래스를
조립기라고도 표현한다.
package assembler;
import spring.ChangePasswordService;
import spring.MemberDao;
import spring.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;
}
}
이전에 학습한 내용처럼 MemberDao클래스가 아니라 MemberDao 클래스를 상속받는 CachedMemberDao클래스를
사용한다면 Assembler클래스에서 객체를 초기화하는 코드만 변경하면 된다.
스프링은 Assembler 클래스의 생성자 코드 처럼 필요한 객체를 생성하고,
생성한 객체에 의존을 주입한다.
또한 스프링은 Assembler#getMemberRegisterService() 메서드처럼 객체를 제공하는 기능을 정의하고있다.
차이점이라면 Assembler는 MemberRegisterService나 MemberDao와 같이 특정 타입의 클래스만 생성한 반면 스프링은 범용 조립기라는 점이다.
package config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.ChangePasswordService;
import spring.MemberDao;
import spring.MemberRegisterService;
@Configuration
public class AppCtx {
public AppCtx() {
}
@Bean
public MemberDao memberDao() {
return new MemberDao();
}
@Bean
public MemberRegisterService memberRegSvc() {
return new MemberRegisterService(this.memberDao());
}
@Bean
public ChangePasswordService changePwdSvc() {
ChangePasswordService pwdSvc = new ChangePasswordService();
pwdSvc.setMemberDao(this.memberDao());
return pwdSvc;
}
}
객체를 생성하고 의존 객체를 주입하는 것은 스프링 컨테이너 이므로 설정 클래스를 이용해서 컨테이너를 생성해야 한다.
AbstractApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);
AnnotationConfigApplicationContext 클래스를 이용해서 스프링 컨테이너를 생성한다.
컨테이너를 생성하면 getBean()메서드를 이용해서 사용할 객체를 구할 수 있다.
MemberRegisterService regSvc = ctx.getBean("memberRegSvc", MemberRegisterService.class);
스프링 컨테이너(ctx)로 부터 이름이 "memberRegSvc"인 빈 객체를 구한다.
package main;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import config.AppCtx;
import spring.ChangePasswordService;
import spring.DuplicateMemberException;
import spring.MemberNotFoundException;
import spring.MemberRegisterService;
import spring.RegisterRequest;
import spring.WrongIdPasswordException;
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");
}
}
private static void processChangeCommand(String[] arg) {
if (arg.length != 4) {
printHelp();
return;
}
ChangePasswordService changePwdSvc =
ctx.getBean("changePwdSvc", 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");
}
}
... printHelp 생략
}
🔽 0. spring/MemberDao.java 여러개의 객체가 주입 되기 때문에 한번에 찾아내는 메서드를 생성
pulbic class MemberDao {
public Collection<Member> selectAll() {
return map.value();
}
... 추가 기능
}
🔽 1. spring/testA.java 클래스를 추가(2개의 객체를 받는 생성자)
pulbic class teatA {
private MemberDao memberDao;
private MemberPrinter printer;
public testA(MemberDao memberDao, MemberPrinter printer) {
this.memberDao = memberDao;
this.printer = printer;
}
... 추가 기능
}
🔽 2. config/AppCtx.java(설정파일) 두 개 이상의 인자를 받는 생성자를 사용하는 설정 추가
import spring.testA;
@Bean
public testA listPrinter() {
return new testA(memberDao(), memberPrinter());//memberDao, memberPrinter 빈 주입
}
🔽 3. spring/MainForSpring.java(메인) testA관련 코드 추가
import spring.testA;
private static void testB() {
testA listPrinter = ctx.getBean("listPringer", testA.class);
...기능 사용
}
🔽 1. spring/testB.java setter메서드를 이용해서 의존 객체를 주입 받을 메서드 추가
pulbic class teatB {
private MemberDao memberDao;
private MemberPrinter printer;
public void setMemberDao(MemberDao memberDao) {
this.memberDao = memberDao;
}
public void setPrinter(MemberPrinter printer) {
this.printer = printer;
}
... 추가 기능
}
🔽 2. config/AppCtx.java(설정파일) setter 메서드 방식을 사용하는 설정 추가
import spring.testB;
@Bean
public testB infoPrinter() {
testB infoPrinter = new testB();
infoPrinter.setMemberDao(memberDao());//memberDao 빈 주입
infoPrinter.setPrinter(memberPrinter());//memberPrinter 빈 주입
return infoPrinter;
}
🔽 3. spring/MainForSpring.java(메인) testA관련 코드 추가
import spring.testB;
private static void testB() {
testB infoPrinter = ctx.getBean("infoPrinter", testB.class);
...기능 사용
}
생성자 방식은 어떤 의존 객체를 설정하는지 알아내려면 생성자의 코드를 확인해야 한다. 하지만 setter 메서드 방식은
메서드의 이름으로 유추할 수 있다.
반면에 생성자 방식은 빈 객체를 생성하는 시점에 필요한 모든 의존 객체를 주입 받기 때문에 객체를 사용할 때 완전한 상태로
사용할 수 있다. setter메서드 방식은 setter메서드를 사용해서 필요한 의존 객체를 전달 하지 않아도 빈 객체가 생성되기 때문에 객체를 사용하는 시점에서
NullPointerException이 발생할 수 있다.
스프링 컨테이너가 생성한 빈은 싱글톤 객체이다.
다른 설정 메서드에서 memberDao()를 몇 번 호출하더라도 항상 같은 객체를 리턴한다는 것을 의미한다.
@Configuration//중복된 memberDao()를 호출하는 예시
public class AppCtx {
@Bean
public MemberDao memberDao() {
return new MemberDao();
}
@Bean
public MemberRegisterService memberRegSvc() {
return new MemberRegisterService(memberDao());
}
@Bean
public ChangePasswordService changePwdSvc() {
ChangePasswordService pwdSvc = new ChangePasswordService();
pwdSvc.setMemberDao(memberDao());
return pwdSvc;
}
}
public class AppCtxExt extends AppCtx {//스프링 런타임에 생성한 설정 클래스 예시
private Map<String, Object> beans = ...;
@Override
public MemberDao memberDao() {
if(!beans.containsKey("memberDao"))
beans.put("memberDao", super.memberDao());
return (MemberDao) beans.get("memberDao");
}
}
package config; //AppCtx.java의 빈 설정을 나눠서 한다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.MemberDao;
import spring.MemberPrinter;
@Configuration
public class AppConf1 {
//생성자 , setter 메서드 방식으로 빈객체를 주입하지 않았다.
@Bean
public MemberDao memberDao() {
return new MemberDao();
}
@Bean
public MemberPrinter memberPrinter() {
return new MemberPrinter();
}
}
package config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.ChangePasswordService;
import spring.MemberDao;
import spring.MemberInfoPrinter;
import spring.MemberListPrinter;
import spring.MemberPrinter;
import spring.MemberRegisterService;
import spring.VersionPrinter;
@Configuration
public class AppConf2 {
//@Autowired 애노테이션을 사용해서 빈객체를 주입한다.
@Autowired
private 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;
}
@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;
}
}
AppConf1 클래스에서 MemberDao 타입의 빈을 설정했으므로 AppConf2 클래스의 memberDao 필드에는
AppConf1 클래스에서 설정한 빈이 할당된다.
ctx = new AnnotationConfigApplicationContext(AppConf1.class, AppConf2.class );
@Autowried 애노테이션을 붙이게되면 스프링 컨테이너 설정 파일인 @Configuration이 붙어있는 @Bean객체에서
생성자 주입, setter 메서드 방식 주입으로 객체를 주입하지않아도 스프링 컨테이너가 알아서 객체를 주입한다.
VersionPrinter versionPrinter = ctx.getBean("versionPrinter", VersionPrinter.class);
// VersionPrinter.class 의 빈 객체가 한개만 존재하는 경우 빈 객체의 이름 생략 버전
VersionPrinter versionPrinter = ctx.getBean(VersionPrinter.class);
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.VersionPrinter;
@Configuration
public class AppCtx4GetBean {
private MemberPrinter printer = new MemberPrinter();
@Bean//versionPrinter 빈 객체, VersionPrinter 타입
public VersionPrinter versionPrinter() {// VersionPrinter라는 타입의 빈 객체를 2개 생성
VersionPrinter versionPrinter = new VersionPrinter();
versionPrinter.setMajorVersion(5);
versionPrinter.setMinorVersion(0);
return versionPrinter;
}
@Bean//oldVersionPrinter 빈 객체, VersionPrinter 타입
public VersionPrinter oldVersionPrinter() {
VersionPrinter versionPrinter = new VersionPrinter();
versionPrinter.setMajorVersion(4);
versionPrinter.setMinorVersion(3);
return versionPrinter;
}
}
package config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.MemberDao;
import spring.MemberInfoPrinter;
import spring.MemberPrinter;
@Configuration
public class AppCtxNoMemberPrinterBean {
private MemberPrinter printer = new MemberPrinter(); // 빈으로 등록해주 읺은 객체
@Bean
public MemberDao memberDao() {
return new MemberDao();
}
@Bean
public MemberInfoPrinter infoPrinter() {
MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
infoPrinter.setMemberDao(memberDao());
infoPrinter.setPrinter(printer);
return infoPrinter;
}
}