본문 바로가기
Java Spring/Spring과 객체지향

[스프링과 객체지향 3편] DI, IoC, 스프링 컨테이너

by 그냥노깡 2022. 7. 4.

 

이전 편에서 객체지향설계 5원칙에 대해 다루었다.

다형성만을 사용해서 OCP와 DIP를 지키기 어렵다는 것도 설명하였다.

이번 편에서는 OCP와 DIP를 지키기 위해 DI, IoC에 대해 알아보고,

나아가 DI 컨테이너, 스프링 컨테이너에 대해 알아볼 것이다.

 

DI (Dependency Injection), 의존관계 주입

애플리케이션 실행 시점 외부에서 실제 구현 객체를 생성하고 전달하여 클라이언트와 서버의 실제 의존관계가 연결되는 것 의존관계 주입(DI)이라고 한다.

 

의존관계는 두 가지로 분리해서 생각해야 한다.

- 정적인 클래스 의존관계

- 동적인 객체 의존관계

 

정적 의존관계는 애플리케이션을 실행하지 않아도 코드를 통해 알 수 있는 의존관계이다. (import 패키지로만 알 수 있는 부분)

정적 의존관계는 클래스 다이어그램을 통해 표현할 수 있다.

 

클래스 다이어그램

클래스 다이어그램을 보면 MemberServiceImpl은 MemberRepository라는 인터페이스에 의존하고 있다.

하지만 이러한 클래스 의존관계만으로 실제 어떤 객체가 주입될지는 알 수 없다.

 

 

동적 의존관계는 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존관계이다.

동적 의존관계는 객체 다이어그램을 통해 표현될 수 있다.

객체 다이어그램

 

이렇게 애플리케이션이 실행중이지 않은 정적 상태와 애플리케이션 실행 시점인 동적 상태를 구분함으로써

정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 의존관계를 쉽게 변경할 수 있게 된다.

 

다시 말해 Jdbc 멤버 리포지토리가 Memory 멤버 리포지토리로 바뀌어도, MemberServiceImpl은 MemberRepository라는 인터페이스만 의존하면 되고, 애플리케이션 실행 시점에 주입하는 객체만 바꿈으로써 변경의 범위를 최소화할 수 있다.

 

 

IoC (Inversion of Control), 제어의 역전

프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부(ConfigClass, 프레임워크 등)에서 관리하도록 제어를 넘기는 것제어의 역전(IoC)이라고 한다.

 

2편에서 작성한 코드를 다시 가져와보자.

public class MemberService {
	private MemberRepository memberRepository = new MemoryMemberRepository();
    //private MemberRepository memberRepository = new JdbcMemberRepository(); //구현체 변경
}

클라이언트인 MemberService가 필요한 서버 구현 객체인 MemoryMemberRepository를 직접 생성하고, 연결하고, 실행한다.

한 마디로 MemberService가 프로그램의 제어 흐름을 스스로 조종했다.

전편에서도 말했듯이 이 방법은 다형성을 사용했지만, OCP, DIP를 지키지 못하는 코드이다.

만약 MemoryMemberRepository가 다른 구현체로 변경되면, 클라이언트인 MemberService의 코드 변경에도 영향이 생기기 때문에 유연한 코드가 아니다.

 

위 코드를 다음과 같이 수정했다.

public class MemberService {
    private MemberRepository memberRepository;
    
    public MemberService(MemberRepository memberRepository) {
    	this.memberRepository = memberRepository
    }
}
public class AppConfig {

    public MemberService memberService() {
    	return new MemberServiceImpl(memberRepository());
    }
    
    public MemberRepository memberRepository() {
    	return new MemoryMemberRepository();
    }
}

이제, 프로그램의 제어 권한은 전적으로 AppConfig가 가지게 된다. (제어의 역전)

 

MemberService는 더이상 MemberRepository의 구체 클래스에 의존하지 않는다. (인터페이스만 의존)

AppConfig가 클래스 생성자를 호출하여 대신 구체 클래스 객체를 생성하고, 의존성을 주입시켜준다.

 

이렇게 구현함으로써 MemberService는 비즈니스 로직의 역할에만 집중할 수 있게 된다.

구체 클래스 인스턴스 생성과 의존성 주입(DI)의 역할은 분리되어 AppConfig가 해당 책임을 가지게 된다.

 

 

프레임워크 vs 라이브러리

제어의 역전은 프레임워크와 라이브러리를 가르는 개념이기도 하다.

 

- 프레임워크는 내가 작성한 코드를 콜백으로 부르든, 중간에 삽입하든 프레임워크의 라이프 사이클 내부로 내 코드의 제어의 흐름을 가져와 대신 실행한다.

예) JUnit 테스트 코드, Spring 프레임워크 등

 

- 반면에 내가 작성한 코드가 직접 제어의 흐름을 담당하고, 필요한 도구만을 가져와 사용한다면, 그 도구는 라이브러리이다.

예) Math.abs()

 

 

 DI 컨테이너, 스프링 컨테이너

위 코드에서 AppConfig처럼 객체를 생성하고, 관리해주면서, 의존관계를 설정해주는 것을 DI 컨테이너, 또는 IoC 컨테이너라고 한다.

 

스프링 프레임워크는 AppConfig와 같이 프로그램 제어를 획득하여 의존성 주입 외에 더 많은 기능들을 제공하는 스프링 컨테이너를 지원한다.

 

스프링 컨테이너는 구현체들을 스프링 빈이라는 이름으로 관리하고, 싱글톤으로 유지하고, 생명주기 콜백을 지원하는 등 더 많은 기능을 제공해준다.

 

 

마무리

이번 글에서는 클라이언트가 오로지 서버 클래스의 인터페이스에만 의존할 수 있도록 DI와 IoC를 적용해보는 과정을 거쳤다.

다형성, DI, IoC를 통해 객체지향의 5원칙 중 DIP, OCP를 지키며 더 유연하고 확장가능한 코드를 구현할 수 있었다.

또한, 이렇게 비즈니스 수행 역할과 객체 생성 및 의존성 주입 역할을 분리하는 목적 이외에도, 더 다양한 기능을 제공해주는 스프링 컨테이너에 대해서도 알아보았다.

 

다음 4편에는 스프링 컨테이너의 가장 기본적인 기능인 싱글톤 빈, 컴포넌트 스캔, 의존성 자동 주입에 대해 정리할 것이다.

스프링이 얼마나 코드를 단순화시켜주고, 효율적으로 소프트웨어를 제어하는지 알 수 있을 것이다.

 


참고

스프링 핵심 원리  - 기본편, 김영한

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8

객체지향의 사실과 오해, 조영호

http://www.yes24.com/Product/Goods/18249021