[SpringBoot] 의존성 주입(DI) 알아보기

728x90

안녕하세요🖐

오늘은 스프링부트에서 의존성 주입에 대하여 알아볼 것 입니다.

 

스프링에서 중요한 개념인 Spring Core 관련된 내용은 꼭 알고서 개발을 시작해야 합니다.

대표적으로 POJO, AOP, DI 등 중요 개념들이 있습니다.

그 중에서 오늘은 DI에 대하여 알아보겠습니다.

 

의존성 주입은 스프링 을 사용하는 백엔드 개발자라고 하면은 개발시 자주 사용하고 있을 것 입니다.

 

저도 개발을 하면서 객체에 의존성 주입을 하는 상황이 자주 있습니다.

 

항상 궁금했습니다. 어떻게 이 방식으로 Bean에 의존성이 주입이 되고, 내가 원하는 객체에서 메소드를 사용하고

또 어떠한 방법들이 있을까? 그래서 이번 기회에 의존성주입(Dependency Injection) 에 대하여 자세하게 알아보았습니다.

모든 글은 Why 와 How에 초점을 맞춰서 작성을 했습니다.

 


 

의존성 주입이란?

의존성 주입은 스프링 컨테이너에 Bean을 먼저 생성해두고 지정된 객체에 주입하는 방식

 

추가적인 설명으로 스프링 에서 의존성 주입은 객체 지향 프로그래밍의 한 원칙으로서, 클래스 간의 의존 관계를 외부에서 주입하는 것을 의미합니다. 이는 코드의 결합도를 낮추고 유연성을 높이는 데 도움이 됩니다.

 

조금 더 설명을 붙이자면 스프링 부트에서 제공하는 IoC컨테이너를 사용하여 의존성 주입을 지원합니다.

IoC는 제어의 역전을 의미하며, 어플리케이션의 흐름을 개발자가 제어하는 것이 아닌

스프링 프레임워크 or 컨테이너에게 맡기는 개념입니다.

 

의존성 주입은 객체 자체가 코드 상에서 객체 생성에 관여하지 않아도 되기때문에 객체 사이의 의존도를 낮출수 있습니다.

 

 

그럼 Why 의존성 주입을 해야할까?

그렇다면 의존성 주입을 하지않는 코드를 보여드리겠습니다.

public class BoardRepository {
    
    public List<Board> getAllBoards() {
        // 데이터베이스에서 모든 게시글을 가져오는 로직
    }

    public Board getBoardById(int id) {
        // 데이터베이스에서 특정 ID의 게시글을 가져오는 로직
    }
}

 

위 코드는 비즈니스 로직이 있는 레포지토리 클래스 입니다. 

 

@Service
public class BoardService {
    // 직접적으로 게시글 리포지토리를 생성
    private BoardRepository boardRepository = new BoardRepository();

    public List<Board> getAllBoards() {
        return boardRepository.getAllBoards();
    }

    public Board getBoardById(int id) {
        return boardRepository.getBoardById(id);
    }
}

 

위 서비스 클래스는 new 연산자를 사용하여 BoardRepository 객체를 직접 생성해서, 객체를 핸들링 합니다.

 

위 처럼 코드를 짜게 되면 문제점은 BoardService 가 BoardRepository에 의존하게 되며, 결합도가 높아집니다.

 

좋은 프로그램은 결합도가 낮고, 응집도가 높아야합니다.  (결합도=의존)

 

위 처럼 코드를 작성하게 되면은 나중에 DB 나 BoardRepository에 변경이 발생할 경우  BoardService에 다시 코드를 수정해야 하는 번거로움이 있습니다.

또한 테스트 시에도 불편함이 있습니다. 모의 객체(mock object)를 주입하기 어렵고, 테스트 케이스 간에 객체를 공유하기 어렵습니다. 

 

이러한 명확한 단점들 때문에 직접 생성을 하는게 아닌 의존성 주입을 통해 객체간의 의존을 줄이고, 테스트 용이성을 향상시켜야 합니다. 

 

 

 

How? 어떻게 하는 걸까?

 

그렇다면 의존성 주입이 좋은걸 알겠는데, 어떻게 하면 될까요? 

 

스프링부트에서는 대표적으로 3가지 방식이 있습니다. 

  • 필드 주입
  • 세터 주입
  • 생성자 주입

그럼 이제 위 방식에 대하여 알아보겠습니다.

 

필드 주입

필드 주입은 클래스에 선언된 필드에 생성된 객체를 주입해주는 방식입니다.

필드에 주입하는 방법은 아주 간단 합니다

@Autowired  어노테이션을 선언하기면 하면 끝 입니다.

@Service
public class BoardService {
    // 필드 주입 방식
    @Autowired
    private BoardRepository repository
}

이렇게 간단하게 사용할 수 있습니다. 

 

특징

  • 스프링이 자동으로 의존성을 주입을 해주기 때문에 코드가 간결한다
  • 테스트 목적에서는 의존성 추적이 어렵다

 

그럼 어떻게하면 @Autowired 선언만으로 의존성 주입이 되는지 궁금하시지 않나요?

https://beststar-1.tistory.com/40#comment18558026

 

@Autowired의 동작원리

@Autowired란? 의존관계 주입(DI)을 할 때 사용하는 어노테이션(Annotation)이며, 의존 객체의 타입에 해당하는 빈(Bean)을 찾아 주입하는 역할을 한다. 💡 의존관계 주입에 대해서는 IoC(Inversion of Control,

beststar-1.tistory.com

위 블로그에 아주 자세하게 기술되어 있습니다. 다음 기회에 제가 직접 포스팅해서 올려보도록 하겠습니다....

 

세터 주입

세터 주입은 클래스의 Setter를 통해서 의존성을 주입하는 방식 입니다.

@Service
public class BoardService {
    // 세터 주입 방식
    
    private BoardRepository repository;
    
    @Autowired
    public void SetBoardRepository(BoardRepository repository) {
    	this.repository = repository
    }
}
  • repository 객체를 선언을 해두고
  • @Autowired 를 통해 Boardrepository 객체에 대한 setter를 생성 후
    • this 를 통해 둘이 같은 객체라고 명시를 해두었습니다.

 

생성자 주입

생성자 주입은 클래스의 생성자를 통해서 의존성을 주입하는 방식 입니다.

생성자 주입은 인스턴스가 생성될 때 1회 호출되는 것이 보장이 됩니다. 

생성자 주입시 필드에 final 키워드를 사용할 수 있습니다.

@Service
public class BoardService {
    // 생성자 주입 방식
    private final BoardRepository repository;
    
    @Autowired
    public void BoardService(BoardRepository repository) {
    	this.repository = repository
    }
}

 

  • 위 코드에서는 @Autowired 어노테이션을 사용합니다만,
    • 스프링 4.3 이상에서는 생성자가 하나뿐인 경우 @Autowired 어노테이션이 없어도 자동으로 주입됩니다.
    • 현재는 스프링 버전이 6까지 나온 상태이므로 @Autowired 를 생성해도 되고, 안해도 됩니다만, 모르는 사람들을 위해 생성해두는 것을 추천합니다... 없으면 가독성면에서 좋긴 합니다.

생성자 주입을 할시 2가지 특징이 있습니다

1) 클래스 안에 생성자는 한개만 있어야함

2) 주입받을 객체가 Bean에 등록이 됩니다.

 

두가지 특징을 활용하여 더 간편한 생성자 주입을 만들 수 있습니다. 

 

바로 LOMBOK 라이브러리를 활용하면 됩니다. 

@RequiredArgsConstructor 어노테이션을사용하면 됩니다.

@RequiredArgsConstructor
@Controller
public class AnswerController {
	private final QuestionService questionService;
	private final AnswerService answerService;
	private final UserService userService;
    
    }
 }

 

위 특징을 이용하면 한 클래스 내에 여러가지 객체를 주입을 받을 수 있습니다. 

 

질문🖐 @RequiredArgsConstructor 는 무엇일까요 그럼?

 

Lombok라이브러리에서 의존성 주입의 방법 중에 생성자 주입을 임의의 코드없이 자동으로 설정해주는 어노테이션이다.

@RequiredArgsConstructor는 초기화 되지않은 final 필드나, @NonNull 이 붙은 필드에 대해 생성자를 생성해 줍니다.

 

즉, 클래스에 선언된 final 변수들, 필드들을 매개변수로 하는 생성자를 자동으로 생성해주는 어노테이션입니다.

 

위 방법을 통해서 조금 더 가독성이 좋은 코드를 만들어 보았습니다. 

 

 

그렇다면 어떤 주입 방식을 사용해야 할까요?

위 방법 중에 각각의 장,단점이 있다고 생각합니다. 본인 편한대로 사용하시는게 좋을 것 같습니다.

대부분 사람들은 @Autowired를 사용하고 있을 것 이라고 생각합니다. 저도 마찬가지 이구요.

 

그러나 스프링에서는 ❗생성자 주입❗ 방식을 권장합니다.

 

 

why? 이 방법을 권장하는 걸까요?

 

1. 불변성(Immutability) : 생성자 주입은 주입받은 의존성을 변경할 수 없는(=불변성) 객체를 생성합니다. 이는 객체의 상태가 변경을 막아 주며, 안전한 코드가 됩니다.

 

2. 의존성 주입을 필수 : 생성자 주입을 사용하면 객체를 생성할 때 필요한 의존성을 반드시 주입받아야 합니다. 이는 필요한 의존성이 주입되지 않으면 객체를 생성할 수 없다는 것을 의미하며, 이는 코드의 안전성을 높여줍니다.

 

3. 테스트 용이성: 생성자 주입은 테스트 용이성을 증가시킵니다. 특히, 생성자를 통해 의존성을 주입받으면 테스트에서 모의 객체(mock object) 또는 가짜 객체를 주입하기가 더 편리하며, 테스트 코드를 작성하기 쉬워집니다.

 

4. 순환 참조 문제 예방: 생성자 주입을 사용하면 순환 참조 문제를 방지할 수 있습니다. 생성자 주입을 통해 의존성을 주입할 때, 순환 참조가 발생하면 스프링은 해당 빈의 생성을 중단하고 예외를 발생시킵니다.

 

5. 코드 가독성 향상: 생성자 주입은 코드의 가독성을 향상시킵니다. 생성자를 통해 필요한 의존성을 명시적으로 나타내므로 코드를 읽을 때 어떤 의존성이 필요한지 쉽게 이해할 수 있습니다.

 

제일 중요한 요점은 생성자로 주입을 받으면, 해당 클래스의 의존성 관계가 변경될 때 마다 생성자 코드를 계속해서 수정하는 번거로움을 해결해주기 때문입니다.

 

 

🎁 이상 생성자 주입 DI에 대하여 알아보았습니다 🎁

 

질문은 항상 환영하며, 틀린 내용 지적해주시면 감사하겠습니다.

728x90