Spring (2) - IoC에 대하여

Prolog

DI와 IoC를 정리하려다가 DI에 대해 정리한 양이 많아졌다. 이를 사람들이 보기엔 스크롤 압박이 있을 수도 있고, 필자도 나중에 보기에 한 번에 많은 양을 쭉 훑어보기란 쉽지 않다고 생각되어 DI와 IoC를 따로 정리하기로 했다.
이렇게 길어도 되나요?! 싶지만, 필자는 중요한데 모르는 개념을 이해하고 싶을 때 많은 레퍼런스들을 펼치고 최소 세 번씩은 정독하는 편이다. 이 포스팅도 여러 번 읽으면 그 개념에 대해 흡수할 수 있지 않을까 라는 생각으로 한 포스팅이다.


IoC(Inversion of Control) - 제어의 역전

바야흐로 자바가 등장하고...

자바 기반으로 애플리케이션을 개발하기 시작하던 최초의 시기에는 자바 객체를 생성하고, 객체 간의 의존 관계를 연결시키는 등의 제어권을 개발자가 직접 가지고 있었다.
또한, 과거에는 많은 형태의 오픈 소스들이 나오고 있었고, 이들의 공통적인 이슈는 서로 다른 객체를 어떻게 연결할 것인지에 대한 문제였다.
그때 Servlet, EJB가 등장하면서 개발자들이 가지고 있던 제어권과 객체의 생명주기를 관리하는 권한이 Servlet과 EJB를 관리하는 컨테이너에게 넘어갔다.
이처럼 객체의 생성에서부터 생명주기의 관리까지, 모든 객체에 대한 제어권이 바뀐 것을 의미하는 것이 제어권의 역전, 즉 Ioc라는 개념이다.


스프링 입장에서의 IoC 정의🍃

객체의 생명주기(생성-설정-초기화-소멸)부터 생명주기의 관리를 개발자가 아닌 스프링 프레임워크로 모든 객체에 대한 제어권이 바뀐, 즉 주체가 되어 담당하는 것을 말합니다.

즉, 제어의 역전(IoC)메서드나 객체의 호출을 개발자가 결정하는 것이 아닌 외부에서 결정되는 것을 의미하며, 줄여서 제어의 흐름을 바꾼다는 뜻으로도 볼 수 있겠습니다.

너희들이 만들 거 내가 다 만들어 줄테니 이거 가져다가 써


IoC를 쓰면 어떤 이점이 있을까요?

객체지향 프로그래밍이라는 단어까지 거슬러 올라가보겠습니다.

객체지향 프로그래밍은 각 객체마다 자기의 역할과 책임을 온전히 다하며 서로 협력하며 변경에 유연한 프로그래밍을 할 수 있는 프로그래밍 기법입니다. 즉, 각 객체마다 올바른 캡슐화를 통해 높은 응집도와 낮은 결합도를 이루어나가는 것이 중요합니다.

이러한 관점에서 제어의 역전으로 인해 제 3자, 즉 다른 객체/다른 컨테이너에게 제어에 대한 역할과 책임을 위임하게 됩니다. 그 후는 신경을 안 써도 되는 것이죠.

이렇게 하면 변경에 유연한 코드 구조를 가져갈 수 있게 되고, 기업 차원에서 수많은 객체들을 편리하게 관리할 수 있게 됩니다.
만약, 제가 작성하고자 하는 코드에서 객체를 생성, 소멸 등 관리 코드와 함께 비즈니스 코드까지 들어가면?오히려 좋아

기능(코드)은 얼마든지 변경될 수 있습니다.
예시를 들어보겠습니다.

A라는 객체를 생성하고 있었는데, A객체는 수정하고 B객체를 추가해야 한다고 하면 어떻게 될까요?
그렇게만 하고 끝나는 비즈니스면 상관이 없겠지만, 1000곳에서 다시 A라는 객체를 사용한다라고 하면 꽤나 고역일 것입니다.

즉, 객체를 관리해주는 독립적인 존재(거시적으로는 컨테이너, 미시적으로는 디자인 패턴 적용)와 그 외 본인이 구현하고자 하는 부분으로 각각 관심을 분리하고, 각자의 역할을 충실히 하면서 변경에 유연한 코드를 작성할 수 있는 구조이기 때문에 제어를 역전했다 라고도 볼 수 있겠습니다.

이를 정리하자면, IoC를 적용시켰을 때
객체의 의존성을 역전시켜 객체 간의 결합도를 줄이고(느슨한 결합) 유연한 코드를 작성할 수 있게 하여 가독성 및 코드 중복, 유지 보수를 편하게 할 수 있게 됩니다.


IoC구현 방법

IoC를 구현하는 방법에는 DL과 DI가 있습니다.

  • DL이란?

    의존 관계 검색(Dependency Lookup)은 의존 관계가 있는 객체를 외부에서 주입받는 것이 아닌, 의존 관계가 필요한 객체에서 직접 검색하는 방식을 말합니다.

  • DI란?

    DI는 스프링에서 지원하는 IoC의 한 형태로써 각 계층 사이, 각 Class 사이에 필요로 하는 의존 관계가 있다면 이를 컨테이너가 자동적으로 연결시켜 주는 것으로, 각 Class 사이의 의존 관계를 Bean 설정 정보를 바탕으로 컨테이너가 자동적으로 연결해주는 개념이라고 보시면 됩니다.
    DI에 대한 자세한 내용은 Spring (1) - DI에 대하여를 통해서 정리했습니다.


IoC와 DI의 차이점

IoC는 객체의 흐름, 생명주기 관리 등 독립적인 제 3자에게 역할과 책임을 위임하는 방식의 디자인 패턴입니다.
다른 컨테이너를 가진 프레임워크들에서도 찾아볼 수 있어 IoC라는 개념은 스프링에만 국한되는 패턴은 아닙니다.
그러나 DI는 인터페이스를 통해 다이나믹하게 객체를 주입하여 유연한 프로그래밍을 가능하게 하는 패턴으로, IoC보다 의존 관계 주입에 초점을 좀 더 맞췄다고 볼 수 있겠습니다.


IoC 예시

일반적인 의존 관계

  • Barista
class Barista {
	private ArabicaBean arabicaBean;
    
    public Barista() {
    	this.arabicaBean = new ArabicaBean();
    }
    
    public void roasting() {
    	System.out.println("나는! 나는! 나는! 원두를 볶는다!");
 		this.arabicaBean.roastBeans();
    }
}
  • ArabicaBean, RobustaBean
class ArabicaBean {
	public void roastBeans() {
    	System.out.println("아라비카 원두를 볶는다!");
    }
}

class RobustaBean {
	public void roastBeans() {
    	System.out.println("로부스타 원두를 볶는다!");
    }
}
  • Cafe
public class Cafe {
	public static void main(String[] args) {
		Barista barista = new Barista();
		barista.roasting();
	}
}

상단의 코드에서 Barista가 roasting()을 실행하기 위해서는 ArabicaBean이 필요하고, Barista 자신이 직접 ArabicaBean을 만들어서 사용하고 있습니다. 이 상태를 Barista가 ArabicaBean에 의존하고 있다고 표현할 수 있습니다.

만약 바리스타가 다른 품종의 콩을 사용해야 할 경우, Barista의 많은 부분을 수정해야 할 것입니다.

그렇다면 이 의존 관계를 역전시켜봅시다!

  • CoffeeBean
interface CoffeeBean {
	void roastBeans();
}

class ArabicaBean implements CoffeeBean {
	public void roastBeans() {
    	System.out.println("아라비카 원두를 볶는다!");
    }
}

class RobustaBean implements CoffeeBean {
	public void roastBeans() {
    	System.out.println("로부스타 원두를 볶는다!");
    }
}
  • Barista
class Barista {
	private CoffeeBean coffeeBean;
    
    public void setCoffeeBean(CoffeeBean coffeeBean) {
		this.coffeeBean = coffeeBean;
	}
    
    public void roasting() {
    	System.out.println("나는! 나는! 나는! 원두를 볶는다!");
        this.coffeeBean.roastBeans();
    }
}
  • Cafe
public class Cafe {
	public static void main(String[] args) {
    	Barista barista = new Barista();
        
        // ArabicaBean
        CoffeeBean arabicaBean = new ArabicaBean();
        barista.setCoffeeBean(arabicaBean);
        barista.roasting();
        
        // RobustaBean
        CoffeeBean robustaBean = new RobustaBean();
        barista.setCoffeeBean(robustaBean);
        barista.roasting();
    }
}

이번에는 CoffeeBean을 인터페이스로 만들었고, CoffeeBean을 implements(구현)하는 각각의 원두 (종류) 클래스를 만들었습니다. 바리스타는 원두를 자신이 만들어서 사용하는 것이 아닌, 외부에서 만들어진 원두를 받아서 사용하고 있습니다. 또한, 타입을 인터페이스로 바꿨기에 어떤 원두든 코드 변경없이 사용할 수 있습니다.

바리스타가 원두에 의존하고 있던 관계가 바뀐(뒤집어진) 것입니다. 이러한 현상을 DIP(Dependency Inversion Principle, 의존 관계 역전 원칙)라고 합니다.

다만, 아직도 코드를 실행하는 부분에서 CoffeeBean의 종류를 선택하여 직접 생성하고 Barista에게 세팅(set)해주는 작업을 해야합니다.

그러면 스프링에서 IoC/DI 개념을 적용하게 되면 어떻게 될까요?

여기서부터는 스프링 프로그램입니다.

  • CoffeeBean
interface CoffeeBean {
	String roastBeans();
}

@Component("arabicaBean") // arabicaBean이란 이름을 가진 Bean(얘는 원두가 아닌 객체입니다)으로 등록
public class ArabicaBean implements CoffeeBean {
	public String roastBeans() {
    	return "아라비카 원두를 볶는다!";
    }
}

@Component("robustaBean") // robustaBean이란 이름을 가진 Bean으로 등록
public class RobustaBean implements CoffeeBean {
	public String roastBeans() {
    	return "로부스타 원두를 볶는다!";
	}
}
  • Barista
@Component	// 의존성을 주입받는 객체도 Bean으로 등록되어있어야 합니다.
public class Barista {
	
    @Autowired	// 의존성 주입
    @Qualifier("arabicaBean")	// 사용할 의존 객체를 선택할 수 있도록 해줍니다
	private CoffeeBean coffeeBean;
    
    public String roasting() {
    	return "나는! 나는! 나는! 원두를 볶는다! " + this.coffeeBean.roastBeans();
    }
  • RoastController
@RestController
public class RoastController {
	
	@Autowired	// Barista라는 타입을 가진 Bean을 찾아서 주입시킴
    private Barista barista;
    
    @RequestMapping("/roast")
    public String roastDriver() {
    	return barista.roasting();
    }
}

아까 전에는 main()에서 바리스타가 원두를 set받아 사용하였습니다. 그러나 Spring 에서의 Ioc/DI 예제에서는 어떤 곳에서도 CoffeeBean 객체를 생성하지 않았습니다.

해당 코드에서는 육안으로 식별하기 힘들 수 있겠지만 @Component라는 Annotation이 붙은 변수의 타입(타입이 같은 Bean이 여러 개 있다면 이름을 봅니다)을 보고 해당 변수에다가 객체를 주입하게 됩니다.

요약

스프링의 Container가 대신 객체를 생성해주고 알아서 객체를 주입해줍니다. 이렇게 생성된 객체는 자신이 어디에 쓰일지는 모릅니다. 이것이 제어의 역전 원칙이며, 스프링은 스프링에 맞게 DI(의존성 주입)라는 개념으로 구현하는 것이 되겠습니다.


Epilogue

DI와 함께 Ioc의 정리를 열흘이라는 기간에 걸쳐 정리를 했다.
네이게이션으로 최단 루트를 갈 수 있음에도 불구하고 굳이 돌아서 가려는 느낌이 마구 들었지만, 그래도 둘의 목적지는 같고 이 느린 방향으로도 배울 수 있는 점은 있기에 후회는 남지 않을듯 싶다.
작년에 아무것도 모르고 프로젝트를 했던 때가 후회될 뿐이다. 그때는 스프링의 기본 개념도 숙지하지 않고 프로젝트에 뛰어들어 삽질이란 삽질은 다 했어도 뭔가 만족스런 결과물은 나오지 않았었다.
그러나 지금은 이렇게 스프링하면 알아야 할 기본 개념들을 여러 레퍼런스를 통해 학습하고나니, 예전에 했던 프로젝트에서 나왔던 Bean 관련 오류가 왜 떴는지(NullPointerException 등) 조금은 알게 됐다.
앞으로의 프로젝트를 수행할 때 이를 적용하여, 더 확장성있고 변경에 유용한 코드를 작성할 수 있을 거라는 자신감이 생겼다.

References

만약 혼동되거나 틀린 내용이 있다면 지적 해주시면 감사드리겠습니다 🙇‍♂️

좋은 웹페이지 즐겨찾기