Spring Boot 관심사 분리(AppConfig), IoC, DI 컨테이너

Wonho Kim·2025년 3월 13일

Spring Boot

목록 보기
2/4

해당 게시글은 김영한 강사님의 스프링 핵심 원리 강의를 바탕으로 작성하였습니다.
https://www.inflearn.com/courses/lecture?courseId=325969&tab=curriculum&type=LECTURE&unitId=55396&subtitleLanguage=ko&audioLanguage=ko

각 개념에 대해 본격적으로 설명하기 전에, 간단한 예제를 하나 들며 설명하도록 하겠다.

우리는 백엔드 개발자로써 아래와 같은 비지니스 요구사항을 받았다.

주문과 할인 정책을 적용할 수 있는 회원 관리 서비스 개발

  1. 회원

    • 회원을 가입하고 조회가 가능
    • 회원은 일반과 VIP 두 가지 등급이 존재
    • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동 가능 (미확정)
  2. 주문과 할인 정책

    • 회원은 상품을 주문할 수 있다.
    • 회원 등급에 따라 할인 정책 적용 가능

※ 일단 할인 정책의 경우 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. 하지만 추후에 변경될 가능성이 높다.

위와 같이 요구사항을 보면 회원 데이터 저장 방식과 할인 정책 같은 부분은 지금 결정하기 어렵고, 추후에 변경될 가능성이 매우 높다.

따라서 우리는 자바 언어의 특성인 객체 지향 설계를 통해 인터페이스를 만들고 구현체를 언제든지 갈아 끼울 수 있도록 설계해야 한다.

따라서 회원 도메인의 경우 아래와 같이 설계할 수 있다.

회원 도메인 협력 관계

회원 클래스 다이어그램

중요하게 봐야할 부분은 MemberServiceImpl인터페이스인 MemberRepository에만 의존하고, 구현체인 MemoryMemberRepositoryDbMemberRepository에는 의존하면 안된다!

그리고 주문과 할인 도메인은 아래와 같이 설게할 수 있다.

주문 도메인 협력 관계

주문 도메인 클래스 다이어그램

주문 도메인 역시 인터페이스인 MemberRepositoryDiscountPolicy에만 의존해야지, 구현체인 FixDiscountPolicy, RateDiscountPolicy의존하면 안되는 것이다.

왜냐하면 앞서 말했듯이, 우리는 대체 가능성이 높은 회원 데이터 저장 방식과 할인 정책은 언제든지 갈아끼울 수 있도록 설계하도록 구성하였기 때문이다.

그런데 OrderServiceImpl을 구현한 코드의 일부분을 보자.

public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
	private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    ...
}

우리는 객체지향 설계 원칙에 따라 철저히 코드를 작성하였지만, OrderServiceImpl의 경우 인터페이스인 DiscountPolicy와 FixDiscountPolicy 모두 의존하는 상황이 발생한다.

따라서 만약 구현체를 FixDicountPolicy 대신 RateDiscountPolicy를 원하는 경우, 클라이언트 코드를 수정해야 하는 문제점이 발생한다.

결국 실제 의존 관계는 설계와 다르게 아래와 같은 셈이 되버린 것이다.

이는 우리가 앞서 배운 SOLID의 DIP 원칙에 위배되는 코드이다. 그렇다면 이를 어떻게 해결해야 할까?

이런 문제점을 해결하기 위해 나온 개념이 관심사의 분리이다.

관심사 분리(AppConfig)

애플리케이션을 하나의 공연이라고 생각해보자. 로미오가 주인공이라고 생각해 본다면, 현재 로미오는 본인이 연극도 해야하고 누구와 연극할지 정해야하는 여러 책임을 가지고 있다.

OrderServiceImpl에 대입해서 생각해보면, 주문 서비스도 구현하면서, 어떤 할인정책을 넣을지도 스스로 결정해야 한다는 것이다.

이를 해결하기 위해 우리는 공연 기획자를 새롭게 불러 로미오는 누구와 연극할지에 대한 고민을 덜어주고, 연극만 충실히 수행할 수 있도록 만들어 주어야 한다.

OrderServiceImpl에서는, 이 공연 기획자 역할을 수행할 수 있도록 외부에 별도로 AppConfig라는 클래스를 만들어 구현 객체를 생성하고, 연결하는 책임을 갖도록 하는 것이다.

public class AppConfig {
	public MemberService memberService() {
    	return new MemberServiceImpl(memberRepository());
    }
    
    public OrderService orderService() {
    	return new OrderServiceImpl(
        	memberRepository(),
            discountPolicy());
    }
    
    public MemberRepository memberRepository() {
    	return new MemoryMemberRepository();
    }
    
	public DiscountPolicy discountPolicy() {
		return new FixDiscountPolicy();
	}
}

그러면 이제 MemberServiceImpl에서는 아래와 같이 구현체에 의존하지 않고 외부에서 구현체를 주입받아 사용할 수 있다.

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);
    }
}

이제 회원 도메인의 클래스 다이어그램은 아래와 같이 구성된다.

OrderServiceImpl 역시 마찬가지이다.

public class OrderServiceImpl implements OrderService {
	private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
		this.memberRepository = memberRepository;
		this.discountPolicy = discountPolicy;
    }
 
 	@Override
 	public Order createOrder(Long memberId, String itemName, int itemPrice) {
		Member member = memberRepository.findById(memberId);
		int discountPrice = discountPolicy.discount(member, itemPrice);
		return new Order(memberId, itemName, itemPrice, discountPrice);
	}
}

이제 주문 도메인의 클래스 다이어그램은 아래와 같이 구성된다.

우리는 이제 DIP를 위배하지 않고 인터페이스에만 의존할 수 있게 되었다. 이를 관심사의 분리라고 한다.

IoC, DI 컨테이너

제어의 역전(Inversion of Control)

기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고 연결하고 실행했다.

하지만 AppConfig의 등장으로 구현 객체는 자신의 로직을 실행하는 역할만 담당하고, 프로그램 제어의 흐름은 AppConfig가 가져간다.

이와 같이 외부에서 관리하는 것을 제어의 역전(IoC)라고 한다.

의존관계 주입 DI(Dependency Injection)

OrderServiceImpl은 DiscountPolicy 인터페이스에 의존한다. 실제로는 어떤 구현 객체가 사용될진 모른다. 여기서 정적인 클래스 의존관계와 실행 시점에 결정되는 동적인 객체 의존관계를 분리해서 생각해야 한다.

정적인 클래스 의존관계는 클래스가 사용하는 import만 보고 의존관계를 쉽게 파악할 수 있다.

예를 들어 OrderServiceImpl은 MemberRepository, DiscountPolicy에 의존하는 것을 알 수 있다.

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

말 그대로 런타임 시점에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달하는 것을 의미한다.

따라서 AppConfig처럼 객체를 생서하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너, DI 컨테이너, 어셈블러, 오브젝트 팩토리라고 부른다.
(가장 많이 사용하는 용어는 DI 컨테이너)

이번 포스팅에서는 순수 자바 언어로만 의존관계를 설정하고 구현하는 것을 파악해 보았다.

지금처럼 간단한 서비스는 괜찮을지 몰라도, 점점 의존관계가 많아지고 커지면 이를 일일이 타이핑하여 연결해 주는 것은 매우 비효율적이고 관리하기도 힘들다.

이를 해결해 주는 것이 스프링 컨테이너이며 우리가 스프링부트를 사용하는 주된 이유이다.

다음 시간에는 스프링부트를 사용하여 의존관계를 연결하는 방식을 살펴보도록 하겠다.
(배우고나면 이거만 사용하게 될 것이다.)

profile
새싹 백엔드 개발자

0개의 댓글