본문 바로가기
프로그래밍/Spring, SpringBoot

DI(Dependency Injection)

by 하나둘지금 2024. 5. 7.

김영한 선생님의 스프링 강의를 보다가 DI에 대해 좀 더 찾아보고 이해하게 된 부분을 정리하고자 한다.
 

DI(Dependency Injection): 의존성 주입

의존성 주입에서의 의존성은 특정한 객체가 동작하기 위해 다른 객체을 필요로 하는 것을 의미한다.

즉, 의존성 주입은 객체가 자신이 필요로 하는 의존성(필요한 객체)을 직접 생성하거나 검색하지 않고, 외부에서 받아 사용하는 것을 의미한다.
DI는 IoC 원칙을 실현하기 위한 패턴 중 하나다.  IoC원칙을 어떻게 적용할 것인가에 대한 해결책 중의 하나라고도 한다.
 

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

제어의 역전(IoC, Inversion of Control)은 프로그래밍에 있어 객체의 생성 및 관리 책임을 개발자에서 전체 애플리케이션 또는 프레임워크에 위임하는 디자인 원칙을 말한다. 프레임워크 없이 개발을 진행할 땐 개발자가 직접 객체의 생성과 관리 등 흐름을 직접적으로 제어한다면, 제어의 역전은 제어의 권한이 프로그래머가 아닌 외부환경(프레임워크 등)으로 역전 되는 형태라고 할 수 있다.
 

컴퓨터라는 객체를 만들 때를 예시로 들어보자.

// 컴퓨터 클래스
public class Computer {
    private WindowsOS os;

    public Computer() {
        this.os = new WindowsOS();
    }

    public void boot() {
        this.os.boot();
        // 컴퓨터 부팅 작업 수행
    }
}

// OS: WindowsOS
public class WindowsOS {
    public void boot() {
        // Windows 운영체제 부팅
    }
}

// 클라이언트 코드
public class Main {
    public static void main(String[] args) {
        Computer computer = new Computer();
        computer.boot();
    }
}

 
위 코드는 컴퓨터 클래스가 WindowsOS 객체에 직접 의존한다.
 
 
컴퓨터가 OS에 의존하지 않게 하려면 어떻게 코드를 변경해야 할까?

// 고수준 모듈: 컴퓨터
public class Computer {
    private OperatingSystem os;

    public Computer(OperatingSystem os) {
        this.os = os;
    }

    public void boot() {
        this.os.boot();
        // 컴퓨터 부팅 작업 수행
    }
}

// 인터페이스: OperatingSystem
public interface OperatingSystem {
    void boot();
}

// 저수준 모듈: WindowsOS
public class WindowsOS implements OperatingSystem {
    @Override
    public void boot() {
        // Windows 운영체제 부팅
    }
}

// 클라이언트 코드
public class Main {
    public static void main(String[] args) {
        OperatingSystem os = new WindowsOS();
        Computer computer = new Computer(os);
        computer.boot();
    }
}

 
컴퓨터가 OS에 직접 의존하는 대신, 인터페이스를 통해 의존하도록 변경했다. 이렇게 하면 다른 종류의 운영체제도 쉽게 사용할 수 있고, 컴퓨터 클래스는 특정 운영체제에 종속되지 않는다.

DI 특징

- 클래스 간의 응집도를 높이고, 결합도를 낮춘다.
- 클래스의 변경에 따른 다른 클래스들의 영향을 최소화하여 애플리케이션의 지속성과 확장성을 높일 수 있다.
- 상위 레벨의 모듈은 하위 레벨 모듈의 구체적인 구현이나 생명주기에 대해 알 필요가 없어진다.

스프링의 DI 패턴

1. 생성자 주입(Constructor Injection)

- 객체의 최초 생성 시점에 스프링이 의존성을 주입해주는 방식

- 생성자 호출 시 최초 1회, 의존관계 불변

- 스프링에서 공식적으로 추천하는 방법

- 스프링 4.3 이후부터는 생성자가 한 개 밖에 없을 경우 해당 생성자에 스프링이 자동으로 @Autowired를 붙여주어 생략 가능하다.

- 필드를 final로 만들어줄 수 있다. 때문에 Null을 주입하지 않는 이상 NullPointerException이 발생할 일이 없다.
- 순환참조 문제 방지가 가능하다. 혹여나 생성자 주입을 사용하는 객체 끼리 의존성이 순환되면 스프링이 에러메세지를 뱉어낸다.(스프링부트 2.6 버전 이후로는 필드 주입 및 setter 주입도 기본 설정으로 순환참조 시 에러를 띄우고 프로그램을 종료시킨다.)

@Component
public class BookService {
    private final BookRepository bookRepository;

    @Autowired
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    public void printBook() {
        bookRepository.findBook().forEach(System.out::println);
    }
}

@Repository
public class BookRepository {
    public List<String> findBook() {
        return Arrays.asList("Spring in Action", "Learning Spring Boot");
    }
}



2. 세터 주입(Setter Injection)

- setter 메서드에 @Autowired 어노테이션을 붙이면 스프링이 setter를 사용해서 자동으로 의존성을 주입한다.

- 빈 객체를 만들고 setter로 의존성을 주입해주기 때문에 빈 생성자 또는 빈 정적 팩토리 메서드가 필요하다.
- final field를 만들 수 없고 의존성이 변할 수 있다. (가변성)

- 상황에 따라 객체 생성 후 수정하거나 필요한 시점에 의존성 선택적으로 주입해야할 상황이라면 유리하다. 

@Component
public class MemberService {
    private MemberRepository memberRepository;

    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public void printMembers() {
        memberRepository.findMembers().forEach(System.out::println);
    }
}

@Repository
public class MemberRepository {
    public List<String> findMembers() {
        return Arrays.asList("John Doe", "Jane Doe");
    }
}

 

3. 필드 주입(Field Injection)

- 필드 주입은 원래는 불가능한 주입을 스프링 프레임워크의 힘을 빌려서 주입해주는 방법이다.

- 주입받고자 하는 필드 위에 @Autowired 어노테이션을 붙여주기만 하면 스프링은 의존성을 직접 주입한다.

- 매우 편리하지만 권장되지 않는 방법이다. 프레임워크에 강하게 종속되는 문제점 때문.
- 필드 주입을 사용하게 되면 테스트도 어렵다. 수동으로 의존성 주입을 하고 싶어도 생성자도 setter도 없기때문에 직접 의존성을 넣을 수가 없다.

@Component
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public void printUsers() {
        userRepository.findUsers().forEach(System.out::println);
    }
}

@Repository
public class UserRepository {
    public List<String> findUsers() {
        return Arrays.asList("Alice", "Bob");
    }
}




 

 

# 참고

https://www.youtube.com/watch?v=8lp_nHicYd4

'프로그래밍 > Spring, SpringBoot' 카테고리의 다른 글

[Spring] Locale 사용하기  (0) 2024.04.02