source
1. 의존이란 ?
DI (Dependency Injection) : 의존 주입
•
한 클래스가 다른 클래스의 메서드를 실행할 때 이를 "의존" 이라고 한다.
package spring;
import java.time.LocalDateTime;
public class MemberRegisterService {
private MemberDao memberDao = new MemberDao();
public void regist(RegisterRequest req) {
// 이메일로 회원 데이터(Member) 조회
Member member = memberDao.selectByEmail(req.getEmail());
if (member != null) {
throw new DuplicateMemberException("dup email " + req.getEmail());
}
// 같은 이메일을 가진 회원이 존재하지 않으면 DB에 삽입
Member newMember = new Member(
req.getEmail(), req.getPassword(), req.getName(),
LocalDateTime.now());
memberDao.insert(newMember);
}
}
Java
복사
MemberDao의 insert() 메서드의 이름을 insertMember()로 변경하면 이 메서드를 사용하는 MemberRegisterService클래스의 소스 코드도 함께 변경 된다.
변경에 따른 영향이 전파되는 관계를 '의존'한다고 표현 한다.
의존 대상이 있으며 그 대상을 구하는 방법이 필요
•
의존 대상 객체를 직접 생성하여, 필드에 할당
source
•
"MemberRegisterService" 클래스에 MemberDao를 의존시켰기 때문에 MemberDao 객체도 함께 생성
source
클래스 내부에서 직접 의존 객체를 생성하는 것이 쉽긴 하지만 유지보수 관점에서 문제점 유발
2. DI를 통한 의존 처리
의존 객체를 전달 받는 방식
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();
}
}
Java
복사
직접 생성하는 방식
public class MemberRegisterService{
// 의존 객체를 직접 생성
private MemberDao memberDao = new MemberDao();
...
}
Java
복사
//의존하는 MemberDao의 객체도 함께 생성
MemberRegisterService svc = new MemberRegisterService();
Java
복사
전달 받는 방식
//생성자를 통해 MemberDao 전달 받는 방식
public MemberRegisterService(MemberDao memberDao) {
this.memberDao = memberDao;
}
Java
복사
MemberDao dao = new MemberDao();
// 의존 객체를 생성자를 통해 주입한다.
MemberRegisterService svc = new MemberRegisterService(dao);
Java
복사
3. DI와 의존 객체 변경의 유연함
의존 객체를 직접 생성
•
의존 객체를 직접 생성하는 방식은 필드나 생성자에서 new 연산자를 이용해서 객체를 생성한다.
•
의존 관계가 있는 클래스의 변경이 생기면 관련 클래스 모두 소스 코드를 변경 해야해 유연함이 떨어진다.
의존 객체 직접 생성
public class MemberRegisterService {
private MemberDao memberDao = new MemberDao();
}
Java
복사
public class ChangePasswordService(){
private MemberDao memberDao = new MemberDao();
...
}
Java
복사
public class CachedMemberDao extends MemberDao{
...
}
Java
복사
의존 된 클래스 모두 소스 코드를 변경 해야한다.
public class MemberRegisterService {
//private MemberDao memberDao = new MemberDao();
private MemberDao memberDao = new CachedMemberDao();
}
Java
복사
public class ChangePasswordService(){
//private MemberDao memberDao = new MemberDao();
private MemberDao memberDao = new CachedMemberDao();
}
Java
복사
Dependency Injection
•
동일한 상황에서 DI를 활용하면 코드 수정이 줄어든다.
•
생성자를 통한 의존 객체 주입
public class MemberRegisterService {
private MemberDao memberDao;
public MemberRegisterService(MemberDao memberDao){
this.memberDao = memberDao;
}
...
}
Java
복사
public class ChangePasswordService{
private MemberDao memberDao;
public ChangePasswordService(MemberDao memberDao){
this.memberDao = memberDao;
}
...
}
Java
복사
객체를 생성하는 코드만 수정하면 된다.
MemberDao memberDao = new MemberDao();
MemberRegisterService regSvc = new MemberRegisterService(memberDao);
ChangePasswordService pwdSvc = new ChangePasswordService(memberDao);
Java
복사
MemberDao memberDao = new CachedMemberDao();
MemberRegisterService regSvc = new MemberRegisterService(memberDao);
ChangePasswordService pwdSvc = new ChangePasswordService(memberDao);
Java
복사
4. 예제 프로젝트 만들기
•
회원 데이터 관련 클래스
◦
Member
◦
WrongldPasswordException
◦
MemberDao
•
회원 가입 처리 클래스
◦
DuplicateMemberException
◦
RegisterRequest
◦
MemberRegisterService
•
암호 관련 클래스
◦
MemberNotFoundException
◦
CahngePasswordService
4.1 회원 데이터 관련 클래스
Member.java
source
WrongldPasswordException.java
source
MemberDao
source
4.2 회원 가입 처리 관련 클래스
DuplicateMemberException.java
source
RegisterRequest.java
source
MemberRegisterService.java
source
4.3 암호 변경 관련 클래스
ChangePasswordService.java
source
5. 객체 조립기
조립기 (Assembler)
•
객체를 주입하려면 객체가 생성 되는 클래스가 필요하다.
•
메인 메서드에 만들면 된다 ?
◦
객체 주입을 사용하기 위해 클래스 내에 여러개의 객체가 생성 된다면 코드 관리 유연함이 떨어진다.
public class Main{
public static void main(String[] args){
MemberDao memberDao = new MemberDao();
MemberRegisterService regSvc = new MemberRegisterService(memberDao);
ChangePasswordService pwdSvc = new ChangePasswordService();
pwdSvc.setMemberDao(memberDao);
..//regSvc와 pwdSvc를 사용하는 코드
Java
복사
Assembler.java
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를 상속 받은 클래스를 사용해야한다면, 객체를 초기화하는 코드만 변경하면 된다.
memberDao = new MemberDao();
//memberDao = new CachedMemberDao();
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;
}
}
Java
복사
5.1 조립기 사용 예제
•
Assembler 클래스를 만들었으니 이를 사용하는 메인 클래스를 확인
source
6. 스프링의 DI 설정
•
스프링이 DI를 지원하는 조립기이다 !
◦
스프링은 Assembler 클래스의 생성자 코드처럼 필요한 객체를 생성하고 생성한 객체에 의존을 주입한다.
◦
Assembler#getMemberRegisterService() 메서드처럼 객체를 제공하는 기능을 정의하고 있다.
◦
스프링을 사용하지 않으면 DI를 활용하는 모든 클래스의 의존 관계를 만들어주어야하지만, 스프링은 의존 관계를 범용적으로 할 수 있다.
6.1 스프링을 이용한 객체 조립과 사용
•
스프링이 어떤 객체를 생성하고, 의존을 어떻게 주입할지를 정의한 설정 정보를 작성해야 한다.
•
스프링을 이용하기 위한 config source
◦
@Configration : 스프링 설정 클래스를 의미
◦
@Bean : 해당 메서드가 생성한 객체를 의미
source (Appctx.java)
•
스프링 설정 클래스 작성 다음은 스프링 컨테이너 생성
/* 컨테이너 생성 */
ApplicationContext ctx = AnnotaionConfigApplicationContext(AppCtx.class)
Java
복사
/* 컨테이너 생성을 통한 getBean() 메서드를 이용해서 객체 구하기 */
MemberRegisterService regSvc =
ctx.getBean("memberRegSvc",MemberRegisterService.class);
Java
복사
/* 스프링 설정 클래스 */
@Bean
public MemberRegisterService memberRegSvc(){
return new MemberRegisterService(memberDao());
}
@Bean
public MemberRegisterService memberRegSvc(){
return new MemberRegisterService(memberDao());
}
Java
복사
DI
package main;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import assembler.Assembler;
import spring.ChangePasswordService;
import spring.DuplicateMemberException;
import spring.MemberNotFoundException;
import spring.MemberRegisterService;
import spring.RegisterRequest;
import spring.WrongIdPasswordException;
public class MainForAssembler {
public static void main(String[] args) throws IOException {
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;
}
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();
}
}
Java
복사
스프링 DI
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;
}
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");
}
}
private static void printHelp() {
System.out.println();
System.out.println("잘못된 명령입니다. 아래 명령어 사용법을 확인하세요.");
System.out.println("명령어 사용법:");
System.out.println("new 이메일 이름 암호 암호확인");
System.out.println("change 이메일 현재비번 변경비번");
System.out.println();
}
}
Java
복사
6.2 DI 방식 1 : 생성자 방식
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();
}
}
Java
복사
...
public Collection<Member> selectAll() {
return map.values();
}
...
Java
복사
@Configuration
public class AppCtx {
@Bean
public MemberDao memberDao() {
return new MemberDao();
}
@Bean
public MemberRegisterService memberRegSvc() {
return new MemberRegisterService(memberDao());
}
...
}
Java
복사
6.3 DI 방식 2 : 세터 메서드 방식
•
일반적인 세터(setter) 메서드는 자바빈 규칙에 따라 다음과 같이 작성한다.
◦
메서드 이름이 set으로 시작한다.
◦
set 뒤에 첫 글자는 대문자로 시작한다.
◦
파라미터가 1개이다.
◦
리턴 타입이 void이다.
•
자바빈에서는 게터와 세터를 이용해서 프로퍼티를 정의한다.
◦
String getName() 메서드 : 읽기
◦
void setName(String name) 메서드 : 쓰기
package spring;
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;
}
}
Java
복사
@Bean
public MemberInfoPrinter infoPrinter() {
MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
infoPrinter.setMemberDao(memberDao());
infoPrinter.setPrinter(printer);
return infoPrinter;
}
Java
복사
public class MainForSpring {
private static ApplicationContext ctx = null;
public static void main(String[] args) throws IOException {
...
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 processVersionCommand() {
VersionPrinter versionPrinter =
ctx.getBean("versionPrinter", VersionPrinter.class);
versionPrinter.print();
}
}
Java
복사
6.4 기본 데이터 타입 값 설정
7. @Configuration 설정 클래스의 @Bean 설정과 싱글톤
•
스프링 컨테이너가 생성한 빈은 싱글톤 객체
•
스프링 컨테이너는 @Bean이 붙은 메서드에 대해 한 개의 객체만 생성
◦
스프링은 설정 클래스를 그대로 사용하지 않고 클래스를 상속한 새로운 설정 클래스를 만들어서 사용한다.
◦
런타임에 생성 된 설정 클래스는 memberDao() 메서드는 매번 새로운 객체를 생성하지 않는다.
▪
한 번 생성한 객체를 보관했다가 이후에는 동일한 객체를 리턴한다. (재사용)
싱글톤 예제
source
8. 두 개 이상의 설정 파일 사용하기
•
스프링 설정 클래스를 여러개로 관리할 수 있다.
◦
스프링을 이용해서 애플리케이션을 개발하다보면 적게는 수십 개에서 많게는 수백여 개 이상의 빈을 설정하게 된다.
◦
설정하는 빈의 개수가 증가하면 한 개의 클래스 파일에 설정하는 것보다 영역별로 설정 파이을 나누면 관리하기 편해진다.
AppConf1.java
source
AppConf2.java
source
•
@Autowired : 스프링 설정 클래스에서 자바 빈을 찾아 주입한다.
source
8.1 @Configuration 애노테이션, 빈, @Autowired 애노테이션
•
@Configuration, @Autowired
◦
스프링 설정 클래스의 두 애노테이션 모두 스프링 컨테이너는 설정 클래스 객체를 빈으로 등록한다.
•
스프링 설정 클래스 모두 등록 됬는지 확인
source
8.2 @Import 애노테이션 사용
•
@import 애노테이션은 스프링 설정 클래스를 별도로 가져올 수 있다.
스프링 설정 클래스 가져오기
source
스프링 설정 클래스 여러개 가져오기
source
9. getBean() 메서드 사용
•
주의사항
source
10. 주입 대상 객체를 모두 빈 객체로 설정해야 하나?
꼭 스프링 빈이어야 할 필요는 없다.
빈으로 등록하지 않고 일반 객체로 생성해서 주입할 수 있다.
다만, 스프링 컨테이너는 자동 주입, 라이프사이클 관리 등 단순 객체 생성 외에 객체 관리를 위한 다양한 기능을 제공 받을 수 없다.
의존 주입 대상은 스프링 빈으로 등록하는 것이 권장이다.