의존(Dependency)
= 한 클래스가 다른 클래스의 메소드를 실행할 때
= 변경에 의해 영향을 받는 관계
의존 객체를 구하는 방법
-
클래스 내부에서 직접 의존 객체 생성
-
DI -> 스프링과 관련
-
서비스 로케이터
의존 주입(DI, Dependency Injection)
= 의존하는 객체를 전달받는 방식
-> DI를 통해 변경의 유연함을 얻을 수 있다.
DI를 사용하면 객체를 사용하는 클래스가 여러 개여도 변경할 곳은 의존 주입 대상이 되는 객체를 생성하는 코드 한 곳 뿐이다.
ex) MemberDao 클래스를 이용해 회원 데이터를 데이터베이스에 저장할 때, 캐시를 적용한 CachedMemberDao를 적용해야한다면 DI 방식은 단 한 줄만 수정하면 된다.
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;
}
...
}
public class Application {
public static void main(String[] args) {
MemberDao memberDao = new MemberDao(); // 변경 후: MemberDao memberDao = new CachedMemberDao();
MemberRegisterService memberRegisterService = new MemberRegisterService(memberDao);
ChangePasswordService changePasswordService = new ChangePasswordService(memberDao);
}
}
= 객체를 생성하고 의존 객체를 주입
특정 객체가 필요한 곳에 객체 제공
| Assembler.java
package assembler;
import spring.ChangePasswordService;
import spring.MemberDao;
import spring.MemberRegisterService;
public class Assembler {
private MemberDao memberDao;
private MemberRegisterService memberRegisterService;
private ChangePasswordService changePasswordService;
public Assembler() {
memberDao = new MemberDao();
memberRegisterService = new MemberRegisterService(memberDao);
changePasswordService = new ChangePasswordService();
changePasswordService.setMemberDao(memberDao);
}
public MemberDao getMemberDao() {
return memberDao;
}
public MemberRegisterService getMemberRegisterService() {
return memberRegisterService;
}
public ChangePasswordService getChangePasswordService() {
return changePasswordService;
}
}
스프링
= DI를 지원하는 조립기
= 필요한 객체를 생성하고 생성한 객체에 의존을 주입
= 범용 조립기
@Configuration
: 스프링 설정 클래스 의미
@Bean
: 해당 메소드가 생성한 객체 스프링 빈으로 설정
- 메소드 이름을 빈 객체의 이름으로 사용
ex) 설정 클래스를 이용해 컨테이너 생성 및 객체 사용
| AppContext.java
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 AppContext {
@Bean
public MemberDao memberDao() {
return new MemberDao();
}
@Bean
public MemberRegisterService memberRegisterService() {
return new MemberRegisterService(memberDao());
}
@Bean
public ChangePasswordService changePasswordService() {
ChangePasswordService changePasswordService = new ChangePasswordService();
changePasswordService.setMemberDao(memberDao());
return changePasswordService;
}
}
| Application.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import config.ApplicationContext;
import spring.MemberRegisterService;
public class Application {
private static ApplicationContext applicationContext = null;
public static void main(String[] args) {
applicationContext = new AnnotationConfigApplicationContext(ApplicationContext.class);
...
// 스프링 컨테이너로부터 이름이 "memberRegisterService"인 빈 객체 가져옴
MemberRegisterService memberRegisterService = applicationContext.getBean("memberRegisterService", MemberRegisterService.class);
}
}
- 생성자 방식
생성자를 통해 의존 객체를 주입받아 필드에 할당
public class MemberRegisterService {
private MemberDao memberDao;
public MemberRegisterService(MemberDao memberDao) {
this.memberDao = memberDao;
}
public Long register(RegisterRequest registerRequest) {
Member member = memberDao.selectByEmail(registerRequest.getEmail());
...
memberDao.insert(newMember);
return newMember.getId();
}
}
- 생성자에 전달할 의존 객체가 두개 이상이어도 동일한 방식으로 주입 가능
- setter 메소드 방식
setter 메소드를 이용해 객체를 주입받아 필드에 할당
-
setter 메소드 규칙
- 메소드 이름이 set으로 시작
- set 뒤에 첫 글자는 대문자로 시작
- 파라미터 1개
- 리턴 타입 void
| MemberInfoPrinter.java
package spring;
public class MemberInfoPrinter {
private MemberDao memberDao;
private MemberPrinter memberPrinter;
public void printMemberInfo(String email) {
Member member = memberDao.selectByEmail(email);
...
memberPrinter.print(member);
}
public void setMemberDao(MemberDao memberDao) {
this.memberDao = memberDao;
}
public void setMemberPrinter(MemberPrinter memberPrinter) {
this.memberPrinter = memberPrinter;
}
}
| ApplicationContext.java
import spring.MemberInfoPrinter;
...
@Configuration
public class AppContext {
...
@Bean
public MemberInfoPrinter memberInfoPrinter() {
MemberInfoPrinter memberInfoPrinter = new MemberInfoPrinter();
memberInfoPrinter.setMemberDao(memberDao());
memberInfoPrinter.setMemberPrinter(memberPrinter());
return memberInfoPrinter;
}
}
- 생성자 방식
장점
-
빈 객체를 생성하는 시점에 모든 의존 객체 주입
-
객체를 사용할 때 완전한 상태로 사용 가능
단점
- 생성자의 파라미터 개수가 많을 경우 각 인자가 어떤 의존 객체를 설정하는지 직접 확인해야 함
- setter
장점
- 이름을 통해 어떤 의존 객체가 주입되는지 확인 가능
단점
- 필요한 의존 객체를 전달하지 않아도 빈 객체가 생성되기 때문에 객체 사용 시점에 NullPointerException 발생 가능
스프링 컨테이너가 생성한 빈은 싱글톤 객체다.
스프링 컨테이너는 @Bean이 붙은 메소드에 대해 한 개의 객체만 생성한다.
-> 스프링은 설정 클래스를 그대로 사용하지 않고, 설정 클래스를 상속한 새로운 설정 클래스를 사용하기 때문에 가능
@Autowired
: 스프링 빈에 의존하는 다른 빈을 자동으로 주입(해당 타입의 빈을 찾아서 필드에 할당)
| ApplicationContext2.java
package config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Bean;
import org.springframework.beans.factory.annotation.Configuration;
import spring.MemberRegisterService;
...
@Configuration
public class ApplicationContext2 {
@Autowired
private MemberDao memberDao;
@Autowired
private MemberPrinter memberPrinter;
@Bean
public MemberRegisterService memberRegisterService() {
return new MemberRegisterService(memberDao);
}
...
}
설정 클래스가 두 개 이상일 때, 스프링 컨테이너 생성 시 파라미터로 설정 클래스 목록을 콤마(,)로 구분해서 전달하면 된다.
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ApplicationContext.class,ApplicationContext2.class);
두 개 이상의 설정 파일 사용 시 @Import
어노테이션 사용 가능
@Configuration
@Import(ApplicationContext2.class)
public class ApplicationConfigImport {
배열을 이용해 두 개 이상의 설정 클래스도 지정 가능
@Configuration
@Import({ApplicationContext.class, ApplicationContext2.class})
public class ApplicationConfigImport {
getBean()
메소드: 사용할 빈 객체 가져옴
- 첫 번째 인자: 빈의 이름
- 두 번째 인자: 빈의 타임
VersionPrinter versionPrinter = apllicationContext.getBean("versionPrinter", VersionPrinter.class);
빈 이름을 지정하지 않고 타입만으로 빈 가져오기 가능 (같은 타입의 빈 객체가 두 개 이상 존재하거나 없을 경우 예외 발생)
VersionPrinter versionPrinter = apllicationContext.getBean(VersionPrinter.class);
주입할 객체가 꼭 스프링 빈이 아니어도 된다.
스프링 컨테이너는 자동 주입, 라이프사이클 관리 등 단순 객체 생성 외에 객체 관리를 위한 다양한 기능을 제공하는데 빈으로 등록한 객체에만 기능을 적용한다.
스프링 컨테이너가 제공하는 관리 기능이 필요없고 getBean()
메소드로 구할 필요가 없다면 빈 객체로 등록하지 않아도 된다.
최근에는 의존 자동 주입 기능을 프로젝트 전반에 걸쳐 사용하는 추세이기 때문에 보통 의존 주입 대상은 스프링 빈으로 등록한다.