TIL(Today I Learned)

Day.32 JDBC -> JPA까지 확장 -> 그 속에서 DI, IoC

지팡구 2022. 12. 14. 11:09
제공받은 강의에서 JDBC를 시작으로 JPA까지 연결하는 부분을 직접 따라하며 만약 JPA가 없었더라면? 이라는 환경을 직접 느끼게 되었다. 

이러한 과정 속에서 의존성 주입 (DI)를 학습하게 되었다.

 

DI (Dependency Injection)

의존성 주입 혹은 의존관계 주입이라고 불리는 DI는 Dependency Injection의 약어로, Spring이 다른 프레임워크와 차별화 된 의존 관계의 주입 기능으로, 객체를 직접 생성하는 것이 아니라 외부에서 생성한 후 주입시키는 방식을 말한다.

(의존 관계를  Bean 정보를 바탕으로 컨테이너가 자동으로 연결해주는 것)

 

Q. 그럼 Spring DI 컨테이너란?

spring DI 컨테이너가 관리하는 객체를 bean (빈) 이라고 하며, 이 bean (빈) 을 관리하는 의미로 컨테이너를 bean factory(빈 펙토리)라고 부른다. 

스프링에선 객체를 Bean이라고 부르는데, 프로젝트가 실핼될 때 사용자가 Bean을 관리하는 객체들의 생성과 소멸에 관련된 작업을 자동적으로 수행하는데, 객체가 생성되는 곳을 스프링에서는 Bean컨테이너라고 부른다.

 

 

이를 통해 모듈 간 결합도가 낮아지고, 유연성이 높아진다 == 코드의 단순화, 결함도 제거

A가 B를 의존하고 있다고 가정해보자,

B의 내용에 변화가 생기면 그 영향이 A까지 미친다.

A라는 객체에서 B 객체를 사용(의존)할 때, A 객체에서 직접 생성하는 것이 아니라 외부(IOC 컨테이너)에서 생성된 B 객체를 조립(주입) 시켜 Setter 혹은 생성자를 통해 사용한다.

 

 

Q. 그럼 IoC는?(Inversion of Control)무엇인가?

IoC란 메서드나 객체의 호출작업을 개발자가 결정하는 것이 아닌, 외부에서 결정되는 것을 의미하며 "제어의 역전"이라고 말하며 쉽게 말해 "제어의 흐름을 바꾼다"라고 한다.

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

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

 

 

Q. 그럼 왜 의존성 주입(DI)를 지원하나요?

Spring framework 에선 3가지 핵심 프로그래밍 모델을 지원하는데, 그 중 하나가 의존성 모델(DI)이며 위에서 설명했던것 처럼 외부에서 두 객체 간의 관계를 결정해주는 패턴으로 인터페이스를 사이에 둬서 클래스레벨에서는 의존관계가 고정되지 않도록 하고 런타임시에 관계를 다이나믹하게 주입해 유연성을 확보하고 결합도를 낮출 수 있게 해준다.

 

public class Ballad{}
public class Music{

private Ballad ballad;

public Music(){
this.Ballad= new Ballad();
      }
}

 

예시로 발라드와 음악이라는 Music 클래스가 있다고 가정하면, 위와 같은 두 클래스는 다음과 같은 문제점을 가지고 있다.

- 객체들 간의 관계 x , 클래스 간의 관계 o

올바른 객체지향적 설계라면 객체들 간의 관계가 맺어져야 하지만, 지금은 클래스 간의 관계를 맺고 있으며, 만약 객체들간의 관계가 맺어졌다면 다른 객체의 구체 클래스를 전혀 알지 못해도, 인터페이스 타입으로 사용할 수있다..

- 두 클래스 결합되어 있음

만약에 Music에 발라드가 아닌 힙합으로 변경하고자 한다면, Music 클래스에 생성자 변경이 필요하다.

즉 유연성이 떨어지며, 이에 대한 해결책으로 상속을 생각할 수 있지만, 상속은 제약이 많고 확장성이 떨어져 피하는 것이 좋다.

 

이 문제에 대한 해결책

의존성 주입(DI)를 통한 문제 해결

위와 같은 문제를 해결하기 위해선 먼저 다형성이 필요한데, 힙합, 발라드, 댄스 등 여러 노래를 하나로 표현하기 위해선 interface가 필요하며, 발라드에 우선 인터페이스를 구현한다.

public interface OldMusic{}

public class Ballad implements OldMusic{}

기존에 결합된 Ballad와 Music을 떼어주며, 다음과 같이 외부에서 상품을 주입(Injection)받아야 한다.

public class Music{

private OldMusic oldmusic;

public Music(Oldmusic oldmusic){
this.oldmusic = oldmusic;
  }

}

여기서 Spring이 DI컨테이너를 필요한 이유를 알 수있는데, 우선 Music에 OldMusic 객체를 주입하기 위해선 애플리케이션 실행 시점에 필요한 Bean(빈, 객체)를 생성해야 하며, 의존성이 있는 두 객체를 연결하기 위해 한 다른 객체를 다른 객체로 주입시켜야 한다. 그래서 밑에 코드를 보면 Ballad라는 객체를 만들고 그 객체를 Music으로 주입시켜주는 역할을 위해 DI컨테이너가 필요하게 된다.

 

Public class BeanFacory{

public void Music(){

//Bean의 생성
OldMusic oldmusic = new Ballad();

//의존성 주입
Music music = new Music(ballad);
  }

}

 

1. 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스만 의존하고 있어야 한다.
2. 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
3. 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.
- 이일민, 토비의 스프링 3.1, 에이콘(2012), p114 -

DI (Dependency Injection)의 3가지 유형
  •  Setter injection (Setter 메서드를 이용한 의존성 삽입)
public class Case{
	private final Service service;
    
    @Autowired
    public void setCase(Service service){
    this.service = service;
    }
}
  •  Constructor injection ( 생성자를 이용한 의존성 삽입)
public class Case{
	private final Service service;
    
    @Autowired
    public Case(Service service){
    this.service = service;
    }
}
  •  Method injection ( 일반 메서드를 이용한 의존성 삽입)
public class Case{

	@Autowired
	private final Service service;
    

}

DI (Dependency Injection)의 장점
  • 재사용성 높은 코드

코드의 분리를 통해 다른 곳에서도 재사용이 가능하다.

 

 

  • 코드 간 의존성이 줄어듬

코드 간 의존성이 높으면 변경시 발생하는 비용이 증가하지만, 분리를 통해 그러한 비용을 감소시킬 수 있으며, 주입받는 대상이 변하더라도 구현 자체를 수정할 일이 없거나 줄어든다.

 

 

  • 코드의 가독성 증가

윗처럼 코드를 통해 생성자를 주입하면 번거롭다는 단점이 있다. 그래서 Lombok에서 생성자 주입과 관련해 지원해주는 방법들이 있는데, 바로 @RequiredArgsConstructor 이다.

 

@RequiredArgsConstructor 이 어노테이션은 final 필드나 @NotNull이 붙은 필드의 생성자를 자동으로 생성해주는데, 어노테이션을 선언만 하면 된다. 그럼 굳이 @Autowired를 붙일 필요가 없다.