본문 바로가기
BE/Spring

Spring Framework 와 IoC, DI에 대해

by Day0404 2021. 11. 20.
728x90
반응형

오늘은 제가 쓰고 있는 Java의 스프링 프레임워크 (Spring Framework)에 대해 작성해보려 합니다.

예전에 공부했던 것을 다시 되새기며 헷갈리는 부분들은 다시 공부하며 작성하고 있습니다.

 

스프링 프레임워크(Spring Framework) 란?

스프링 프레임워크는 자바(Java) 플랫폼을 위한 오픈 소스 애플리케이션 프레임워크입니다.

보통은 대부분 "스프링"으로 통칭하여 부르지만 정확한 명칭은 "스프링 프레임워크"가 맞습니다.

스프링 프레임워크의 장점은 가볍다는 것입니다.

대부분 스프링을 설명할 때 경량화된, 가벼운 프레임워크라고 설명을 합니다.

여기서 가볍다는 의미는 EJB에서 POJO로 변경되면서 클래스가 구조적으로 간결해졌다는 의미이지 스프링 프레임워크 자체가 가벼운 프레임워크는 아닙니다. 단적으로 스프링으로 만든 서버와 Node.js로 만든 서버는 사용하는 메모리에서도 큰 차이를 보입니다. 그래서 스프링이 가볍다는 의미를 혼동하면 안 될 것 같습니다.

그리고 스프링의 어원은 전통적인 EJB라는 겨울을 넘어 새로운 시작이라는 뜻으로 스프링이라고 명칭을 짓게 되었다고 합니다.

 

스프링의 특징

1. POJO

POJO는 Plain Old Java Object의 약자로 위키백과에서는 아래와 같이 설명하고 있습니다.

Plain Old Java Object, 간단히 POJO는 말 그대로 해석을 하면 오래된 방식의 간단한 자바 오브젝트라는 말로서 
Java EE 등의 중량 프레임워크들을 사용하게 되면서 해당 프레임워크에 종속된 "무거운" 객체를 만들게 된 것에 반발해서 사용되게 된 용어이다.

간단히 말하면 어떤 상속이나 구현등이 없는 속성과 기능만 있는 객체를 뜻합니다.

public class Person {
	private String name;
	private int age;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
}

위 코드와 같이 다른 클래스나 인터페이스의 extends 나 implements를 받지 않고 getter, setter와 같이 기본적인 기능만을 가진 자바 객체를 말합니다.

그러나 이상과 현실은 다르듯 현대에선 워낙 기술들이 다양하고 복잡해져서 완벽한 POJO의 형태로 사용하지만은 않습니다.

 

자바를 이용해 서비스를 개발할 때 비지니스 로직뿐 아니라 트랜잭션 등의 로직까지 작성해야 하는 부담감을 없애고자 EJB를 사용하게 되었는데 이 EJB를 사용하게 되면서 트랜잭션등 로우 레벨의 로직 개발의 부담은 덜었지만 이런 기능을 사용하기 위해 거대한 EJB를 extends 하거나 implement 하게 되어 간단한 서비스가 무겁게 변하게 되었고 이 부분을 수정하기 위해서는 전체 코드를 수정해야 하는 문제점들이 있었습니다.

자바의 기본인 객체지향에 집중하고, 특정 클래스나 라이브러리에 종속되지 않는 POJO로 코드를 작성한다면 이런 문제점을 해결할 수 있을 것이라고 생각했다고 합니다.

그래서 스프링은 이러한 POJO 방식을 기반으로 한 프레임워크입니다.

POJO에 관해서는 따로 더 자세히 글을 작성할 예정입니다.

 

2. IoC 와 DI

IoC란 Inversion of Control의 약자로 말 그대로 풀면 "제어의 역전" 이라는 의미입니다.

작성한 메서드나 객체의 호출을 개발자가 결정하는 것이 아닌 외부, 즉 스프링 프레임워크에서 이루어지게 되는데 이것을 제어의 역전(IoC)라고 합니다.

이러한 객체의 호출을 스프링 프레임워크에서 결정하게 되면 객체의 생명주기(Lifecycle) 관리를 스프링 프레임워크에서 도맡아서 하기 때문에 개발자는 온전히 비즈니스 로직 작성에 집중할 수 있는 환경을 갖게 됩니다.

객체 호출에 대한 제어권이 프레임워크에 있기 때문에 DI(의존성 주입)이 가능하게 됩니다.

 

DI란 Dependency Injection의 약자로 "의존성 주입"이라는 의미입니다.

DI는 스프링 프레임워크가 제공하는 특별한 기능으로 객체를 직접 생성하여 사용하는 게 아닌, 스프링 프레임워크에게 주입받아 사용하는 기능입니다.

이러한 스프링의 의존성 주입은 세 가지 방법이 있습니다.

 

1. 필드 주입 (Filed Injection)

@RestController
public class DiTestController {
	@Autowired
	private DiService diService;
}

필드 주입의 경우 의존성 주입이 매우 쉽습니다. 필드에 @Autowired을 선언하기만 하면 아주 쉽게 의존성 주입이 가능합니다. 필드 주입의 경우 코드도 간결하여 보기도 쉽고 사용하기도 쉬워 과거에 많이 사용하던 방법입니다.

하지만 "과거에" 사용했다면 당연히 최근에는 지양하는 방법입니다.

왜냐하면 DI Container와 강한 결합으로 연결되어 있으며 외부에서 사용할 수 없습니다. 또한 생성자 주입처럼 final 객체를 만들 수 없습니다. 

필드 주입의 경우 의존성 주입을 아주 쉽게 무제한으로 주입이 가능합니다. 너무 많은 종속성을 이렇게 쉽게 설정이 가능하면 하나의 클래스가 갖는 책임이 너무 많아질 수 있기 때문에 단일 책임 원칙을 위반할 수 있어 지양해야 합니다.

IntelliJ에서도 @Autowired 어노테이션을 사용하면 추천하지 않는다는 경고 메시지를 확인할 수 있습니다.

 

2. 수정자 주입 (Setter Injection)

수정자 주입setter 메서드를 통해 의존성을 주입하는 방법입니다.

@RestController
public class TestController {
	private TestService testService;
	
	@Autowired
	public void setTestService(TestService testService) {
		this.testService = testService;
	}
}

setter 메서드에 @Autowired 어노테이션을 붙여 사용합니다.

 

3. 생성자 주입 (Constructor Injection)

생성자 주입(Constructor Injection)은 위 수정자 주입에서 setter 메서드가 아닌 생성자를 통한 의존성을 주입하는 방법입니다.

@RestController
public class TestController {
	private final TestService testService;

	public TestController(TestService testService) {
		this.testService = testService;
	}
}

Lombok을 통해 아래와 같이 간결하게 작성도 가능합니다.

@RestController
@RequiredArgsConstructor
public class TestController {
	private final TestService testService;
}

스프링 프레임워크에서도 이 생성자 주입을 권장하고 있습니다.

스프링에서 이 생성자 주입을 왜 권하는 걸까요?

생성자 주입의 경우 객체를 생성할 때 딱 1번만 호출이 되고 이후에 호출이 되는 일이 없기 때문에 불변하게(final) 설계가 가능합니다.

그리고 이건 저의 경험입니다만, 생성자 주입의 경우 순환 참조 오류를 방지할 수 있습니다.

제가 스프링을 처음 사용할 때 생성자 주입을 사용하지 않고 @Autowired를 통한 필드 주입을 남발하며 손이 가는 대로 코딩을 하던때가 있었습니다. 저는 완벽하게 공부를 하고 시작을 하기보단 부딪혀보고 배우는 스타일이라 나름 사이드 프로젝트를 스프링으로 해보자라고 생각하면서 스프링으로 API 서버를 만들었습니다.

그렇게 손이 가는대로 코딩을 하다 이것저것 테스트를 해봤는데 처음 보는 오류를 발견했습니다.(지금 생각하면 상당히 무지했습니다...)

순환 참조 오류라는 것을 발견하고 이 순환 참조에 대해 공부하기 시작했었는데 제가 아주 바보같이 코딩을 하고 있다는 것을 깨달았습니다.ㅎㅎ

 

 

순환 참조는 A라는 빈(Bean)이 B라는 빈을 참조하고 B는 다시 A를 참조하는 서로가 서로를 계속해서 순환하여 참조하는 것을 말합니다.

@Component
public class A {
	private B b;

	public A(B b) {
		this.b = b;
	}
}
@Component
public class B {
	private A a;

	public B(A a) {
		this.a = a;
	}
}

위 예제처럼 각각 A, B 클래스를 만들고 실행시키면 아래와 같은 순환 참조 오류가 발생합니다.

a와 b 가 서로를 계속 순환 참조하기 때문에 오류가 발생한 겁니다.

 

이 순환 참조 오류와 생성자 주입이 무슨 관계가 있냐 하면,

필드 주입수정자 주입은 A 클래스의 객체가 생성되었을 시 그 당시엔 b를 생성할 필요가 없습니다. 그렇기 때문에 컴파일 상에는 문제가 없습니다. 그러나 잘 실행되다가 A클래스나 B클래스를 로드할 때 오류가 발생합니다. 컴파일 시 오류가 생기지 않다가 런타임 시에 오류가 발생하기 때문에 아주 많이 크리티컬 합니다.

이에 비해 생성자 주입의 경우 생성자에서 의존관계 주입이 일어나기 때문에 A의 객체가 생성될 때 B의 객체를 생성하여 담습니다. 바로 이 부분이 순환 참조 오류를 방지해주는 포인트입니다.

이렇게 컴파일 시 오류를 알아차릴 수 있다면 좀 더 좋은 애플리케이션을 만들 수 있습니다.

순환 참조 오류가 발생했을 때는 여러 가지 돌려치기 방법이 있습니다만 가장 좋은 방법은 순환 참조가 발생한 로직을 다시 재설계하는 것입니다.(저도 이것 때문에 결국엔 다시 설계했었습니다.ㅎㅎ)

순환 참조에 대해서는 나중에 좀 더 자세하게 글을 써야겠습니다.

 

스프링 프레임워크에 대해 글을 작성하다 순환 참조까지 나오게 되었네요.

스프링에서 IoC와 DI는 아주 중요한 개념입니다.

물론 저도 많이 까먹고 있다가 다시 복습하면서 이 글을 작성했습니다.ㅎㅎ

스프링 프레임워크의 특징은 IoC와 DI만 있는 것이 아니기에 다음 글에는 좀 더 다양한 스프링 프레임워크에 대해 작성하도록 하겠습니다.

혹시나 알고 계신 내용과 다른 부분이 있다면 댓글로 남겨주세요!

반응형

댓글