챕터 3: 스프링 DI

binary_j·2023년 9월 28일
0

의존이란?


한 클래스가 다른 클래스의 메서드를 실행할 떄 이를 '의존'한다고 표현
의존 객체를 구하는 여러 방법이 존재하는데, 스프링과 관련된 것이 DI(의존 주입)이다.

DI를 통한 의존 처리

DI는 의존하는 객체를 직접 생성하는 대신 의존 객체를 전달받는 방식을 사용한다.

MemberRegisterService라는 클래스에서 MemberDao 클래스의 메서드를 사용한다고 하자. MemberDao 객체를 어떻게 받아올 수 있을까?

직접 객체를 생성하는 방법

private MemberDao memberDao = new MemberDao();

생성자를 통해 의존 객체를 전달받는 방법

public MemberRegisterService(MemberDao memberDao) {
	this.memberDao = memberDao;
}

이제 MemberRegisterService 객체를 생성하려면 생성자에 MemberDao 객체를 전달해야만 한다.

MemberDao dao = new MemberDao();
MemberRegisterService svc = new MemberRegisterService(dao);

의존 객체를 생성자를 통해 주입한다.

DI와 의존 객체 변경의 유연함

의존 객체를 직접 생성하면 의존 객체에 변경이 있을 때마다 관련 있는 모든 소스를 수정해야 하지만, DI를 적용하면 다음과 같이 한 군데만 수정하는 것으로 해결된다. 의존 객체를 직접 생성하는 방식에 비해 변경할 코드가 한 곳으로 집중되기 때문에 유지 보수에 편리하다.

변경 전

MemberDao memberDao = new MemberDao();
MemberRegisterService RegSvc = new MemberRegisterService(memberDao);
ChangePasswordService pwdSvc = new ChangePasswordService(memberDao);

MemberDao 클래스에 캐시를 적용하기 위해 MemberDao 클래스를 상속받는 CachedMemberDao 클래스를 생성하고 이를 적용하고자 한다고 하자.

변경 후

MemberDao memberDao = new CachedMemberDao();
MemberRegisterService RegSvc = new MemberRegisterService(memberDao);
ChangePasswordService pwdSvc = new ChangePasswordService(memberDao);

객체 조립기

책에 있는 예제 코드를 굳이 여기에 적고 설명하진 않을 예정이다.
앞서 교재에서 간단한 회원가입 프로젝트를 작성하였다.

객체들을 생성하고 의존 객체를 주입해주는 클래스를 별도로 작성할 수 있는데 서로 다른 객체들을 조립해준다는 의미에서 객체 조립기(assembler)라고 표현한다.

객체 조립기 Assembler 클래스를 사용하는 코드는 Assembler 객체를 생성한 후에 get 메서드를 사용하여 필요한 객체를 구하고 그 객체를 사용한다.

스프링의 DI 설정

스프링은 앞서 설명한 객체 조립기와 비슷한 기능을 제공한다. 스프링은 필요한 객체를 생성하고 생성한 객체에 의존을 주입하고 조립기의 getter처럼 객체를 제공하는 기능을 정리하고 있다.

조립기 대신 스프링을 사용하려면 스프링이 어떤 객체를 생성하고, 의존을 어떻게 주입할 지 정의하고 있는 설정 정보를 작성해야 한다. 설정 코드는 다음과 같다.

package config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import spring.ChangePasswordService;
import spring.MemberDao;
import spring.MemberRegisterService;

@Configuration
public class AppCtx {
	
	@Bean
	public MemberDao memberDao() {
		return new MemberDao();
	}
	
	@Bean
	public MemberRegisterService memberRegSvc() {
		return new MemberRegisterService(memberDao());
	}
	
	@Bean
	public ChangePasswordService changePwdSvc() {
		ChangePasswordService pwdSvc = new ChangePasswordService();
		pwdSvc.setMemberDao(memberDao());
		return pwdSvc;
	}

}

앞서 설명했듯 @Configuration 애노테이션은 스프링 설정 클래스를 의미한다.

소스를 보면 알 수 있듯이 MemberRegisterService는 생성자를 통해 의존을 주입하고 있으며, ChangePasswordService는 세터를 통해 의존을 주입하고 있다.

실제로 객체를 생성하고 의존 객체를 주입하는 것은 스프링 컨테이너이므로 설정 클래스를 다 만들었으면 이를 통해 컨테이너를 생성해야 한다.

ApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);

컨테이너를 생성했으면 getBean()을 통해 객체를 구할 수 있다.

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

DI 주입 방식: 생성자 vs 세터 메서드

책에서는 둘 중 하나를 더 낫다고 할 수 없고 적절하게 섞어 사용하면 된다고 서술하고 있다. 실제로 그런지 구글링을 해보았다.

스프링 공식 문서에 의하면 가능하면 생성자 주입을 사용하는 것이 바람직하다고 한다. 생성자 주입 방식을 사용하면 순환 참조를 방지할 수 있고, 불변성이 보장되게 할 수 있으며(final을 붙임), nullPointerException이 방지되고, 테스트가 편리하다는 이점이 있다. 이에 대해서는 다음 글에서 더 자세히 다루도록 하겠다.

@Configuration 설정 클래스의 @Bean 설정과 싱글톤

스프링 컨테이너는 @Bean이 붙은 메서드에 대해 한 개의 객체만을 생성한다고 앞서 말했다. 설정 메서드에서 memberDao()를 몇 번을 호출하더라도 항상 같은 객체를 리턴한다는 말이다. 스프링은 설정 클래스를 그대로 사용하지 않고, 설정 클래스를 상속한 새로운 설정 클래스를 만들어서 사용한다.

스프링이 런타임에 생성한 설정 클래스는 해당 빈이 이미 존재하는지 확인하고 존재하지 않을 경우에만 새로운 빈 객체를 생성한다. 이후 동일한 객체를 다시 호출하면 미리 생성해둔 빈 객체를 보관해뒀다가 리턴한다.

두 개 이상의 설정 파일 사용하기

스프링은 한 개 이상의 설정 파일을 사용해서 컨테이너를 생성하도록 허용한다. 설정하는 빈의 개수가 증가하면 영역별로 설정 파일을 나누어 관리하는 것이 더 편리할 수 있다. 이 때 @Autowired라는 애노테이션을 사용할 수 있다.

AppConf1.java에서 다음과 같이 MemberDao, MemberPrinter 타입의 빈을 설정해놓고 이를 AppConf2.java에서 @Autowired 애노테이션을 사용하여 해당 타입의 빈을 찾아서 필드에 할당해줄 수 있다.

AppConf1.java

package config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import spring.MemberDao;
import spring.MemberPrinter;

@Configuration
public class AppConf1 {

	@Bean
	public MemberDao memberDao() {
		return new MemberDao();
	}
	
	@Bean
	public MemberPrinter memberPrinter() {
		return new MemberPrinter();
	}
	
}

AppConf2.java

package config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import spring.ChangePasswordService;
import spring.MemberDao;
import spring.MemberInfoPrinter;
import spring.MemberListPrinter;
import spring.MemberPrinter;
import spring.MemberRegisterService;
import spring.VersionPrinter;

@Configuration
public class AppConf2 {
	@Autowired
	private MemberDao memberDao;
	@Autowired
	private MemberPrinter memberPrinter;
	
	@Bean
	public MemberRegisterService memberRegSvc() {
		return new MemberRegisterService(memberDao);
	}
	
	@Bean
	public ChangePasswordService changePwdSvc() {
		ChangePasswordService pwdSvc = new ChangePasswordService();
		pwdSvc.setMemberDao(memberDao);
		return pwdSvc;
	}
	
	@Bean
	public MemberListPrinter listPrinter() {
		return new MemberListPrinter(memberDao, memberPrinter);
	}
	
	@Bean
	public MemberInfoPrinter infoPrinter() {
		MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
		infoPrinter.setMemberDao(memberDao);
		infoPrinter.setPrinter(memberPrinter);
		return infoPrinter;
	}
	
	@Bean
	public VersionPrinter versionPrinter() {
		VersionPrinter versionPrinter = new VersionPrinter();
		versionPrinter.setMajorVersion(5);
		versionPrinter.setMinorVersion(0);
		return versionPrinter;
	}
}

스프링 컨테이너를 생성할 때에는 그냥 설정 파일 두개를 모두 전달해 주면 된다.

ctx = new AnnotationConfigApplicationContext(AppConf1.class, AppConf2.class);

두 개 이상의 설정 파일을 사용하는 또 다른 방법이 있다. @Import 애노테이션을 사용하는 것이다.

package config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import spring.MemberDao;
import spring.MemberPrinter;

@Configuration
@Import({AppConf2.class})
public class AppConfImport {

	@Bean
	public MemberDao memberDao() {
		return new MemberDao();
	}
	
	@Bean
	public MemberPrinter memberPrinter() {
		return new MemberPrinter();
	}
}

이렇게 작성하면 @Import 애노테이션으로 지정한 AppConf2 클래스도 함께 사용되기 때문에 컨테이너를 생성할 때 굳이 지정해 줄 필요가 없다.
배열 형태로 여러개의 설정 파일을 import 할 수도 있다.

@Import({AppConf1.class, AppConf2.class});

getBean 메서드 사용

앞서 사용한 getBean 메서드는 첫 번째 인자가 빈의 이름, 두 번째 인자가 빈의 타입이기 때문에 getBean을 호출할 때 존재하지 않는 빈의 이름을 사용하거나 잘못된 타입을 전달하면 익셉션이 발생한다.

빈 이름을 지정하지 않고 타입만으로 빈을 구할수도 있지만, 같은 타입의 빈이 여러개 존재할 경우 마찬가지로 익셉션이 발생한다.

주입 대상 객체의 설정

의존 주입을 할 때 주입 대상 객체가 항상 빈 객체일 필요는 없다. 빈으로 등록되지 않은 일반 객체도 생성해서 주입할 수 있다.

객체를 스프링 빈으로 등록하지 않은 경우 스프링 컨테이너는 해당 객체를 관리하지 않는다. getBean() 메서드로 해당 객체를 구할 수 없으며 이 외의 스프링의 여러 객체 관리 기능도 사용할 수 없다.

0개의 댓글