3. 스프링 DI(Dependency Injection) #1 객체 의존과 의존 주입(DI)

2021. 7. 2. 00:03JAVA/Spring

본 글은 최범균 저자의 초보 웹 개발자를 위한 스프링5 프로그래밍 입문 도서의 내용을 복습하기 위해 작성된 글입니다.

1. 의존이란?

DI는 'Dependency Injection'의 약자로 우리말로는 '의존 주입'이라고 번역한다. 여기서 말하는 의존은 객체 간의 의존을 의미한다.

 

예를 들어 회원 가입을 처리하는 기능을 구현하는 다음의 코드를 보자.

  • 서로 다른 회원은 동일한 이메일 주소를 사용할 수 없음
  • 해당 제약사항을 처리하기 위해 MemberRegisterService 클래스는 MemberDao 객체의 selectByEmail() 메서드를 이용하여 동일한 이메일을 가진 회원 데이터가 존재하는지 확인
  • 만약 같은 이메일을 가진 회원 데이터가 존재하면 예외 발생
  • 같은 이메일을 가진 회원 데이터가 존재하지 않으면 회원 정보를 담은 Member 객체를 생성 후 MemberDao 객체의 insert() 메서드를 이용하여 DB에 회원 데이터를 삽입
public class MemberRegisterService {
	private MemberDao memberDao = new MemberDao();
	
    public Long 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);
        return newMember.getId();
    }

}

위 소스코드에서 주목할 점은 MemberRegisterService 클래스가 DB 처리를 위해 MemberDao 클래스의 메서드(selectByEmail(), insert())를 사용한다는 점이다.

 

의존이란 무엇인가?

한 클래스가 다른 클래스의 메서드를 실행할 때 이를 '의존'한다고 표현한다. 앞의 코드에서는 MemberRegisterService 클래스가 MemberDao 클래스에 의존한다고 표현할 수 있다.

 

더보기

의존은 변경에 의해 영향을 받는 관계를 의미한다. 예를 들어 MemberDao의 insert() 메서드의 이름을 insertMember()로 변경하면 이 메서드를 사용하는 MemberRegisterSerivce 클래스의 소스 코드도 함께 변경된다. 이렇게 변경에 따른 영향이 전파되는 관계를 '의존'한다고 표현한다.

 

의존하는 대상을 구하는 단순한 방법

  • 의존 대상 객체를 필드 멤버에 직접 생성하는 방법
public class MemberRegisterService {
	// 의존 객체를 직접 생성
	private MemberDao memberDao = new MemberDao();
	...
// 의존하는 MemberDao의 객체도 함께 생성
MemberRegisterService svc = new MemberRegisterService();

하지만 위와 같은 방법은 클래스 내부에서 직접 의존 객체를 생성하는 것이 쉽긴 하지만 유지보수 관점에서 문제점을 유발할 수 있다.

 

2. DI를 통한 의존 처리

DI(Dependency Injection, 의존 주입)는 의존하는 객체를 필드 멤버와 같이 직접 생성하는 대신 의존 객체를 생성자를 통해서 전달받는 방식을 사용한다. 예를 들어 앞서 의존 객체를 직접 생성한 MemberRegisterService 클래스에 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();
	}
	
}

 

변경 및 주목할 부분

Before

private MemberDao memberDao = new MemberDao();

After

private MemberDao memberDao;

public MemberRegisterService(MemberDao memberDao) {
	this.memberDao = memberDao;
}

직접 의존 객체를 생성했던 코드와 달리 바귄 코드는 의존 객체를 직접 생성하지 않는다. 대신 생성자를 통해서 의존 객체를 전달받는다. 즉, 생성자를 통해 MemberRegisterService 클래스가 의존(Dependency)하고 있는 MemberDao 객체를 주입(Injection) 받은 것이다.

 

객체 생성

MemberDao dao = new MemberDao();
// 의존 객체를 생성자를 통해 주입한다.
MemberRegisterService svc = new MemberRegisterService(dao);

위와 같이 굳이 필드 멤버에 MemberDao 객체를 생성하지 않고 생성자를 통해서 주입받는 이유는 변경의 유연함 때문이다.

 

3. DI와 의존 객체 변경의 유연함

의존 객체를 직접 생성하는 방식은 필드나 생성자에서 new 연산자를 이용해서 객체를 생성한다.

 

회원 등록 기능을 제공하는 MemberRegisterService 클래스에서 아래 코드처럼 의존 객체를 직접 생성 가능하다.

public class MemberRegisterService {
	private MemberDao memberDao = new MemberDao();
    ...
}

 

회원의 암호 변경 기능을 제공하는 ChangePasswordService 클래스도 다음과 같이 의존 객체를 직접 생성한다고 가정하자.

public class ChangePasswordService {
	private MemberDao memberDao = new MemberDao();
    ...
}

이 상태에서 회원 데이터의 빠른 조회를 위해 캐시를 적용해야 하는 상황이 발생할 수 있다. 그래서 MemberDao 클래스를 상속받은 CachedMemberDao 클래스를 생성할 수 있다.

public class CachedMemberDao extends MemberDao{
    ...
}

 

하지만 문제는 캐시 기능을 적용한 CachedMemberDao를 사용하려면 MemberRegisterService 클래스와 ChangePasswordService 클래스의 코드를 아래와 같이 변경해주어야 한다.

Before

public class MemberRegisterService {
	private MemberDao memberDao = new MemberDao();
    ...
}

public class ChangePasswordService {
	private MemberDao memberDao = new MemberDao();
    ...
}

 

After

public class MemberRegisterService {
	private MemberDao memberDao = new CachedMemberDao();
    ...
}

public class ChangePasswordService {
	private MemberDao memberDao = new CachedMemberDao();
    ...
}

변경한 위의 소스코드의 문제점은 MemberDao 객체가 필요한 클래스가 늘어날수록 소스 코드를 전부 변경해야하는 문제점을 가지고 있다.

 

의존 주입(DI, Dependency Injection)의 적용 

public class MemberRegisterService {
	private MemberDao memberDao;

	public MemberRegisterService(MemberDao memberDao) {
		this.memberDao = memberDao;
	}
    ...
}

public class ChangePasswordService {
	private MemberDao memberDao;

	public MemberRegisterService(MemberDao memberDao) {
		this.memberDao = memberDao;
	}
    ...
}

위와 같이 생성자를 통해서 객체를 받는다면 소스코드를 변경하지 않아도 된다.

 

두 클래스의 객체를 생성하는 코드

 MemberDao memberDao = new MemberDao();
 MemberRegisterService regSvc = new MemberRegisterService(memberDao);
 ChangePasswordService pwdSvc = new ChangePasswordService(memberDao);

위의 객체 생성 코드를 통해서 두 클래스 모두 생성자를 통해서 MemberDao 생성 객체를 주입받는다.

 

이제 MemberDao 대신 CachedMemberDao를 사용한다고 가정하자. 수정해야할 코드는 한 곳 뿐이다.

Before

 MemberDao memberDao = new MemberDao();
 MemberRegisterService regSvc = new MemberRegisterService(memberDao);
 ChangePasswordService pwdSvc = new ChangePasswordService(memberDao);

After

 MemberDao memberDao = new CachedMemberDao();
 MemberRegisterService regSvc = new MemberRegisterService(memberDao);
 ChangePasswordService pwdSvc = new ChangePasswordService(memberDao);

위와 같이 new CachedMemberDao() 부분만 변경하면 변경이 완료된다. MemberDao 클래스는 CachedMemberDao의 부모 클래스이기 때문에 저장이 가능하다. 위와 같이 가능하기 때문에 변경이 유연하게 할 수 있다.

 

References

초보 웹 개발자를 위한 스프링5 프로그래밍 입문, 최범균 저