스프링 Di (의존성 주입)
목차
1.Di(의존성 주입)?
2.의존성 주입 설정 방법
1.Di(의존성 주입)?
의존성 주입은 객체 간의 의존성을 외부에서 주입하는 디자인 패턴입니다. 이 패턴은 객체 간의 결합도를 낮추고 유연성을 향상시키는 데 사용이 됩니다.
그럼 의존성 주입은 왜 필요할까??
한번 예시를 보자.
// 메시지 전송 기능을 가진 클래스
class MessageService {
public void send(String message) {
// 메시지를 전송하는 로직
System.out.println("메시지 전송: " + message);
}
}
// 클라이언트 클래스
class Client {
private MessageService messageService;
// 의존성을 직접 생성하는 코드
public Client() {
this.messageService = new MessageService();
}
// 메시지 전송 기능을 사용하는 메서드
public void processMessage(String message) {
this.messageService.send(message);
}
}
public class Main {
public static void main(String[] args) {
Client client = new Client();
client.processMessage("안녕하세요!");
}
}
위의 코드에서 Client 클래스는 MessageService 클래스에 의존하고 있습니다. 그러나 이 의존성은 클래스 내부에서 직접 생성되고 있습니다. 이런 경우에는 다음과 같은 문제가 발생할 수 있습니다.
유연성 부족: 만약 다른 메시지 전송 방식이 필요하다면(MessageService의 다른 구현체), Client 클래스의 코드를 변경해야 합니다.
단일 책임 원칙 위반: Client 클래스가 메시지 전송 로직을 생성하고 있어 단일 책임 원칙을 위반하게 됩니다.
그럼 위의 코드를 의존성 주입을 통해서 구현을 해보겠습니다.
// 메시지 전송 기능을 가진 인터페이스
public interface MessageService {
void send(String message);
}
// 메시지를 콘솔에 출력하는 구현체
@Component
public class ConsoleMessageService implements MessageService {
@Override
public void send(String message) {
System.out.println("메시지 전송: " + message);
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
// 클라이언트 클래스
@Component
public class Client {
private final MessageService messageService;
@Autowired
public Client(MessageService messageService) {
this.messageService = messageService;
}
public void processMessage(String message) {
this.messageService.send(message);
}
}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
// 클라이언트 빈 가져오기
Client client = context.getBean(Client.class);
// 클라이언트 실행
client.processMessage("안녕하세요!");
// 스프링 컨텍스트 닫기
context.close();
}
}
이렇게 하면 스프링이 Client 클래스의 생성자를 호출하고 필요한 의존성을 자동으로 주입합니다. 그리고 Client 클래스에서는 messageService 필드에 할당된 의존성을 사용하여 메시지를 전송할 수 있습니다.
2. 의존성 주입 방법
의존성 주입방법으로는 대표적으로는 1) 필드 주입 2) Setter를 이용한 주입 3) 생성자 주입 이렇게 3가지가 있습니다.
1) 필드 주입
필드 주입은 클래스에 선언된 필드에 생성된 객체를 주입해주는 방식입니다.
필드에 주입할때는 어노테이션을 사용합니다. 스프링에서 제공하는 @Autowired 어노테이션을 주입할 필드위에 명시해줍니다.
public class BoardService{
@Autowired
private BoardRepository boardRepository;
}
2) Setter를 이용한 주입
Setter를 통해서 의존성을 주입해주는 방식입니다.
public class BoardService{
private BoardRepository boardRepository;
@Autowired
public void setBoardRepository(BoardRepository boardRepository){
this.boardRepository = boardRepository;
}
}
3)생성자 주입
생성자 주입은 클래스의 생성자를 통해서 의존성을 주입해주는 방식입니다. 생성자 주입은 인스턴스가 생성될때 1회 호출되는것이 보장됩니다. 생성자 주입시 필드에 final키워드를 사용할 수 있습니다.
public class BoardService{
private final BoardRepository boardRepository;
@Autowired
public BoardRepository(BoardRepository boardRepository){
this.boardRepository = boardRepository;
}
}
특히 생성자 주입방식은 스프링에서도 권장을 하는데 권장을 하는 이유는 다음과 같습니다.
객체의 불변성(immutable)
생성자 주입 방식을 사용할 경우 객체가 생성되는 시점에 생성자를 호출하여 최초 1회만 주입합니다. 이와 같은 특성으로 불변(immutable) 객체를 보장합니다. 또한 객체를 포함한 클래스가 생성되는 시점에 포함된 객체들도 반드시 생성되기 때문에 객체가 비어있을 가능성도 배제합니다.
순환 참조 문제 방지 가능
순환 참조란 A,B 두 객체가 각각 서로를 필드에 포함하여 참조하고 있는 상태를 말합니다. 서로가 서로를 참조하고 있기 때문에 A,B 두 클래스가 맞물려서 서로의 객체를 계속 생성하는 무한반복 상태에 빠지는 문제가 발생합니다. 3가지 방식 모두 순환 참조 문제가 발생하지만 발생 시점이 다릅니다. 필드주입, 수정자주입은 실제 메소드가 호출되었을때 runtime 에러와 함께 수정자 주입 문제가 불거지게 됩니다. runtime에 에러가 발생하게 되면 미리 예측이 어렵기 때문에 서비스 진행중에 문제를 야기할 수 있습니다. 생성자 주입은 스프링 어플리케이션이 구동되는 순간에 에러가 발생합니다. 즉 컴파일 타임에 에러가 발생하는 것이죠. 컴파일 타임에 발생하는 에러는 개발자가 쉽게 추적이 가능하기 때문에 서비스 진행전 미리 예방할 수 있습니다.
테스트 용이
단위 테스트를 진행할때 순수 자바 코드로 테스트가 가능합니다. 필드 주입을 사용하는 경우에 순수 자바 코드에서는 DI가 이루어지지않기 때문에 필드가 null 상태가 되어 에러가 발생합니다.