본문 바로가기
Spring

Spring. DI / IOC / Bean 이란?

by 리승우 2022. 10. 6.

[핵심요약 내용]

DI(Dependency Injection)

DI(Dependency Injection)란 스프링이 다른 프레임워크와 차별화되어 제공하는 의존 관계 주입 기능으로,
객체를 직접 생성하는 게 아니라 외부에서 생성한 후 주입 시켜주는 방식이다.

DI(의존성 주입)를 통해서 모듈 간의 결합도가 낮아지고 유연성이 높아진다.

 

Ioc(Inversion of Control)

IoC(Inversion of Control)란 "제어의 역전" 이라는 의미로, 말 그대로 메소드나 객체의 호출작업을 개발자가 결정하는 것이 아니라, 외부에서 결정되는 것을 의미한다.

IoC는 제어의 역전이라고 말하며, 간단히 말해 "제어의 흐름을 바꾼다"라고 한다.

객체의 의존성을 역전시켜 객체 간의 결합도를 줄이고 유연한 코드를 작성할 수 있게 하여 가독성 및 코드 중복, 유지 보수를 편하게 할 수 있게 한다.

 

  • 일반적인 의존성에 대한 제어권 : 개발자가 직접 의존성을 만든다.
  • 의존성은 쉽게 말해 어떤 객체가 사용해야 할 객체라고 할 수 있고, 이것을 직접 new 등을 써서 만들어 쓰면 의존성을 자기가 직접 만들어 쓴다고 할 수 있습니다.
public class OwnerController {
	private OwnerRepository ownerRepository = new OwnerRepository();
}
// OwnerController가, 필요한 OwnerRepository의 객체를 직접 생성하는 경우

 

  • Inversion of Control - 제어권 역전
  • 말 자체는 어렵게 들릴 수 있지만, 위에서처럼 직접적으로 의존성을 만들지 않고, 외부에서 의존성을 가져오는 경우를 말합니다.
  • 즉, 밖에서 나에게 의존성을 주입해주는 것을 DI(Dependency Injection) 라고 합니다. 따라서 DI는 IoC의 일종이라고 생각하시면 됩니다.
public OwnerController(OwnerRepository clinicService, VisitRepository visits) {
		this.owners = clinicService;
		this.visits = visits;
}
// OwnerController의 생성자에서 OwnerRepository를 인자로 받고, owners에 담고 있다.
// 이는 앞의 예시처럼 객체를 직접 생성하지 않고, 외부의 객체를 받고 있는 것이다.

스프링에서는 다음과 같은 순서로 객체가 만들어지고 실행된다.

  1. 객체 생성
  2. 의존성 객체 주입. 스스로가 만드는것이 아니라 제어권을 스프링에게 위임하여 스프링이 만들어놓은 객체를 주입한다.
  3. 의존성 객체 메소드 호출

스프링이 모든 의존성 객체를 스프링이 실행될때 다 만들어주고 필요한곳에 주입시켜줌으로써 Bean들은 싱글턴 패턴의 특징을 가지며, 제어의 흐름을 사용자가 컨트롤 하는 것이 아니라 스프링에게 맡겨 작업을 처리하게 된다.

 

Bean

스프링 IoC 컨테이너가 관리하는 객체들을 Bean 이라고 부릅니다. 스프링은 이러한 Bean들의 의존성을 관리하고, 객체를 만들어 주며, Bean으로 등록을 해 주고, 이렇게 만들어진 것들을 관리합니다. 개발자가 이 부분까지 신경쓰지 않아도, 프레임워크가 알아서 해 주는 것입니다.

 

스프링에서의 의존성 주입은 반드시 Bean으로 등록된 객체들 끼리만 가능합니다. 스프링 IoC 컨테이너는 Bean으로 등록되지 않은 객체에는 의존성 주입을 해 주지 않습니다.

 

  • 위에서 예시를 들었듯, OwnerController 생성자에서 의존성 주입이 일어나고 있습니다.
  • 그러면 OwnerController, OwnerRepository 모두 Bean이어야 할 것입니다.

 

Bean으로 등록하는 방법은?

@Component Annotation을 붙인다!

 

 

 

 

[실전예시!!]

스프링에서는 @component로 객체를 클래스 메모리 heap에 생성한다.

@Autowired는 생성된 객체를 특정 변수에 넣어준다.

 

[그림과정]

1. Class A를 @component 를 사용하여 heap 메모리에 올림 (Bean생성)

    기존 new() 를 통한 객체 생성이 아닌, 어노테이션으로 스프링이 알아서 객체 생성하게 함!

 

2. Class B에서 @Autowired + 생성자를 이용하여 A a 변수에 A객체를 주입해준다.

     초기화 후 component bean을 주입해준다. 그래야만 사용이 가능하다!

WHY? 
스프링에서의 의존성 주입은 반드시 Bean으로 등록된 객체들 끼리만 가능합니다. 스프링 IoC 컨테이너는 Bean으로 등록되지 않은 객체에는 의존성 주입을 해 주지 않습니다.

 

 +추가정보) RequiredArgsConstructor@Autowired+생성자 방식을 자동으로, 그리고 더 짧은 방식으로 처리해준다)

 

3. 이제 B 클래스에서는 A클래스의 필드를 사용할 수 있다.

 

해당 작업을, 의존성 주입이라고 한다.

 

의존성이 뭘까?

//HelloWorld.java
 class HelloWorld {
 	private SayHello sayHello;
    
    public HelloWorld() {
    	this.sayHello = new SayHello();
    }
    
    public startHelloWorld() {
    	this.sayHello.hello();
    }
 }

HelloWorld 클래스에서 hello함수가 호출되기 위해서는 SayHello 클래스가 필요하다.

 

이 때 HelloWorld 클래스는 SayHello 클래스의 의존성을 가진다고 이야기한다.

 

왜 @Autowired를 지양해야 할까?

@Controller
public class BoardController {
	@Autowired private IBoardItemService boardItemService;
	
    /* 이하 생략 */
}

@Autowired를 활용한 의존성 주입을 필드 주입이라고 한다.

필드 주입은 사용법이 매우 간단해서 대부분 의존성 주입을 필드 주입으로 접하지만,

편리하단 것 말고는 장점이 없어서 스프링 4.3부터는 사용하지 않는 것을 권장한다고 한다.

 

그럼 @Autowired 대신 뭘 써야 하나?

Spring Team recommends: “Always use constructor based dependency injection in your beans. Always use assertions for mandatory dependencies”.

스프링팀에서는 항상 생성자 주입을 사용하는 것을 권장하고 있다.

 

생성자 주입을 사용할 경우 아래와 같은 장점이 있다고 한다.

① 순환 참조 방지

② 테스트 코드 작성 용이

③ 코드 악취 제거

④ 객체 변이 방지 (final 가능)

 

위의 예제를 필드 주입에서 생성자 주입으로 바꿔보겠습니다.

@Controller
public class BoardController {
	private final IBoardItemService boardItemService;
    
    public BoardController(IBoardItemService boardItemService) {
    	this.boardItemService = boardItemService;
    }
    
    /* 이하 생략 */
}

더 편하게 하려면?

생성자 주입으로 작성하게 되면 객체가 추가될 때마다 선언을 하고 생성자를 수정해줘야 하는데 여간 번거로운 게 아니다.

 

그럴 때는 lombok에서 제공하는 @RequiredArgsContructor 어노테이션을 활용하면 선언만으로도 생성자 주입이 가능하다.

@Controller
@RequiredArgsConstructor
public class BoardController {
	private final IBoardItemService boardItemService;
    
    /* 이하 생략 */
}

 

@AllArgsConstructor

  • 클래스의 모든 필드에 대한 생성자를 자동으로 생성합니다.
    인스턴스 변수로 선언된 모든 것을 파라미터로 받는 생성자를 생성하게 됩니다.

1. Lombok 적용 전

public class Test {

    private final Long age;
    private final String name;
    private String address;

    public Test(Long age, String name, String address) {
        this.age = age;
        this.name = name;
        this.address = address;
    }

    public Long getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

2. 롬복 적용 후

import lombok.*;

@AllArgsConstructor
@Getter @Setter
public class Test {

    private final Long age;
    private final String name;
    private String address;
}
public class Execute {

    public static void main(String agrs[]) {

        Test test = new Test(10L, "veneas", "seoul");

        System.out.println(test.getAge());
        System.out.println(test.getName());
        System.out.println(test.getAddress());
    }
}

@RequiredArgsConstructor

  • 여러 개의 인스턴스 변수들 중에서 final이 붙은 특정한 변수에 대해서만 생성자를 작성하고 싶다면 사용합니다.
  • 클래스의 final 필드에 대한 생성자를 자동으로 생성합니다. 
  • final 필드는 클래스에서 초기화를 하던지 객체 생성 시 생성자를 이용해 꼭 초기화해줘야 합니다.

1. Lombok 적용 전

public class Test {

    private final Long age;
    private final String name;
    private String address;

    public Test(Long age, String name) {
        this.age = age;
        this.name = name;
    }

    public Long getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

2. 롬복 적용 후

import lombok.*;

@RequiredArgsConstructor
@Getter @Setter
public class Test {

    private final Long age;
    private final String name;
    private String address;
}
public class Execute {

    public static void main(String agrs[]) {

        Test test = new Test(10L, "veneas");

        System.out.println(test.getAge());
        System.out.println(test.getName());
    }
}

Spring 의존성 주입(dependency injection) 활용

  • Spring 생성자를 활용한 의존성 주입시 사용할 수 있습니다.
  • 다음 예시는
    1. 서비스 코드에서 리포지토리 주입

1. 서비스 코드에서 리포지토리 주입

[Lombok 적용 전]

import org.springframework.stereotype.Service;

@Service
public class TestService {

    private final TestRepository testRepository;

    public TestService(TestRepository testRepository) {
        this.testRepository = testRepository;
    }
}

 

[Lombok 적용 후]

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

@Service
@RequiredArgsConstructor
public class TestService {

    private final TestRepository testRepository;
}

댓글