IOC(Inversion of Control, 제어의 역전)
제어의 역전이란 프로그램의 제어 흐름 구조가 뒤바뀌는 것이다.
프로그램의 제어권이 개발자가 작성한 코드에 있는 것이 아니라 외부에 존재하는 것을 말한다.
이때 프로그램의 제어권에 포함되는 내용은 사용할 객체 결정, 객체 생성, 메서드 호출 등 프로그래밍 언어로 표현할 수 있는 모든 행위를 포함한다.
제어의 역전을 통해 제어권을 외부에 넘긴다는 것은 그에 대한 책임도 넘긴다는 것이다.
예를 들면 IOC 개념이 적용된 DI(의존주입)에서 객체 생성, 의존 객체 결정, 의존 객체 주입은을 프레임워크가 대신하니
이에대한 책임도 프레임워크가 갖고 개발자는 객체 생성, 사용 객체 결정에 대한 책임에서 벗어난다.
(프로그래밍 코드로 책임을 벗어날 수 있다는 것이고, xml 설정이나, 자바 config, 어노테이션 설정 같은 것은 구현해야한다.)
[제어의 역전 예시]
- DI : 객체 생성 및 생명 주기 관리
객체 결정, 생성, 사용, 관리의 책임을 외부로 돌림(일반적으로 사용하는 쪽 책임)
=> 사용하는 쪽에서 직접 생성자를 통해 객체를 생성하지 않고 스프링 IOC 컨테이너가 미리 생성된 객체의 참조를 주입시켜줌 - 프레임워크 : 실행 로직
개발자의 소스코드가 프레임워크에 의해 실행됨(템플릿/콜백 메서드)
=> 프레임워크가 적절할 시점에 나의 소스코드를 실행 시켜준다. 예를들면 사용자의 요청이 온 경우 서블릿을 통해 전달 받은 클라이언트 요청을 내가 정의한 컨트롤러의 경로와 일치한 http 메서드 타입에 메서드를 실행시킴
위와 같이 제어의 역전은 객체 생성과 생명주기에 관하여 적용될 수 있고, 실행 로직 전체가 제어의 역전 개념이 포함될 수 있다.
DI에 적용된 객체 생성과 관리에 대한 제어의 역전 개념을 좀 더 살펴보자
우리(객체)는 우리가 어떤 객체를 사용할지와 객체 생성, 객체가 갖고 있는 행위의 사용 권한을 갖고있다.
당연히 이에 대한 책임도 우리가 가지고 있다.
하지만 제어의 역전을 적용하면 사용 객체 결정, 객체 생성, 메서드 호출과 같은 권한을 외부에게 넘기고 그에 대한 책임도 외부에서 갖게끔 할 수 있다. DI는 객체결정과 객체 생성의 책임을 외부에 넘기는 것이다.
객체를 사용하는 쪽에서 생성자를 통해 생성하지 않아도 되며 어떤 객체를 사용할지 구체적으로 알 필요도 없다.
DI(Dependency Injection, 의존 주입)
DI는 의존관계주입이라는 용어로 IOC 개념이 적용되어있다.
의존에 대해서 정의하고 가자. ‘의존한다’는 것은 객체가 자신의 행위(책임)를 수행하기 위해 다른 객체를 필요로 하는 것을 말한다.
의존 관계가 생기면 의존하는 객체의 변경사항에 영향을 받을 수 있다.
class Hamburger(val source: Source, val patty: Patty){
fun price() {
return source.price()+patty.price()
}
}
위 클래스에서 Hamburger 객체는 price를 제공하기 위해 Patty객체의 price를 사용한다.
Hamburger가 Patty를 의존하는 구조이다. 객체의 의존성에는 방향이 존재한다.
위의 경우에서는 Hamburger → Patty로 Hamburger는 Patty에 의존하지만 Patty는 Hamburger에 의존하지 않는다.
그렇다면 의존관계 주입이라는건 무엇인가? 의존관계 주입은 의존하는 객체를 주입시켜준다는 것이다.
즉, 생성자를 통해 직접 생성하지 않아도 프레임워크가 주입시켜준다는 것이다.
(객체 주입은 실제 객체 주입이라기 보다 참조 주소값을 주입시켜주는 것이다. 각각의 객체는 힙영역에 그대로 존재한다. 객체가 복사되어 의존하는 객체에 포함되는 것은 아니다. 싱글톤 유지)
예제를 통해 알아보자.
class GiveawayMachineComp(
val customInfoExtract: CustomInfoExtract,
) {
fun `5자리 랜덤 숫자로 고객 정보 추출`(): CustomInfo {
val 추첨번호 = (0..9).map { (0..9).random() }
return customInfoExtract.`당첨 고객 정보 추출`(추첨번호)
}
fun `5자리 랜덤 문자열로 고객 정보 추출`(): CustomInfo {
// 당첨 코드 추출
val chars = ('A'..'Z')
val 추첨코드 = (1..5)
.map { chars.random() }
return customInfoExtract.`당첨 고객 정보 추출`(추첨코드)
}
}
@JvmStatic
fun main(args: Array<String>) {
val 추첨기계 = GiveawayMachineComp(ACustomerInfoExtract())
val customInfo = 추첨기계.`5자리 랜덤 숫자로 고객 정보 추출`()
print(customInfo)
}
main 메서드를 보면 GiveawayMachineComp를 생성하기 위하여 CustomInfoExtract 객체를 생성하여 주입시켰다.
만약 다른 객체로 변경이 필요하다면 코드를 통해 직접 수정해야한다.
하지만 DI 가 적용된다면 위와 같은 객체 생성의 책임을 외부에 넘겨 코드 변경 없이 진행할 수 있다.
@SpringBootTest
class GiveawayMachineCompDITest(
@Autowired
val giveawayMachineComp: GiveawayMachineComp,
) {
@Test
fun test() {
val 고객정보 = giveawayMachineCompDI.`5자리 랜덤 문자열로 고객 정보 추출`()
print(고객정보)
}
}
GiveawayMachineComp를 Autowired를 통해 의존 주입받게 되면 GiveawayMachineComp의 참조속성인 CustomInfoExtract도 함께 생성하여 주입시켜준다. 어떤 곳에서도 객체를 생성하는 코드가 없다.
개발자는 DI를 통해 객체 생성 책임과 의존 객체 결정, 의존 객체 주입에서 자유로워질 수 있다.
이는 의존객체가 많아지고 의존관계가 복잡해질 수록 더욱 강력한 장점을 제공해준다.
아래와 같이 TestClass1,2,3도 의존하는 구조를 갖추게 됬다고 해보자.
class GiveawayMachineComp(
val customInfoExtract: CustomInfoExtract,
val testClass: TestClass1,
val testClass: TestClass2,
val testClass: TestClass3,
)
객체 생성을 우리가 직접 해야할 경우 어떻게 변하겠나
val 추첨기계 = GiveawayMachineComp(ACustomerInfoExtract(), TestClass1(), TestClass2(), TestClass3())
여기서 TestClass1,2,3이 아래와 같이 다른 클래스를 의존하는 구조를 갖추게 되면
class TestClass1(val testClassDepth1: TestClassDepth1)
class TestClass2(val testClass2Depth1: TestClass2Depth1)
class TestClass3(val testClass3Depth1: TestClass3Depth1)
우리의 코드는 다시 이렇게 변할 것이다.
val 추첨기계 = GiveawayMachineComp(ACustomerInfoExtract(), TestClass1(TestClassDepth1()), TestClass2(TestClass2Depth1()), TestClass3(TestClass3Depth1()))
의존성이 많아지고 의존관계가 복잡해질 수록 코드가 복잡해지고 이는 모두 변경사항의 대상이되고
개발자가 관리해야할 부분이 많아진다는 것을 의미한다.
하지만 @Component와 같은 어노테이션을 붙여 스프링 빈으로 등록하여 DI를 사용한다면 개발자는 의존성의 책임에서 벗어나 비즈니스 로직에 집중하여 개발을 진행할 수 있다.
DIP
IOC 개념이 적용된 DI를 잘 사용하기 위해서는 DIP 원칙을 지키는 것이 좋다. DIP 원칙에 대해 알아보자.
- 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
즉, 상위와 하위 객체 모두가 동일한 추상화에 의존해야 한다는 것으로 그림으로 표현하면 아래와 같다.
상위 클래스인 햄버거가 하위 클래스인 소고개 패티에 의존하지 않고 중간에 패티라는 인터페이스 타입의 추상화에 상위클래스와 하위 클래스 모두 의존하는 방식을 DIP라고 한다.
추상화에 의존함으로써 서로의 구체적인 정보를 알지 못하게 함으로써 결합도를 느슨하게 할 수 있다.
그렇다면 왜 DI를 DIP를 통해 구현하는게 바람직하는 것일까
아래와 같은 에제를 통해 알아보겠다.
interface Patty {
fun price(): Int
}
class BeefPatty() : Patty {
override fun price(): Int {
return 2000
}
}
class PorkPatty() : Patty {
override fun price(): Int {
return 1800
}
}
class ChickenPatty() : Patty {
override fun price(): Int {
return 1500
}
}
class Hamburger(val patty: Patty) {
fun hamburgerPrice(): Int {
return patty.price() + 1000
}
}
위와 같이 햄버거와 패티의 의존관계를 설정하고 패티의 구현체들을 생성하였다.
@Configuration
class AppConfig {
@Bean
fun beefPatty(): BeefPatty {
return BeefPatty()
}
@Bean
fun chickenPatty(): ChickenPatty {
return ChickenPatty()
}
@Bean
fun porkPatty(): PorkPatty {
return PorkPatty()
}
@Bean
fun hamburger(): Hamburger {
return Hamburger(beefPatty())
}
}
그리고 AppConfig에 스프링 빈 설정을 하고 위와 같이 Hamburger에 beefPatty를 넣어주었다.
@SpringBootTest
class HamburgerTest(
@Autowired
val hamburger: Hamburger,
) {
@Test
fun test() {
print(hamburger.hamburgerPrice())
}
}
// 결과 : 3000
그리고 스프링부트 테스트를 통해 실행하니 3000원이 나왔다. beefPatty 2000원에 기본값 1000원이 더해져 3000원이 나왔다.
이번에 AppConfig에 beefPatty 대신 porkPatty 객체를 주입시켜보자.
@Bean
fun hamburger(): Hamburger {
return Hamburger(porkPatty())
}
이때 테스트를 다시 진행시켜 보면 결과는 2800원이 나온다. porkPatty 1800원에 기본값 1000원이 더해졌으니 결과값이 맞다.
우리는 아무런 자바 소스 코드를 변경하지 않고 config 설정만 변경하여 소고기 패티에서 돼지고기 패티로 변경시켰다.
DI를 할 때 DIP 원칙을 지킨다면 위와 같이 기존 소스 코드 변경없이 기능을 변경시키는 느슨한 결합을 가져갈 수 있다.
사실 위와 같이 AppConfig를 통해 의존관계를 설정할 수 있지만 Config 없이 각 클래스에 직접 @Component, @Controller, @Service 등과 같은 어노테이션을 붙여 빈으로 등록하여 사용하는 경우가 있다.
자 그렇다면
interface Patty { ... }
@Component
class BeefPatty() : Patty { ... }
@Component
class PorkPatty() : Patty { ... }
@Component
class ChickenPatty() : Patty { ... }
@Component
class Hamburger(@Autowired val patty: Patty) { ... }
위와 같이 선언되어있을 때, 어떤 patty가 Hamburger에 주입될까
이전의 테스트 코드를 그대로 돌려보았더니
Parameter 0 of constructor in com.yojic.springstudy.toby.chap1.di.Hamburger required a single bean, but 3 were found
와 같은 에러가 발생했다. 하나의 싱글 빈이 필요한데 3개가 발견됬다는 것이다.
위와 같이 DIP 원칙을 적용하여 @Autowired로 객체를 주입시켜주는 경우 동일 타입 빈이 복수개이면 스프링이 어떤 구현체를 주입할지 몰라 에러가 나타날 수 있다.
위와 관련된 설명을 아래 포스팅에서 진행하겠다.
https://developer111.tistory.com/126
'Framework & Lib & API > 스프링' 카테고리의 다른 글
[스프링 개념] 스프링 IOC 컨테이너, 싱글톤 레지스트리 (0) | 2024.02.04 |
---|---|
[스프링 개념] 동일 타입 빈, 빈 이름 중복, DI 구현 방법 (0) | 2024.01.02 |
스프링 시큐리티 개념 정리 (1) | 2023.12.06 |
스프링 개발 배포 운영 환경설정 파일 관리(spring.profiles.active) (0) | 2022.11.21 |
리액트 스프링부트 연동[2] (ec2 실서버에서 nginx로 리액트 배포 및 스프링 부트 연동) (0) | 2022.10.25 |