(Spring) DI, IOC와 Spring Container란 ?
스프링을 공부하다 보면 IoC, DI 라는 용어를 자주 접하게 된다.
최근 면접에서도 IoC와 DI에 대해 설명해보라는 질문을 받았는데 개념이 확실하게 잡혀있지 않아서 버벅이면서 대답했던 기억이 난다.
이번 기회에 IoC와 DI의 개념을 확실하게 잡고 스프링의 컨테이너에 대해서 정리해보도록 하겠다.
IoC(Inversion of Control) - 제어의 역전
IoC는 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부 설정 파일에서 제어하는 것을 말한다.
한 문장으로 압축하려니 이해하기 어려운 부분이 있으므로 예시를 들어 설명하겠다.
회원가입과 회원조회의 기능을 수행하는 MemberApp을 구현한다고 가정하자. MemberApp의 관계도는 다음과 같다.
(실제 DB와 외부 시스템 저장소를 사용하는 것은 코드가 복잡해질 수 있으므로 메모리 회원 저장소만 사용하여 구현해보겠다.)
클래스 다이어그램은 아래와 같으며 비즈니스 로직을 처리하는 MemberService를 인터페이스로 만들고 실제로는 구현체인 MemberServiceImpl을 사용하여 회원가입, 조회 기능을 수행한다. 또한, DAO역할을 수행하는 MemberRepository 또한 인터페이스로 구현하고 MemoryMemberRepository를 구현체로 사용한다.
가장 먼저, Member 클래스를 생성하고 회원 가입과 조회를 실시할 MemberService, DAO인 MemberRepository를 작성한다.
// Member Class
public class Member {
private Long id;
private String name;
public Member(Long id, String name, Grade grade) {
this.id = id;
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
//Member Service
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
//MemberRepository
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
join과 findMember 메서드를 통해 가입, 조회의 기능을 수행한다. save와 findById 메서드를 통해 가입, 조회 기능을 수행한다.
MemoryMemberRepository는 HashMap을 사용하여 <회원ID, 회원정보>의 쌍으로 이루어진 데이터를 저장하도록 구현했다.
import java.util.HashMap;
import java.util.Map;
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
@Override
public void save(Member member) {
store.put(member.getId(), member);
}
@Override
public Member findById(Long memberId) {
return store.get(memberId);
}
}
이제 실제 비즈니스 로직을 처리하는 MemberServiceImpl을 구현해야 한다.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
public void join(Member member) {
memberRepository.save(member);
}
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
MemberServiceImpl의 2번째 라인을 보자.
private final MemberRepository memberRepository = new MemoryMemberRepository();
여기서는 사용자의 요청을 처리하기 위해 사용하는 memberRepository 객체를 직접 new로 생성한다.
이 경우는 SOLID 원칙의 DIP를 위배하는 좋지 않은 코드일 뿐더러 MemoryMemberRepository가 아닌 DbMemberRepository를 사용할 경우 new DbMemberRepository를 직접 수정해야 하는 상황이 발생한다.
이러한 상황에서 IoC 방법을 적용하면 어떻게 될까?
AppConfig라는 외부 클래스를 하나 생성한다. 이 클래스는 객체의 생성, 객체 간의 의존성, 참조 등 제어 흐름을 담당하는 클래스이다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
위 클래스를 사용하여 MemberService 객체를 생성한다면 MemberService의 코드는 아래와 같이 바뀔 것이다.
MemberRepository는 구현체를 직접 연결하는 것이 아닌 인터페이스만 참조하며 AppConfig에 의해 객체가 생성되어 연결하게 된다. 이것이 바로 제어의 역전이다. AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의 제어 흐름은 이제 AppConfig가 가져간다.이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라고 한다.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public void join(Member member) {
memberRepository.save(member);
}
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
DI(Dependency Injection) - 의존성 주입
DI는 위 IoC를 설명한 예시를 통해 쉽게 설명할 수 있다.
우리는 MemberServiceImpl에서 MemoryMemberRepository를 사용하기 위해 내부에서 new 키워드로 객체를 생성, 할당하는 것이 아닌 외부의 AppConfig 클래스를 통해 memberRepository 객체를 생성해서 주입해주었다. 이것이 바로 DI이다.
그렇다면 DI와 IoC는 똑같은 개념이 아닌가?
정확히 말하면. DI는 IoC에 속하는 개념(DI ⊂ IoC)과도 같다고 할 수 있다.
IoC는 외부(여기서는 AppConfig)에서 객체에 대해 흐름 제어를 하는 방식을 말하고
Di는 외부에서 객체에 대해 의존성을 주입하는 것이다.
실제로 객체 지향 프로그래밍의 권위자임 Martin Fowler는 아래의 글에서 다음과 같이 서술하였다.
Inversion of Control Containers and the Dependency Injection pattern
As a result I think we need a more specific name for this pattern. Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection
.
그 결과, 이 패턴을 나타내기 위해 좀 더 구체적인 명칭이 필요하다고 생각한다. IoC는 너무 포괄적인 용어여서 사람들이 혼동하기 때문에 IoC 옹호자들과 많은 토론을 거쳐 우리는 이를 Dependency Injection이라고 명명했다.
Spring Container
스프링의 컨테이너는 IOC Container 또는 DI Container라고도 한다. 이름에서 알 수 있듯이 위에서 설명한 DI와 IoC를 수행한다.
우리가 만들었던 AppConfig의 역할을 수행하는 것이다. 스프링 컨테이너는 어노테이션을 통해 역할을 나타낼 수 있다.
@Configuration : 구성정보를 담당하는것을 설정할때 @Configuration 을 붙여줍니다.
@Bean : 각 메서드에 @Bean을 붙이면 스프링 컨테이너에 자동으로 등록이 됩니다.
이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 하며 스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다.
AppConfig를 스프링 컨테이너로 바꾸면 다음과 같다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
스프링 컨테이너는 다른 포스팅에서 좀 더 구체적으로 다루도록 하겠다.
*본 포스팅은 김영한님의 "스프링 핵심 원리 - 기본편" 에서 학습한 내용을 기반으로 작성되었습니다.