[ Spring ] 스프링5 프로그래밍 입문 - chapter3. DI (의존성 주입)
1. 객체 의존과 의존 주입
객체 의존
: 변경에 의해 영향을 받는 관계
: 한 클래스가 다른 클래스의 메서드를 실행할 때 이를 '의존'한다고 표현한다
예를 들어, 회원가입의 기능을 구현하는 상황이라 가정해보자.
MemberRegisterService 클래스가 회원가입의 비즈니스 로직이 구현되어 있는 클래스고
MemberDao 클래스가 DB에 회원의 정보를 넣는 로직이 구현되어 있는 클래스라고 할 때,
MemberRegisterService는 MemberDao의 메서드들을 사용하게 된다.
public class MemberRegisterService {
private MemberDao memberDao = new memberDao();
public void regist(RegisterRequest req) {
Member member = memberDao.selectByEmail(req.getEmail);
if (member != null) {
throw new DuplicateMemberException();
}
Member newMember = new Member (
req.getEmail(), req.getPassword(), req.getName(),
LocalDateTime.now());
memberDao.insert(newMember);
}
}
따라서, MemberService는 MemberDao에 의존한다고 하며,
MemberDao의 로직이 수정될 때 MemberService 또한 영향을 받게 되므로
변경에 의해 영향을 받는 관계라고 할 수 있는 것이다.
의존 주입
위 설명처럼 객체를 의존하는 관계는 매우 빈번하다
(사실상 거의 대부분의 서비스 관련 객체들이 서로를 의존하고 있다)
이때 객체들 간의 의존 관계를 맺어주는 방식은
1. 직접 객체 생성 ( ex - MemberService class 내에서 MemberDao class를 직접 생성하기 )
2. 스프링의 DI
3. 서비스 로케이터
세 가지가 있다
이때 직접 객체 생성 방식은 유지보수 차원에서 문제가 크다
public class MemberRegisterService {
private MemberDao memberDao = new memberDao();
...
}
public class MemberResignService {
private MemberDao memberDao = new memberDao();
...
}
public class MemberUpgradeService {
private MemberDao memberDao = new memberDao();
...
}
위와 같이 세 개의 Service에서 Member와 DB간의 로직을 구현해놓은 MemberDao class를 의존했다고 치자.
이때 Member아 DB간 로직이 개편되고 개편된 로직이 CachedMemberDao 클래스에 구현된다면,
MemberDao를 의존하고 있던 위 세개의 서비스 클래스에서 모두 코드를 아래와 같이 변경해줘야 한다.
public class MemberRegisterService {
private MemberDao memberDao = new CachedmemberDao();
...
}
public class MemberResignService {
private MemberDao memberDao = new CachedmemberDao();
...
}
public class MemberUpgradeService {
private MemberDao memberDao = new CachedmemberDao();
...
}
이는 유지보수가 최악인 코드일 것이다
이러한 문제를 해결하기 위해 나타난 개념이 객체 조립이다
객체 조립이란 별개의 클래스에서 객체들간의 의존관계를 정의하는 것이다
public class Assembler {
private MemberDao memberDao;
private MemberRegisterService memberRegisterService;
private MemberResignService memberResignService;
private MemberUpdateService memberUpdateService;
public Assembler() {
// memberRegisterService, memberResignService 등의 의존 객체를 수정하고 싶다면 이 부분만 수정하면 됨
memberDao = new MemberDao();
memberRegisterService = new MemberRegisterService(memberDao);
memberResignService = new MemberResignService(memberDao);
memberUpdateService = new MemberUpdateService(memberDao);
}
public MemberDao getMemberDao() {
return memberDao;
}
public MemberRegisterService getMemberRegisterService() {
return memberRegisterService;
}
...
}
위와 같이 Assembler라는 클래스에서 의존관계를 정의해놓게 되면
여기에서 모든 수정을 진행하면 되기 때문에 유지 보수 측면에서 훨씬 좋은 코드가 된다.
2. Spring DI 설정
위에서 나온 Assembler 클래스를 대신하는 Spring 기능이 바로 DI다
DI는 @Configuration과 @Bean을 통해 이루어진다
@Configuration
public class AppConfig {
private MemberDao memberDao;
private MemberRegisterService memberRegisterService;
@Bean
public MemberDao memberDao() {
return new MemberDao();
}
@Bean
public MemberRegisterService memberRegisterService() {
return new MyService(memberDao());
}
}
위와 같이 의존 관계를 @Configuration을 사용한 설정 클래스에 넣어주고
각각의 객체를 @Bean을 통해 싱글톤 형식으로 생성해준 뒤
ApplicationContext context = new AnnotationConfigApplicaionContext(ApplicaionContext.class);
이런 식으로 스프링 컨테이너에 넣어주면
객체의 의존관계를 하나의 클래스에서 관리할 수 있다
( 사실 이렇게 명시적으로 작성하지 않아도 SpringApplication.run()이 실행되면 자동으로 생성해준다 )
또한, 알다시피 @Bean과 @Configuration은 싱글톤 형식으로 저장되기 때문에
다른 클래스에서 생성자를 통해 Bean으로 등록된 클래스를 호출하면 항상 같은 객체가 불러와진다
✅ 컨테이너 내부에서 일어나는 일
1. @Bean 메서드는 한 번만 실행되며, 같은 빈을 요청하면 스프링이 캐싱된 인스턴스를 반환한다.
2. new MemberRegisterService(memberDao());를 호출하면, memberDao() 메서드가 이미 생성한 인스턴스를 반환한다.
@Configuration 클래스는 내부적으로 프록시(proxy) 객체로 관리되어, @Bean 메서드가 여러 번 호출되더라도 같은 객체가 반환된다