본 게시글의 내용과 소스 코드는 <초보 웹 개발자를 위한 스프링5 프로그래밍 입문> 도서를 참고하여 작성되었습니다.
객체 의존
여기 회원 가입을 처리하는 기능을 구현한 코드가 있다.
package spring;
import java.time.LocalDateTime;
public class MemberRegisterService {
// new 연산자를 사용하여 MemberDao를 생성한 모습
private MemberDao memberDao = new 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();
}
}
MemberRegisterService에서 멤버를 등록시키기 위해서 두가지의 로직을 처리한다.
- MemberDao에 존재하는 selectByEmail 메소드를 사용하여 email과 동일한 유저를 반환 받는다.
- 반환된 결과가 null이 아니라면 예외를 발생, null이면 등록한다.
이때 MemberRegisterService는 MemberDao에 의존한다고 한다. MemberRegisterService의 메소드를 실행하기 위해서는 반드시 MemberDao 객체가 필요하기 때문이다. 만약 MemberDao의 메소드가 변경되면 MemberRegisterService 역시 코드를 수정해야한다. 즉 의존이란 변경에 의해 영향을 받는 관계를 의미한다.
의존 대상이 있다면 그 대상을 반드시 사용해야하므로 대상을 어디선가 구해와야한다. 그 방식에는 객체를 직접 생성하거나 DI & 서비스 로케이터를 통해 구하는 것이 있다. 객체를 직접 생성하는 것은 쉽고 간편하지만 유지보수 관점에서 별로 좋지 않다.
의존 주입
DI는 의존하는 객체를 직접 생성하는 대신 의존 객체를 전달(주입)받는 방식을 사용한다.
package spring;
import java.time.LocalDateTime;
public class MemberRegisterService {
private MemberDao memberDao;
// 전달 받은 의존 대상을 private 변수에 집어넣고 있다
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();
}
}
여기서 MemberRegisterService가 의존하고 있는 대상인 MemberDao가 생성자의 인자로 주어지고, 그 주어진 인자를 private 변수에 집어 넣음을 알 수 있다.
때문에 사용자는 MemberRegisterService 클래스를 생성하면서 MemberDao 클래스도 같이 생성해야한다.
MemberDao memberDao = new MemberDao();
MemberRegisterService regSvc = new MemberRegisterService(memberDao);
사실 객체를 직접 생성해서 만든다면 그냥 Service 객체를 만드는 것으로 끝날 수 있다. 굳이 번거롭게 Service 객체와 Dao 객체를 한꺼번에 만들어서 주입시키는 이유는 바로 변경의 유연함 때문이다.
DI와 의존 객체 변경의 유연함
의존 객체를 직접 생성하는 방식은 필드나 생성자에서 new 연산자를 이용해서 객체를 생성한다.
MemberRegisterService 말고도 MemberDeleteService, ChangePasswordService 등이 있다고 하자. 의존 대상 객체를 직접 생성하는 방식을 사용한다면 코드는 다음과 같을 것이다.
public class MemberRegisterService {
private MemberDao memberDao = new MemberDao();
...
}
public class MemberDeleteService {
private MemberDao memberDao = new MemberDao();
...
}
public class ChangePasswordService {
private MemberDao memberDao = new MemberDao();
...
}
그런데 여기서 MemberDao 클래스에 변화가 일어났다! 서비스가 확장됨에 따라 사용자가 늘어났고, 회원 데이터의 빠른 조회를 위해 캐시 메모리를 적용하게 되었다. 그래서 MemberDao를 상속한 CachedmemberDao 클래스를 만들어 기존의 것을 대체하기로 하였다.
public class CachedMemberDao extends MemberDao {
...
}
그러기 위해서는 다음 세곳을 바꾸어야 한다.
public class MemberRegisterService {
// private MemberDao memberDao = new MemberDao();
private MemberDao memberDao = new CachedMemberDao();
...
}
public class MemberDeleteService {
// private MemberDao memberDao = new MemberDao();
private MemberDao memberDao = new CachedMemberDao();
...
}
public class ChangePasswordService {
// private MemberDao memberDao = new MemberDao();
private MemberDao memberDao = new CachedMemberDao();
...
}
만약 이 중에 하나라도 바꾸지 않는다면 서비스는 제대로 돌아가지 않을 것이다. 그런데 만약 MemberDao를 사용해서 회원 데이터에 접근하는 서비스가 이 세개로 끝나지 않는다면? 몇십개의 서비스가 존재한다면? 우리는 그 몇십개의 코드를 일일이 다 바꿔줘야 할 것이다. 이렇 듯 변경에 유연하지 않은 코드는 유지 보수를 어렵게 만든다.
그럼 주입된 의존 객체를 사용하는 방식으로 바꾸어 보자.
// MemberDao memberDao = new MemberDao();
MemberDao memberDao = new CachedMemberDao();
MemberRegisterService regSvc = new MemberRegisterService(memberDao);
MemberDeleteService delSvc = new MemberDeleteService(memberDao);
ChangePasswordService pwdSvc = new ChangePasswordService(memberDao);
우리가 변경해야 할 부분은 딱 한 곳이 된다. 얼마나 많은 서비스를 만들든 말이다. 이것이 의존 주입을 하는 이유이다.
그리고 의존 대상을 각 개체에 주입하는 과정은 조립기(Assembler) 클래스에서 한꺼번에 실행할 수 있다. 그리고 스프링은 바로 이 조립기의 역할을 수행한다.
package assembler;
import spring.ChangePasswordService;
import spring.MemberDao;
import spring.MemberRegisterService;
import spring.MemberDeleteService;
public class Assembler {
private MemberDao memberDao;
private MemberRegisterService regSvc;
private MemberDeleteService delSvc;
private ChangePasswordService pwdSvc;
public Assembler() {
memberDao = new MemberDao();
regSvc = new MemberRegisterService(memberDao);
delSvc = new MemberDeleteService(memberDao);
pwdSvc = new ChangePasswordService();
pwdSvc.setMemberDao(memberDao);
}
public MemberDao getMemberDao() {
return memberDao;
}
public MemberRegisterService getMemberRegisterService() {
return regSvc;
}
public MemberDeleteService getMemberDeleteService() {
return delSvc;
}
public ChangePasswordService getChangePasswordService() {
return pwdSvc;
}
}