본문 바로가기
카테고리 없음

[Spring] 스프링의 DI 의존성 주입 이유, 장점 알아보기

by socialcomputer 2025. 9. 29.
반응형

예전에 스프링의 di 그중에서도 생성자주입을 하는 것이 좋다고 설명하는 문서들을 조합, 정리해서 올렸던 적이 있다. 전에는 뭔지도 모르고 이렇게 해야 된다니까 했었는데 저 흐름을 알고 나니까 한층 머리가 정리된 것 같았다. 

나는 무작정 외우기 보단 이해를 해야 암기를 잘하는 편이라 그런지 그런 원리를 알고 나니까 훨씬 스프링이 덜 복잡해졌다. 스프링을 쓰면 가장 먼저 접하는 게 의존성 주입이기 때문에 기본적인 개념이지 않을까 싶다. 그러니까 반복해도 좋겠지 모

그리고 디자인패턴을 한번 싹 따라만 해봐도 전보다 훨씬 이해가 잘된다. 
누가 이걸 해보면 좋다길래 무작정 인프런강의 보면서 따라 쳤는데 아 그래서 이렇게 쓰는구나?! 유레카 한다.ㅋㅋㅋ

그래서 다시 di를 알아보려 한다. 전에는 남들이 올려둔 글을 내가 공부한다, 필기한다 이런 느낌이 강했어서 '내가 남에게 설명해 준다면 어떻게 할까?' 하는 관점에서 작성해보려고 한다. 

하지만 난 전문가가 아니고 한낱 나부랭이기 떄문에 혹시 잘못 적은 게 있을 수 있다는 것,, 발견하면 빨리 알려주시길 바랍니다.. 

어쨌든 시작!

1. DI 의존성 주입과 IoC 제어의 역전

Dependency Injection과 Inversion of Controrl 

di를 알기 위해선 ioc를 알아야 한다. 

제어의 역전은 개발자가 관리해야 하는 것을 외부, 프레임워크에(스프링에게) 넘긴다는 것이다. 즉 스프링에 빈을 등록해 두면 스프링이 그 빈 객체의 생성과 생명주기를 관리해 개발자는 해당 객체를 신경 쓰지 않아도 된다는 것. 

제어의 역전을 구현한 방법(디자인패턴)중 하나가 di, 의존성 주입인 것이다. 

의존성 주입은 이 외부(스프링 IoC 컨테이너)에 넘겨진 객체를 주입받아 사용하는 방식이다. 

아래 코드처럼 라떼는 에스프레소를 외부에서 주입받는다. 이 라떼는 외부에서 관리되는 에스프레소를 가져다 쓴다. 

public class Latte{
    private final Espresso espresso;
    // 어떤 원두, 방식의 에스프레소인지 상관없이 에스프레소 인터페이스 구현체라면 주입 가능
    
    public Latte(Espresso espresso){ // 생성자
    	this.espresso = espresso;
    }
    public void makeCold(){
    	//...
    }
}

그러면 에스프레소의 원두가 바뀌거나 블렌딩 방식이 변화된다고 해도 라떼를 변경하는 일은 필요하지 않다. 

에스프레소 클래스를 수정하기만 하면 된다. 

이것이 다음에 나올 장점인, 결합도는 낮추고 응집도는 높인다는 점을 보여준다. 

 

2. DI를 사용하면 좋은점 - 결합도와 응집도

di를 사용하는 이유가 바로 결합도를 줄이고 응집도를 높이기 위함이다.

결합도(coupling)는 클래스간, 모듈간의 상호 의존성 정도를 말한다. 결합도가 높으면 한 클래스가 수정될 때 연쇄적으로 다른 클래스도 수정을 해줘야 해서 유지보수 비용이 커진다. 반대로 결합도가 낮으면 수정될 클래스 하나만 수정하면 돼서 유지보수 비용이 작아진다.

응집도(cohension)는 클래스 내의 코드가 오로지 그 클래스를 위해서만 쓰이도록 하는 것이다. 하나의 클래스가 자신의 책임에만 집중하고 관련 있는 기능들로만 구성되어 있는지를 말한다. 이렇게 하나의 클래스가 하나의 책임만 가지도록 하면  단일 책임 원칙(SRP)을 지키게 되어 응집도 높은 코드를 작성하게 된다. 

 

3. Spring의 3가지 의존성 주입 방식

  1. 생성자 주입 Constructor Injection
    • 스프링에서 권장하는 방식
    • final로 생성자를 주입받아 사용
    • 롬복으로 편리하게
  2. 수정자 주입 Setter Injection
  3. 필드 주입 Field Injection
    • 클래스 내부에 필요한 클래스를 @Autowired 붙여서 가져옴
    • 간결함

간결한 필드주입 대신 생성자 주입을 권장하는 이유를 알아보자

  생성자 주입 필드주입
의존관계의 불변 final 키워드를 사용하여 의존성이 변경되는 것을 막을 수 있다
객체가 생성되는 시점에만 주입이 가능하므로, 런타임 중에 의존관계가 바뀌어 생길 수 있는 문제를 원천 차단한다
생성자 주입과 달리 필드에 final 키워드를 사용할 수 없다
이로 인해 객체가 생성된 이후에도 외부에서 의존성이 변경될 여지가 생겨, 객체의 불변성을 보장할 수 없게 된다
이는 예상치 못한 버그의 원인이 될 수 있다
테스트 DI 컨테이너 없이 순수 자바 코드로 테스트 객체를 쉽게 생성하고, Mock 객체를 주입해 줄 수 있어 단위 테스트에 매우 유리하다 DI 프레임워크(Spring)의 도움 없이는 객체를 생성하고 테스트하기가 매우 어렵다
순환참조 의존하는 Bean들이 서로를 물고 물리는 순환 참조가 발생했을 때, 애플리케이션 실행 시점BeanCurrentlyInCreationException을 발생시켜 문제를 바로 알려준다 필드 주입은 실제 해당 Bean이 호출되기 전까지는 오류를 감지하지 못하고, 런타임에 StackOverflowError가 발생하여 문제의 원인을 찾기가 더 어려워진다
명확한 의존관계 생성자의 파라미터를 통해 이 클래스가 어떤 의존성을 '필수적으로' 필요로 하는지 명확하게 드러낼 수 있다 필드 주입을 사용하면 해당 클래스를 사용하는 외부 코드에서는 이 클래스가 어떤 의존성을 필요로 하는지 알 수가 없다
오직 클래스 내부 코드를 들여다봐야만 파악할 수있어 컴포넌트의 명확성을 떨어뜨린다
필수 의존성 누락방지
 / 단일 책임 원칙 위반
객체 생성 시점에 반드시 필요한 의존성을 생성자 파라미터로 강제할 수 있다
만약 주입되어야 할 Bean이 없다면 애플리케이션 실행 시점에 바로 오류가 발생하여 문제를 빠르게 인지할 수 있다
필드 주입은 필드 하나와 @Autowired 어노테이션만 추가하면 되기 때문에 무의식적으로 수많은 의존성을 주입하게 될 수 있다
이는 하나의 클래스가 너무 많은 책임을 갖게 되는 단일 책임 원칙 위반으로 이어지기 쉽다

 

4. Lombok으로 생성자 주입 편리하게 하기

생성자 주입이 좋다는 것은 알지만 간결함은 필드 주입이 제일이다. 그러나 롬복의 도움을 받으면 생성자 주입도 간결하고 편리하게 작성할 수 있다. 

롬복의  @RequiredArgsConstructor 어노테이션 덕분에 가능하다. 이 어노테이션은 final 또는 @NonNull이 붙은 필드만을 파라미터로 받는 생성자를 자동으로 만들어준다. 

아래 예제 코드가 있다. 이렇게  @RequiredArgsConstructor 를 붙이면 생성자 코드를 만들지 않아도 자동으로 만들어진다. 

import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;
    private final PointService pointService;

    /*
    	@RequiredArgsConstructor가 이 코드를 자동으로 만들어준다.
    public MemberService(MemberRepository memberRepository, PointService pointService) {
        this.memberRepository = memberRepository;
        this.pointService = pointService;
    }
    */

    public void join(Member member) {
        memberRepository.save(member);
        pointService.grantWelcomePoint(member);
    }
}

 

 

반응형

댓글