DI(Dependency Injection)
DI는 Dependency Injection의 약자로 의존 주입 이라고 한다.
DI를 이해하려면 먼저 의존(dependency)이 뭔지 알아야 한다.
여기서 말하는 의존이란 객체간의 의존을 의미한다.
회원 가입을 처리하는 기능을 구현할 때 요구사항으로 같은 회원이 아니라면 다른 이메일을 써야 한다고 한다.
이때 비지니스 로직을 담당하는 Service 클래스에서 DB에 직접 접근하는 DAO 클래스에 사용하여 selectByEmail()같은 메서드를 사용하여 DB에 동일한 이메일을 가진 회원이 있는지 확인한다.
여기서 Service 클래스가 DB처리를 위해 DAO 클래스의 메서드를 사용한다는 점인데,
이렇게 한 클래스가 다른 클래스의 메서드를 실행할 때 의존한다고 표현한다.
의존은 변경에 의해 영향을 받는 관계를 의미한다.
만약 위에서 쓴 DAO 클래스의 selectByEmail() 메서드의 이름을 findByEmail()로 변경하면 이 메서드를 사용하는 Service 클래스의 소스코드도 함께 변경되는데
이렇게 변경에 따른 영향이 전파되는 관계를 '의존'한다고 표현한다.
의존하는 대상이 있은 경우 그 대상을 구하는 방법
1. 의존 객체를 직접 생성하기
public class MemberRegisterService {
private MemberDao memberDao = new MemberDao();
}
MemberRegisterService 클래스에서 의존하는 Memberdao 객체를 직접 생성하기 때문에 MemberRegisterService 객체를 생성하는 순간 MemberDao 객체도 함께 생성된다.
특징 : 직접 생성하기 때문에 직관적이며 사용하기 쉽지만 변경과 확장이 어려워질 수 있어 유지보수 관점에서 보면 느슨한 결합과 같은 디자인 패턴으로의 이점을 보기 힘들다
2. DI를 통한 의존 처리
public class MemberRegisterService {
private MemberDao memberDao;
public MemberRegisterService(MemberDao memberDao) {
this.memberDao = memberDao;
}
}
의존하는 객체를 직접 생성하는 대신 의존 객체를 생성자로 전달받는 방식을 사용한다.
의존 객체를 직접 구하지 않고 생성자를 통해서 전달받기 때문에 DI(의존 주입) 패턴을 따르고 있다.
특징 : 더 복잡해 보이지만, 직접 생성하는 방식은
public class MemberRegisterService {
private MemberDao memberDao = new MemberDao();
}
public class AdminRegisterService {
private MemberDao memberDao = new MemberDao();
}
여기서 MemberDao의 구현체가 있고 그 구현체를 memberDao 변수에 담고 싶으면
public class MemberRegisterService {
private MemberDao memberDao = new ConcreteMemberDao1();
}
public class AdminRegisterService {
private MemberDao memberDao = new ConcreteMemberDao2();
}
이렇게 생성해주는 코드를 전부 다 바꿔줘야 한다. 하지만 DI방식을 사용하면
public class MemberRegisterService {
private MemberDao memberDao;
public MemberRegisterService(MemberDao memberDao) {
this.memberDao = memberDao;
}
}
public class AdminRegisterService {
private MemberDao memberDao;
public MemberRegisterService(MemberDao memberDao) {
this.memberDao = memberDao;
}
}
// 객체 생성 코드
MemberDao memberDao = new ConcreteMemberDao1();
MemberRegisterService mrs = new MemberRegisterService(memberDao);
AdminRegisterService ars = new AdminRegisterService(memberDao);
// 객체 생성 코드
MemberDao memberDao = new ConcreteMemberDao2();
MemberRegisterService mrs = new MemberRegisterService(memberDao);
AdminRegisterService ars = new AdminRegisterService(memberDao);
이 코드에서 볼 수 있듯이 객체를 생성할 때 사용할 클래스 한 곳만 변경하면 된다.
(예제에선 생략됐지만 setter방식도 있다)
객체 조립기
DI는 객체 생성에 사용할 클래스를 변경하기 위해 객체를 주입하는 코드 한 곳만 변경하면 된다.
그럼 실제 객체 생성은 어떻게 할까
publci class Main {
MemberDao memberDao = new MemberDao();
MemberRegisterService mrs = new MemberRegisterService(memberDao);
AdminRegisterService ars = new AdminRegisterService(memberDao);
}
쉽게 생각하면 메인 메서드에서 하면 된다.
이 방법보다 더 나은 방법은 객체를 생성하고 의존 객체를 주입해주는 클래스를 따로 만드는 것입니다.
의존 객체를 주입한다는 것은 서로 다른 객체를 조립한다고 생각할 수 있습니다.
이러한 의미로 이런 클래스를 조립기라고도 표현합니다.
@Getter
public class Assembler {
private MemberDao memberDao;
private MemberRegisterService mrs;
private AdminRegisterService ars;
public Assembler() {
memberDao = new MemberDao();
mrs = new MemberRegisterService();
ars = new AdminRegisterService();
}
}
MemberRegisterService와 AdminRegisterService 객체에 대한 의존성을 주입한다.
조립기 클래스를 사용하는 코드는 get메서드를 이용해서 필요한 객체를 구하고 그 객체를 사용한다.
Assembler assembler = new Assembler();
MemberRegisterService mrs = assembler.getMemberRegisterService();
mrs.regist(request);
스프링을 이용한 객체 조립
앞에서 알아본 조립기 대신 스프링을 사용하려면 먼저 스프링이 어떤 객체를 생성하고, 의존을 어떻게 주입할지를 정의하는 설정 정보를 가진 클래스가 있어야 한다.
@Configuration
public class AppCtx {
@Bean
public MemberDao memberDao() {
return new MemberDao();
}
@Bean
public MemberRegisterService memberRegSvc() {
return new MemberRegisterService(memberDao());
}
@Bean
public AdminRegisterService adminRegSvc() {
return new AdminRegisterService(memberDao());
}
}
@Configuration : 스프링 설정 클래스를 의미한다. 이 어노테이션이 붙어야 설정 클래스로 사용할 수 있다.
@Bean : 해당 메서드가 생성한 객체를 스프링 빈이라고 설정한다.
이 어노테이션이 붙은 어노테이션는 빈 객체를 생성하고, 빈 객체의 이름은 메서드의 이름을 따라간다.
ex) memberDao() -> memberDao
설정 클래스만 만든다고 끝난 것이 아니다.
객체를 생성하고 의존 객체를 주입하는 것은 스프링 컨테이너가 하기 때문에 설정 클래스를 이용해서 컨테이너를 생성해야 한다.
AnnotationConfigApplicationContext 클래스를 이용해서 스프링 컨테이너를 생성할 수 있다.
컨테이너를 생성하면 getBean() 메서드를 이용해서 사용할 객체를 구할 수 있다.
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);
// 컨테이너에서 이름이 memberRegSvc인 빈 객체를 가져온다
MemberRegisterService mrs = ctx.getBean("memberRegSvc", MemberRegisterService.class);
}
스프링의 의존성 주입
스프링의 의존성 주입 2가지 방법이 있는데 setter방식은 권장하지 않으므로 DI 방식만 보겠습니다.
DI 방식 : 생성자 방식
public class MemberRegisterService {
private MemberDao memberDao;
// 생성자를 통해 의존 객체 주입
public MemberRegisterService(MemberDao memberDao) {
// 주입 객체를 필드에 할당
this.memberDao = memberDao;
}
public Long regist(RegisterRequest request) {
// 주입 받은 의존 객체의 메서드 사용
Member findMember = memberDao.findByName(request.getName());
return memberDao.join(member);
}
먼저 맨 처음에 봤던 스프링이 아닌 순수 자바의 코드로 의존성 주입을 가져왔습니다.
여기서 스프링으로 바꾸게 된다면 설정 클래스(AppCtx)를 통해 스프링 빈에 등록해야 합니다.
@Bean
public MemberDao memberDao() {
return new MemberDao();
}
@Bean
public MemberRegisterService mrs() {
// 스프링 빈에 등록한 memberDao를 호출하여 의존 객체 주입
return MemberRegisterService(memberDao());
}
// 사용 코드
public static void regist() {
MemberRegisterService mrs = appCtx.getBean("mrs", MemberRegisterService.class);
}
만약 전달할 객체가 두 개 이상이여도 똑같은 방식을 사용하면 된다.
ex) return MemberRegisterService(memberDao1(), memberDao2());
'스프링' 카테고리의 다른 글
스프링 시큐리티 defaultSecurityFilterChain (1) | 2023.12.20 |
---|---|
스프링 시큐리티 내부 흐름 (1) | 2023.12.18 |
JPA N + 1 문제와 해결 방법 (0) | 2023.11.14 |
DTO매핑을 해야하는 이유 (0) | 2023.11.13 |
스프링 부트 핵심가이드 9장 (0) | 2023.04.07 |