본문 바로가기

Spring

[Spring] DI와 IoC에 대한 공부

Dependency (의존관계)

A가 B를 의존하면 B가 변하면 A에도 영향을 미치는 관계를 말한다. 

public class Computer {
    private SamsungRam16g samsungRam16g;

    public Computer() {
        samsungRam16g=new SamsungRam16g();
    }
}

위 코드처럼 Computer는 SamsungRam16g에 의존하게 된다. 필자는 간단한 예시로 하나의 클래스에서만 들었지만 실제로 이렇게 모든 코드를 짠다면 만약에 SamsungRam16g이 아니라 SamsungRam32g으로 변경하거나 다른 램으로 변경을 할 경우 쓰고 있는 모든 코드를 변경해야 한다.

public class Computer {
    private Ram ram;

    public Computer() {
        ram=new SamsungRam16g(); //Ram을 변경하고 싶으면 수정하는 부분
    }
}

public interface Ram {
    void runRam();
}

public class SamsungRam16g implements Ram{
    @Override
    public void runRam() {
        System.out.println("운영 중입니다.");
    }
}

 

그렇기에 위 코드처럼 인터페이스를 추가하면 기능과 책임을 분리되어 수정을 할 때 영향을 덜 받는다. 실제로 Ram을 SamsungRam32g이나 다른 램으로 코드를 변경한다고 해도 생성자 부분만 변경하면 되기에 영향을 덜 받는다.

DI (Dependency Injection)

외부에서 객체 간의 관계를 결정해 주는 것을 의미한다. 객체를 직접 생성하는 게 아니라 외부에서 생성하고 주입한다는 것이다.

위 그림을 보면 DI는 외부에서 객체를 생성하고 주입한다.

 

자바에서 하는 DI방법에서는 2가지가 있다.

1. Setter 주입

public class Computer {
    private Ram ram;
    
    public void setRam(Ram ram) {
        this.ram = ram;
    }

    public void call(){
        System.out.println("컴퓨터가 켜졌습니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Computer computer = new Computer();

        computer.setRam(new SamsungRam16g());

        computer.call();
    }
}

 

2. 생성자 주입

public class Computer {
    private final Ram ram;

    public Computer(Ram ram) {
        this.ram = ram;
    }
    
    public void call(){
        System.out.println("컴퓨터가 켜졌습니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Computer computer = new Computer(new SamsungRam16g());

        computer.call();
    }
} 

setter와 생성자 코드를 보면 객체를 Main에서 생성한다. 그리고 Computer 내에서는 다형성으로 값을 주입받고 상세하게 어떤 값을 받는지 신경 쓰지 않는다. 이렇게 코드를 바꾸면 수정할 때 Main에서 객체를 생성할 때만 바꾸어주면 되기에 편리하다.

 

스프링에서 제공하는 DI 방법에는 3가지가 있다.

 

1. 필드 주입

@Component
public class Computer {
	
    @Autowired
    private Ram ram;
}

 

2. Setter 주입

@Component
public class Computer {
    private Ram ram;
    
    @Autowired
    public void setRam(Ram ram) {
        this.ram = ram;
    }
}

 

3. 생성자 주입

@Component
public class Computer {
    private final Ram ram;

    @Autowired
    public Computer(Ram ram) {
        this.ram = ram;
    }
}

위 3가지 방법을 보면 파라미터로 Ram의 값을 받는다. 이로써 스프링은 외부에서 객체를 가져올 수 있다. 

 

순환참조

생성자 주입에서 순환참조를 생각할 수 있다. 순환참조는 A클래스가 B클래스의 Bean을 주입받고, B클래스가 A클래스의 Bean을 주입받는 상황에서 문제가 발생한다. 

 

여기서 서로가 주입을 받는 상황에 왜 문제가 발생할까? 

 

이는 spring boot의 Bean생성과정을 보면 이해가 가능하다. spring boot는 a>b>c로 의존하는 클래스가 있다면 Bean을 생성할 때 의존하지 않는 c>b>a순으로 Bean을 생성한다. 그런데 예시에서 보면 A클래스가 B를 의존하고 B클래스가 A를 의존하는 서로가 의존을 해버리는 상황을 마주한다. 서로가 서로를 의존하여 Bean을 생성하지 못하는 상황 된다. 이를 순환참조문제라 한다.

 

먼저 필드 주입과 setter주입은 순환참조가 되지 않는다. 그 이유는 클래스끼리의 로직 호출은 순환참조가 아니라 순환호출이 된다. 하지만 생성자 주입에서는 순환참조가 발생한다. 왜냐하면 생성자는 서로의 메서드를 호출하기에 순환참조 문제가 발생한다.

 

한 가지 예시를 보자.

public interface AnimalService {
    void animalMethod();
}

@Service
public class AnimalServiceImpl implements AnimalService{

    private final PeopleService peopleService;

    public AnimalServiceImpl(PeopleService peopleService) {
        this.peopleService = peopleService;
    }

    @Override
    public void animalMethod() {
        peopleService.peopleMethod();;
    }
}
public interface PeopleService {
    void peopleMethod();
}

@Service
public class PeopleServiceImpl implements PeopleService{

    private AnimalService animalService;

    public PeopleServiceImpl(AnimalService animalService) {
        this.animalService = animalService;
    }

    @Override
    public void peopleMethod() {
        animalService.animalMethod();
    }
}

위 코드를 간단하게 설명하면 Aniaml ServiceImpl와 PeopleServiceImpl은 생성자 주입으로 서로를 참조한다. 

위 코드를 실행하면 아래와 같이 순환참조문제라는 결과를 낸다.

이를 해결하기 위해서는 @Lazy를 활용하거나 순환 참조되는 구조를 재설계하여 순환 참조가 되지 않게 한다. 스프링에서는 @Lazy방식을 권장하지 않는다.

 

loC(Inversion of Control) 

제어의 역전이라는 의미로, 프로그램의 제어 흐름을 직접 제어하는 게 아니라 외부에서 관리하는 것을 의미한다. 즉 제어권을 상위 템플릿 메서드에 넘기고 자신은 필요할 때 호출되어 사용되는 것을 의미한다.

 

한 가지 예시로 프레임워크와 라이브러리를 생각해 보면 된다. 라이브러리는 개발자가 직접 라이브러리를 호출하며 관리한다. 프레임워크는 객체를 개발자가 아닌 프레임워크가 관리한다. 여기서 관리는 객체의 생성부터 소멸까지 모든 생명주기를 프레임워크가 관리한다. 이처럼 제어 통제권이 프레임워크로 넘어가는 것을 IoC라고 한다. 

 

즉 스프링에서는 IoC를 사용하여 스프링 프레임워크가 객체의 생명주기를 관리하고 이 관리된 객체를 각클래스에서 DI를 통해 사용한다. DI를 통해서 사용하기에 객체 간의 결합이 느슨하여 확장성이 뛰어난 코드가 된다.

 

스프링 컨테이너

스프링의 스프링 컨테이너에서 Bean을 생성하고 DI를 관리한다. 여기서 Bean은 스프링 컨테이너에서 생성된 자바 객체를 말한다. 스프링 컨테이너는 ApplicationContext로  IoC 컨테이너, DI 컨테이너라고 부른다. 

ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

위 코드는 스프링 컨테이너를 생성하고 구성 정보를 AppConfig.class로 설정한 코드이다. 이 처럼 스프링 컨테이너에 Bean을 생성하고 관리하는 과정을 IoC와 DI를 통해 스프링에서 사용한다.

위 그림처럼 IoC로 IoC컨테이너에서 객체를 생성하고 DI를 통해서 생성된 Bean을 주입받는다.

 

 

 

 

출처 

https://product.kyobobook.co.kr/detail/S000000935360

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

https://tecoble.techcourse.co.kr/post/2021-04-27-dependency-injection/

https://minsoolog.tistory.com/52

https://velog.io/@gillog/Spring-DIDependency-Injection

https://yaboong.github.io/spring/2019/08/29/why-field-injection-is-bad/

 

 

 

 

'Spring' 카테고리의 다른 글

[Spring] 프록시와 AOP에 대한 공부  (0) 2024.07.06
[Spring] Bean에 대한 공부  (0) 2024.05.27