케네스로그

스프링에서 의존과 주입은 무엇이며 왜 써야할까? 본문

Dev/스프링

스프링에서 의존과 주입은 무엇이며 왜 써야할까?

kenasdev 2022. 4. 15. 23:52
반응형

의존이란?

DI(Dependency Injection)은 의존성 주입을 뜻하는 말로 스프링의 주요 특징이다. 여기서 의존(Dependency)는 객체간의 관계 속에서의 의존성을 뜻한다. 좀 더 실질적으로, 한 클래스가 다른 클래스의 기능을 필요로할 때, 이를 의존 dependent 한다고 표현한다.

 

 

회원을 관리하는 서비스가 있다. 이 서비스는 특정 저장공간을 사용해야 한다. 저장공간은 로컬 파일, 클라우드, 또는 데이터베이스가 될 수 있다. 그럼 이 서비스는 저장공간을 필요로 하고 있으며, 이를 저장공간에 의존하고 있다고 표현할 수 있는 것이다.

 

public class MemberService {
	private Repository repo = new Repository();
	
	public void register(String id, String name) {
		Member member = new Member(id, name);
		repo.insert(member);
	}
}

위와 같이 회원을 등록하는 단순한 예제 MemberService 클래스가 있다. 이 클래스 내부에서 Repository 클래스의 insert()메소드를 통해 새로운 member를 저장한다. 즉, MemberService라는 서비스 클래스는 Repository라는 저장공간을 상징하는 클래스를 필요로 한다. 이 때, MemberService가 Repository클래스에 의존한다고 말할 수 있는 것이다.

 

이러한 관계의 클래스에서 만약 메소드가 변경되거나 클래스가 변경된다면, 해당 의존관계에 있으며 해당 메소드를 사용하는 클래스 내부의 소스코드를 모두 수정해야한다. 즉, 한 클래스의 변화가 다른 클래스에 영향을 끼치기에 의존한다고 표현하는 것이다.

 

DI를 통한 의존 처리

이전의 예시에서 MemberService클래스를 객체로 구현하면, 내부에서 사용되는 Repository도 동시에 구현된다.

MemberService ms = new MemberService(); // ms 내부에 Repository클래스 객체도 생성됨

의존 주입(DI)방식은 위와 같이 직접 객체를 생성하는 것이 아니라, 파라미터를 통해 의존하는 객체를 전달하는 방식을 이용한다.

 

public class MemberService {
	private Repository repo;

	// 생성자를 통한 Memory 클래스 객체 전달
	public MemberService(Repository repo) {
		this.repo = repo;
	}
	
	public void register(String id, String name) {
		Member member = new Member(id, name);
		repo.insert(member);
	}
}

생성자를 통해 Repository 클래스의 객체를 인자로 받고, MemberService클래스 멤버변수에 할당한다. 이러한 방식을 통해, 객체를 생성할때 의존하는 객체를 받아서 생성하도록 할 수 있다.

 

이러한 주입 방식을 사용하는 이유? 의존 객체 변경의 유연성

앞서 언급한 예시코드보다 위의 코드가 더 길어졌다. 그럼에도 불구하고, 이러한 주입방식을 사용하는 이유가 무엇일까? 그것은 변경에 유용하다는 것이다.

public class MemberRegisterService {
	private Repository repo = new repo();
}

public class ChangePasswordService {
	private Repository repo = new repo();
}

이 예시처럼 Repository클래스를 사용하는 두 가지의 클래스에서 각자 Repository 객체를 생성해서 사용할 경우, 만약 Repository클래스가 아닌 다른 클래스로 변경된다면 두 클래스 내부의 소스코드를 모두 수정해야한다. 만약 Repository클래스에 의존중인 클래스가 100개라면 100개의 소스코드를 모두 수정해야한다.

 

 

Repository repository = new Repository();
MemberRegisterService mrs = new MemberRegisterService(repository);
ChangePasswordService cps = new ChangePasswordService(repository);

DI를 통해 주입받는 형태의 클래스라면, 위와 같은 방식으로 의존 객체를 받을 수 있다.

만약 Repository클래스가 아니라 Repository클래스를 상속받은 CloudRepository클래스를 이용해야한다면 다음과 같이 한 문장의 소스코드만 변경하면 된다.

 

//Repository repository = new Repository();
Repository repository = new CloudRepository();
MemberRegisterService mrs = new MemberRegisterService(repository);
ChangePasswordService cps = new ChangePasswordService(repository);

 

 

 

객체 조립기 Assembler

위의 예시대로 객체를 생성한다면 아래와 같이 main함수에서 모든 것을 관리할 수 있다.

 

public static void main(String[] args) {
	// Assembling process in main method
	MemberRepository repository = new MemberRepository();
	MemberRegisterService mrs = new MemberRegisterService(repository);
	ChangePasswordService cps = new ChangePasswordService(repository);
}

객체를 생성하고 의존 객체를 주입해주는 클래스를 따로 작성하는 것이 더 효율적이다. 이러한 의존 객체를 주입한다는 것은 서로 다른 두 객체를 조립하는 것과 유사하여 클래스 조립기(Assembler)라고 표현한다.

 

Assembler 클래스를 이용한 객체 생성

public class Assembler {
	private MemberRepository repository;
	private MemberRegisterService mrs;
	private ChangePasswordService cps;

	public Assembler() {
		// actual object manufacture process
		repository = MemberRepository();
		mrs = new MemberRegisterService(repository);
		cps = new ChangePasswordService();
		cps.setMemberRepository(repository);
	}

	/*
		getters and setters ...
	*/
}

위의 예시처럼 Assembler클래스를 만들고, 해당 클래스의 생성자에서 의존객체들을 생성하도록 작성한다. 이렇게하면, main메소드 내의 복잡도를 낮추고 객체 생성부를 명확히 할 수 있다.

 

public static void main(String[] args) {
	Assembler assembler = new Assembler();

	ChangePasswordService cps = assembler.getChangePasswordService();
	cps.changePassword("kenneth@gamil.com", "0000", "9999");
}

 

위의 main메소드에서 Assembler객체를 통해서 비즈니스 로직을 수행하는 객체를 가져와서 작업을 수행한다. 객체를 생성하는 조립기(Assembler)에게 책임과 역할을 분리하여 객체지향 프로그래밍의 단일 원칙을 따르도록 한다. 만약 객체 생성에서 문제가 생기면 개발자는 조립기만 확인하면 된다.

 

스프링의 DI 설정, 그리고 조립기 Assembler

이전까지는 순수 자바를 이용한 의존관계와 의존 객체를 주입했다. 이제는 스프링을 이용하면 얼마나 편리하게 객체를 관리할 수 있는지 알아보자.

스프링은 앞의 객체를 조립하고 전달하는 조립기(Assembler)와 같다. 필요한 객체를 생성하고, 생성한 객체에 의존을 주입한다. 이러한 역할을 맡고 있는 조립기를 스프링 컨테이너 또는 IoC 컨테이너라고 한다. 이 컨테이너는 말 그대로 무언가를 담는 상자(컨테이너)인데, 생성된 객체(빈)들을 담고 있으며, 이들이 생성되어 소멸될때까지의 모든 라이프사이클을 관리한다.

 

조립기와 설정클래스 ApplicationContext

@Configuration
public class AppCtx {
	@Bean
	public MemberRepository memberRepository() {
		return new MemberRepository();
	}

	@Bean
	public MemberRegisterService memberRegSvc() {
		return new MemberRegisterService();
	}
	
	// ...
}

이처럼 @Bean 애노테이션은 memberRepository, memberRegSvc메소드의 객체를 생성하고 스프링 빈으로 설정한다. 이렇게 @Configuration 애노테이션을 가진 클래스 AppCtx는 설정 클래스이다. 

 

public class Main {

	private static ApplicationContext ctx = null;

	public static void main(String[] args) {

		ctx = new AnnotationCOnfigAPplicationContext(AppCtx.class);

		// ...

		MemberRegisterService regSvc = ctx.getBean("memberRegSvc", MemberRegisterService.class);

		// ...
	}
}

main()메소드 내부에서 이 설정클래스를 통해서 컨테이너를 생성한다. 앞서 조립기(Assembler)를 main()내부에서 객체로 만들어서 사용했던것과 같다. 실제 스프링 컨테이너는 앞서 작성한 조립기 클래스보다 훨씬 복잡하게 구성되어있다.

 

의존주입 방식

DI방식 - 생성자

public class MemberRegisterService {
	private MemberRepository repository;
	public MemberRegisterService(MemberRepository repository) {
		this.repository = repository;
	}
}

위의 MemberRegisterService의 매개변수인 repository는 해당 클래스가 생성자를 통해 인스턴스화될 때 할당받는다. 이러한 방식은 생성자를 통해 의존객체를 주입받는다.

 

DI방식 - setter() 메소드

@Configuration
public class AppCtx {
	// ...
	@Bean
	public Printer printer() {
		Printer printer = new Printer();
		printer.setMemberRepository(memberRepository());
		return printer;
	}
}

public class Printer {
	private MemberRepository memberRepository;
	// ...
}

Printer클래스에 맴버변수로 MemberRepository객체를 지닌다. 즉, Printer클래스는 MemberRepository클래스 객체에 의존성을 지닌다. 이 객체는 설정 클래스 AppCtx 내부에서 Bean으로 등록될 때, setter()를 통해 의존객체를 주입받는다.

 

 

DI방식 - field 주입

public class MyComponent {
	@Autowired
	private Cart cart;
}

아직 @Autowired 애노테이션에 대해 배우지않았지만, 위의 방식을 필드주입(field injection)이라고 한다. 대부분의 경우에 이러한 방식을 피하라고 말한다. 그 이유는 지금 스터디의 범위를 벗어남으로 다음 포스팅에서 다루도록 하겠다.

@Component
public class MyComponent {
	private final Cart cart;
	
	@Autowired
	public MyComponent(Cart cart) {
		this.cart = cart;
	}
}

위와 같은 방식의 생성자 주입방식을 추천한다.

출처: https://stackoverflow.com/questions/39890849/what-exactly-is-field-injection-and-how-to-avoid-it

 

What exactly is Field Injection and how to avoid it?

I read in some posts about Spring MVC and Portlets that field injection is not recommended. As I understand it, field injection is when you inject a Bean with @Autowired like this: @Component public

stackoverflow.com

 

 

 

생성자 방식 vs setter() 방식

특징 생성자 기반 주입 setter() 기반 주입
순환의존(circular dependency) 순환의존 방지 순환의존 가능성 존재
순서(ordering) 주입될 때, 객체가 주입되는 순서에 따라 모두 주입된다. 오직 해당 의존 객체가 필요할때에 주입된다.
멀티쓰레드 final로 멤버변수를 선언하여 주입받는 경우, 안정성 보장 해당없음
Spring code generation Library Spring code generation library는 생성자 주입을 지원하지 않음으로 프록시 생성 불가 스프링 프레임워크 레벨에서는 setter방식을 사용함
실제사례 필수 의존 관계에 사용 선택적 의존 관계에 사용

*순환의존(circular dependency): 서로 다른 Bean이 서로에게 의존하는 경우. A->B, B->A

이 외에, 생성자 방식은 빈 객체를 생성하는 순간 모든 의존 객체가 주입된다. setter()방식은 해당 setter메소드의 이름을 통해 어떤 객체가 주입되는지 알 수 있다는 장점이 있다.

참조: https://www.tutorialspoint.com/difference-between-constructor-injection-and-setter-injection-in-spring

 

Difference Between Constructor Injection and Setter Injection in Spring

Difference Between Constructor Injection and Setter Injection in Spring Dependency Injection is a practice to pass dependent object to other objects. Spring has two types of Dependency Injection : Constructor based Injection -When container call the constr

www.tutorialspoint.com

 

 

Spring 포스팅 시리즈는 "초보 웹 개발자를 위한 스프링5 프로그래밍 입문(최범균 저)"을 독학하며 정리한 글입니다. 책 내용을 기반으로 정리하며, 개인적으로 궁금한점을 첨언하거나 소스코드를 더 이해하기 쉽게 작성합니다.

잘못된 내용이 있다면 언제든 피드백 부탁드립니다🙏

반응형