3. 스프링 DI
스프링 5 프로그래밍 입문 3/7
1. 의존이란?
DI
는 Depedency Injection의 약자로 의존 주입이라고 번역한다. 여기서 말하는 의존(dependency)
는 객체 간의 의존을 의미한다. 한 클래스가 다른 클래스의 메서드를 실행할 때 이를 의존
한다고 표현한다. (의존은 변경에 의해 영향을 받는 관계를 의미한다. 이렇게 변경에 따른 영향이 전파되는 관계를 의존한다고 표현한다.)
public class MemberService {
private MemberDao memberDao = new MemberDao();
}
의존하는 대상이 있으면 그 대상을 구하는 방법이 필요하다. 가장 쉬운 방법은 의존 대상 객체를 직접 생성하는 것이다. 클래스 내부에서 직접 의존 객체를 생성하는 것이 쉽긴 하지만 유지보수 관점에서 문제점을 유발할 수 있다.
2. DI를 통한 의존 처리
DI(Dependency Injection, 의존 주입)
는 의존하는 객체를 직접 생성하는 대신 의존 객체를 전달하는 방식을 사용한다.
public class MemberService {
private MemberDao memberDao;
public MemberService(MemberDao memberDao) {
this.memberDao = memberDao;
}
}
위 코드는 생성자를 통해 MemberService가 의존(Dependency)
하고 있는 MemberDao 객체를 주입(Injection)
받은 것이다. 의존 객체를 직접 구하지 않고 생성자를 통해서 전달받기 때문에 이 코드는 DI(의존 주입)
패턴을 따르고 있다.
DI를 적용한 결과 MemberService를 사용하는 코드는 다음과 같이 MemberService 객체를 생성할 때 생성자에 MemberDao 객체를 전달해야 한다.
MemberDao dao = new MemberDao();
MemberService service = new MemberService(dao);
3. DI와 의존 객체 변경의 유연함
의존 객체를 직접 생성하는 방식은 필드나 생성자에서 new 연산자를 이용해서 객체를 생성한다. MemberDao 클래스를 CachedMemberDao 클래스로 교체하려 할 때, 만약 MemberDao 객체가 필요한 클래스가 세 개라면 세 클래스 모두 동일하게 소스 코드를 변경해야 한다.
동일한 상황에서 DI를 사용하면 수정할 코드가 줄어든다. DI를 사용하면 MemberDao 객체를 사용하는 클래스가 세 개여도 변경할 곳은 의존 주입 대상이 되는 객체를 생성하는 코드 한 곳 뿐이다.
4. 예제 프로젝트
회원 데이터 관련 클래스
Member
WrongPasswordException
MemberDao
회원 가입 처리 관련 클래스
DuplicateMemberException
RegisterRequest
MemberRegisterService :
생성자
를 통해 의존 객체(MemberDao)를 주입받는다.
암호 변경 관련 클래스
MemberNotFoundException
ChangePasswordService :
세터(setter)
를 통해 의존 객체(MemberDao)를 주입받는다.
5. 객체 조립기
main 메서드에서 의존 대상 객체를 생성하고 주입하는 방법이 나쁘진 않지만 이 방법보다 더 나은 방법은 객체를 생성하고 의존 객체를 주입해주는 클래스를 따로 작성하는 것이다. 의존 객체를 주입한다는 것은 서로 다른 두 객체를 조립한다고 생각할 수 있는데, 이런 의미에서 이 클래스를 조립기(assembler)
라고도 표현한다.
조립기는 객체를 생성하고 의존 객체를 주입하는 기능을 제공한다. 또한 특정 객체가 필요한 곳에 객체를 제공한다. 예를 들어 Assembler 클래스의 getMemberRegisterService() 클래스는 MemberRegisterSevice 객체가 필요한 곳에서 사용한다.
5.1 조립기 사용 예제
6. 스프링의 DI 설정
스프링은 앞서 구현한 조립기
와 유사한 기능을 제공한다. 즉 스프링은 Assembler 클래스의 생성자 코드처럼 필요한 객체를 생성하고 생성한 객체에 의존을 주입한다. 또한 객체를 제공하는 기능을 정의하고 있다.
6.1 스프링을 이용한 객체 조립과 사용
스프링을 사용하려면 먼저 스프링이 어떤 객체를 생성하고, 의존을 어떻게 주입할지를 정의한 설정 정보
를 작성해야 한다. 이 설정 정보는 자바 코드를 이용해서 작성할 수 있다.
@Configuration
public class AppCtx {
@Bean
public MemberDao memberDao() {
return new MemberDao();
}
@Bean
public MemberRegisterService memberRegSvc() {
return new MemberRegisterService(memberDao());
}
@Bean
public ChangePasswordService changePwdSvc() {
ChangePasswrodService pwdSvc = new ChangePasswordService();
pwdSvc.setMemberDao(memberDao());
return pwdSvc;
}
}
설정 클래스를 만들었다고 해서 끝난 것이 아니다. 객체를 생성하고 의존 객체를 주입하는 것은 스프링 컨테이너
이므로 설정 클래스를 이용해서 컨테이너를 생성해야 한다. AnnotationConfigApplicationContext 클래스를 이용해서 스프링 컨테이너를 생성할 수 있다.
ApplicationContext ctx = new AnnotationConfigApplication(AppCtx.class);
컨테이너를 생성하면 getBean()
메서드를 이용해서 사용할 객체를 구할 수 있다,
// 컨테이너에서 이름이 memberRegSvc인 Bean 객체를 구한다.
MemberRegisterService service =
ctx.getBean("memberRegSvc", MemberRegisterService.class);
스프링 컨테이너 활용 예제 : MainForSpring.java
6.2 DI 방식 1 : 생성자 방식
생성자를 통해 의존 객체를 주입 받고 주입 받은 객체를 필드에 할당한다.
6.3 DI 방식 2 : 세터 메서드 방식
생성자 외에 세터 메서드를 이용해서 객체를 주입받기도 한다. 일반적인 세터(setter)
메서드는 자바빈 규칙에 따라 다음과 같이 작성한다.
메서드 이름이 set으로 시작한다.
set 뒤에 첫 글자는 대문자로 시작한다.
파라미터가 1개이다.
리턴 타입이 void이다.
7. @Configuration 설정 클래스의 @Bean
@Configuration
public class AppCtx {
@Bean
public MemberDao memberDao() {
return new MemberDao();
}
@Bean
public MemberRegisterService memberRegSvc() {
return new MemberRegisterService(memberDao());
}
@Bean
public ChangePasswordService changePwdSvc() {
ChangePasswrodService pwdSvc = new ChangePasswordService();
pwdSvc.setMemberDao(memberDao());
return pwdSvc;
}
}
memberRegSvc() 메서드와 changePwdSvc()는 둘 다 memberDao() 메서드를 실행하고 있다. 그리고 memberDao() 메서드는 매번 새로운 MemberDao 객체를 생성해서 리턴한다.
그런데 여기서 memberDao()가 새로운 MemberDao 객체를 생성해서 리턴하므로 MemberRegisterService 객체와 ChangePasswordService 객체가 서로 다른 MemberDao 객체를 사용하는 것이 아닌가라는 궁금증이 생긴다.
그러나 스프링 컨테이너가 생성한 빈은 싱글톤 객체
이므로 @Bean
이 붙은 메서드에 대해 한 개의 객체만 생성한다. 이는 다른 설정 메서드에서 memberDao()을 몇 번을 호출하더라도 항상 같은 같은 객체를 리턴한다는 것을 의미한다.
이게 어떻게 가능할까? ⇒ 스프링이 런타임에 생성한 클래스의 memberDao() 메서드는 매번 새로운 객체를 생성하지 않는다. 대신 한 번 생성한 객체를 보관했다가 이후에는 동일한 객체를 리턴한다.
8. 두 개 이상의 설정 파일 사용하기
스프링은 한 개 이상의 설정 파일을 이용해서 컨테이너를 생성할 수 있다.
AppConfig2 클래스의 17~18행 코드는 다음과 같다.
@Autowired
private MemberDao memberDao;
여기서 @Autowired
어노테이션은 스프링의 자동 주입 기능을 위한 것이다. 스프링 설정 클래스의 필드에 @Autowired
어노테이션을 붙이면 해당 타입의 빈을 찾아서 필드에 할당한다. AppConfig1 클래스에 MemberDao 타입의 빈을 설정했으므로 AppConfig2 클래스의 memberDao 필드에는 AppConfing1 클래스에서 설정한 빈이 할당된다.
설정 클래스가 두 개 이상이어도 스프링 컨테이너를 생성하는 코드는 크게 다르지 않다. 다음과 같이 파라미터로 설정 클래스를 추가로 전달하면 된다.
ctx = new AnnotationConfigApplicationContext(AppConfig1.class, AppConfig2.class);
8.2 @Import 어노테이션 사용
두 개 이상의 설정 파일을 사용하는 또 다른 방법은 @Import
어노테이션을 사용하는 것이다. @Import
어노테이션은 함께 사용할 설정 클래스를 지정한다. @Import
어노테이션은 다음과 같이 배열을 이용해서 두 개 이상의 설정 클래스도 지정할 수 있다.
9. getBean() 메서드 사용
지금까지 getBean()
메서드를 이용해서 사용할 Bean 객체를 구했다.
Version Printer versionPrinter = ctx.getBean("versionPrinter", VersionPrinter.class);
여기서 getBean()
메서드의 첫 번째 인자는 빈의 이름이고 두 번째 인자는 빈의 타입이다.
getBean() 메서드를 호출할 때 존재하지 않는 빈 이름을 사용하면 익셉션이 발생한다. NoSuchBeanDefinitionException
빈의 실제 타입과 getBean() 메서드에 지정한 타입이 다를 경우 익셉션이 발생한다.
BeanNotOfRequiredTypeException
다음과 같이 빈 이름을 지정하지 않고 타입만으로 빈을 구할 수도 있다. 이 때 해당 타입의 빈 객체가 한 개만 존재하면 해당 빈을 구해서 리턴한다.
VersionPrinter versionPrinter = ctx.getBeaan(MemberPrinter.class);
해당 타입의 빈 객체가 존재하지 않으면 익셉션이 발생한다.
NoSuchBeanDefinitionException
같은 타입의 빈 객체가 두 개 이상 존재할 경우 익셉션이 발생한다.
NoUniqueBeanDefinitionException
10. 주입 대상 객체를 모두 빈 객체로 설정해야 하나?
주입할 객체가 꼭 스프링 빈이어야 할 필요는 없다. 빈으로 등록하지 않고 일반 객체로 주입할 수 있다.
객체를 스프링 빈으로 등록할 때와 등록하지 않을 때의 차이는 스프링 컨테이너가 객체를 관리하는지 여부이다. 빈으로 등록하지 않으면 스프링 컨테이너에서 객체를 구할 수 없다.
스프링 컨테이너는 자동 주입
, 라이프사이클 관리
등 단순 객체 생성 외에 객체 관리를 위한 다양한 기능을 제공하는데 빈으로 등록한 객체에만 기능을 제공한다.
스프링 컨테이너가 제공하는 관리 기능이 필요없고 getBean() 메서드로 구할 필요가 없다면 빈 객체로 꼭 등록해야 하는 것은 아니다.
Last updated
Was this helpful?