의존주입이란(DI)
1. 의존이란?
DI는 ‘Dependency Injection’의 약자로 우리말로는 ‘의존 주입’이라고 번역한다. 여기서 말하는 의존은 객체 간의 의존을 의미한다. 이해를 돕기 위해 회원 가입을 처리하는 기능 을 구현한 다음의 코드를 보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.time.LocalDateTime;
public class MemeberRegisterService {
private MemberDao memberDao = new MemeberDao();
public void regist(RegisterRequest req){
// 이메일로 회원 데이터(Member) 조회
Member member = memberDao.selectByEmail(req.getEmail());
if(member != null){
// 같은 이메일을 가진 회원이 이미 존재한다면 익셉션 발생
throw new DuplicateMemberException("dup email " + req.getEmail());
}
// 같은 이메일을 가진 회원이 존재하지 않으면 DB에 삽입
Member newMemeber = new Member(
req.getEmail(), req.getPassword(), req.getName(),
LocalDateTime.now());
memberDao.insert(newMember);
}
}
서로 다른 회원은 동일한 이메일 주소를 사용할 수 없다는 요구사항이 있다고 가정해보자. 이 제약사항을 처리하기 위해 MemberRegisterService 클래스는 MemberDao 객체의 selectByEmail() 메서드를 이요해서 동일한 이메일을 가진 회원 데이터가 존재하는지 확인한다.
위 코드에서 눈여겨볼 점은 MemberRegisterService 클래스가 DB 처리를 위해 MemberDao 클래스의 메서드를 사용한다는 점이다. 회원 데이터가 존재하는지 확인하기 위해 selectByEmail(), 회원 데이터를 DB에 삽입하기 위해 insert() 메서드를 실행한다.
이렇게 한 클래스가 다른 클래스의 메서드를 실행할 때 이를 ‘의존’한다고 표현한다. 위 코드에서는 “MemberRegisterService 클래스가 MemberDao 클래스에 의존한다”고 표현한다.
의존하는 대상이 있으면 그 대상을 구하는 방법이 필요하다. 그 방법은 2가지가 존재한다.
1) 의존 대상 객체를 직접 생성. but 유지보수 관점에서 문제 유발
2) DI와 서비스 로케이터. 스프링과 관련된 것은 DI
2. DI를 통한 의존 처리
DI(Dependency Injection, 의존 주입)는 의존하는 객체를 직접 생성하는 대신 의존 객체를 전달받는 방식을 사용한다. 예를 들어 앞서 의존 객체를 직접 생성한 MemberRegisterService 클래스에 DI 방식을 적용하면 다음과 같이 구현할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.time.LocalDateTime;
public class MemeberRegisterService {
private MemberDao memberDao;
public MemberRegisterService(MemberDao memberDao){
this.memberDao = 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 newMemeber = new Member(
req.getEmail(), req.getPassword(), req.getName(),
LocalDateTime.now());
memberDao.insert(newMember);
}
}
바뀐 부분은 생성자부분이다. 직접 의존 객체를 생성했던 코드와 달리 바뀐 코드는 의존 객체를 직접 생성하지 않는다. 대신 생성자를 통해서 의존 객체를 전달받는다. 즉 생성자를 통해 MemberRegisterService가 의존하고 있는 MemberDao 객체를 주입(Injection)받은 것이다. 따라서 DI 패턴을 따르고 있다.
3. DI와 의존 객체 변경의 유연함
의존 객체를 직접 생성하는 방식은 필드나 생성자에서 new 연산자를 이용해서 객체를 생성한다. 회원 등록 기능을 제공하는 MemberRegisterService 클래스에서 다음 코드처럼 의존 객체를 직접 생성할 수 있다.
1
2
3
4
public class MemeberRegisterService {
private MemberDao memberDao = new MemeberDao();
...
}
회원의 암호 변경 기능을 제공하는 ChangePasswordService 클래스도 다음과 같이 의존 객체를 직접 생성한다고 하자.
1
2
3
4
public class ChangePasswordService{
private MemberDao memberDao = new MemberDao();
...
}
MemberDao 클래스는 회원 데이터를 데이터베이스에 저장한다고 가정해보자. 이 상태에서 회원 데이터의 빠른 조회를 위해 캐시를 적용해야 하는 상황이 발생했다. 그래서 MemberDao 클래스를 상속받은 CachedMemberDao 클래스를 만들었다.
1
2
3
public class CachedMemberDao extends MemberDao{
...
}
캐시 기능을 적용한 CachedMemberDao를 사용하려면 MemberRegisterService 클래스와 ChangePasswordService 클래스의 코드를 다음과 같이 변경해주어야 한다.
1
2
3
4
5
6
7
8
9
public class MemeberRegisterService {
private MemberDao memberDao = new CachedMemeberDao();
...
}
public class ChangePasswordService{
private MemberDao memberDao = new CachedMemberDao();
...
}
만약 MemberDao 객체가 필요한 클래스가 세 개라면 세 클래스 모두 동일하게 소스 코드를 변경해야 한다.
동일한 상황에서 DI를 사용하면 수정할 코드가 줄어든다. 에를 들어 다음과 같이 생성자를 통해서 의존 객체를 주입 받도록 구현했다고 하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MemeberRegisterService {
private MemberDao memberDao;
public MemberRegisterService(MemberDao memberDao){
this.memberDao= memberDao;
}
...
}
public class ChangePasswordService{
private MemberDao memberDao;
public ChangePasswordService(MemberDao memberDao){
this.memberDao= memberDao;
}
...
}
두 클래스의 객체를 생성하는 코드는 다음과 같다.
1
2
3
MemberDao memberDao = new MemberDao();
MemberRegisterService regSvc = new MemberRegisterService(memberDao);
ChangePasswordService pwdSvc = new ChangePasswordService(memberDao);
이제 MemberDao 대신 CachedMemberDao를 사용하도록 수정하면 수정할 코드는 한 곳뿐이다. 아래 코드와 같이 MemberDao 객체를 생성하는 코드만 변경하면 된다.
1
2
3
MemberDao memberDao = new CachedMemberDao();
MemberRegisterService regSvc = new MemberRegisterService(memberDao);
ChangePasswordService pwdSvc = new ChangePasswordService(memberDao);
DI를 사용하면 앞서 의존 객체를 직접 생성했던 방식에 비해 변경할 코드가 한 곳으로 집중되는 것을 알 수 있다.
4. 객체 조립기
앞서 DI를 설명할 때 객체 생성에 사용할 클래스를 변경하기 위해 객체를 주입하는 코드 한 곳만 변경하면 된다고 했다. 그렇다면 실제 객체를 생성하는 코드는 어디에 있을까? 쉽게 생각하면 메인 메서드에서 객체를 생성하면 될 것 같다.
물론 이 방법도 나쁘진 않다. 하지만 이 방법보다 좀 더 나은 방법은 객체를 생성하고 의존 객체를 주입해주는 클래스를 따로 작성하는 것이다. 의존 객체를 주입한다는 것은 서로 다른 두 객체를 조립한다고 생각할 수 있는데, 이런 의미에서 이 클래스를 ‘조립기’라도고 표현한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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.setMemeberDao(memberDao);
}
public MemberDao getMemberDao(){
return memberDao;
}
public MemberRegisterService getMemberRegisterService(){
return regSvc;
}
public ChangePasswordService getChangePasswordService() {
return pwdSvc;
}
}
MemberRegisterService 객체와 ChangePasswordService 객체에 대한 의존을 주입한다. MemberRegisterService는 생성자를 통해 MemberDao 객체를 주입받고, ChangePasswordService는 세터를 통해 주입받는다.
1
2
3
4
Assembler assembler = new Assembler();
ChangePasswordService changePwdSvc =
assembler.getChangePasswordService();
changePwdSvc.changePassword("aaa@google.com", "1234", "newpwd");
assembler.getChangePasswordService()로 구한 객체는 Assembler 클래스에서 생성한 객체이므로 세터를 통해서 MemberDao 객체를 주입받은 객체이다.
정리하면 조립기는 객체를 생성하고 의존 객체를 주입하는 기능을 제공하고 특정 객체가 필요한 곳에 객체를 제공한다.
Ref.
- 최범균, 스프링프로그래밍입문5, 가메출판사.