3. 스프링 DI(Dependency Injection) #3 스프링 DI 설정

2021. 7. 2. 14:58JAVA/Spring

본 글은 최범균 저자의 초보 웹 개발자를 위한 스프링5 프로그래밍 입문 도서의 내용을 복습하기 위해 작성된 글입니다.

6. 스프링의 DI 설정

실제로 스프링은 앞서 구현한 조립기와 유사한 기능을 제공한다. 즉 스프링은 Assembler 클래스의 생성자 코드처럼 필요한 객체를 생성하고 생성한 객체에 의존을 주입한다. 또한 스프링은 Assembler#getMemberRegisterService() 메서드처럼 객체를 제공하는 기능을 정의하고 있다. 차이점이라면 Assembler는 MemberRegisterService나 MemberDao와 같이 특정 타입의 클래스만 생성한 반면 스프링은 범용 조립기라는 점이다.

 

6.1 스프링을 이용한 객체 조립과 사용

스프링을 사용하려면 먼저 어떤 객체를 생성하고, 의존을 어떻게 주입할지를 정의한 설정 정보를 작성해야 한다. 이 설정 정보는 자바 코드를 이용해서 작성할 수 있다.

AppCtx.java

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 : 스프링 설정 클래스를 의미함. 이 애노테이션(@Configuration)을 붙여야 스프링 설정 클래스로 사용할 수 있다.
  • @Bean : 해당 메서드가 생성한 객체를 스프링 빈이라고 설정한다. 예를 들어 meberDao() 메서드를 이용해서 생성한 빈 객체는 "memberDao"라는 이름으로 스프링에 등록한다.
  • new MemberRegisterService(memberDao()) : memberDao() 메서드가 생성한 객체를 MemberRegisterService 생성자를 통해 주입한다. (생성자 주입 방식)
  • pwdSvc.setMemberDao(memberDao()) : ChangePasswordService 객체가 setter 메서드를 통해서 MemberDao 객체를 주입받는다. ( setter 주입 방식)

AnnotationConfigApplicationContext 클래스를 이용한 스프링 컨테이너 생성

ApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);

 

스프링 컨테이너의 getBean() 메서드의 사용

 

// 컨테이너에서 이름이 memberRegSvc인 빈 객체를 구한다.
MemberRegisterService regSvc = ctx.getBean("memberRegSvc", MemberRegisterService.class);

위 코드는 스프링 컨테이너(ctx)로부터 이름이 "memberRegSvc"인 빈 객체를 구한다. 앞서 자바 설정을 보면 위 코드처럼 이름이 "memberRegSvc"인 @Bean 메서드를 설정했다. 이 메서드는 MemberRegisterService 객체에 생성자를 통해 memberDao를 주입한다. 따라서 위 코드에서 구한 MemberRegisterService 객체는 내부에서 memberDao 빈 객체를 사용한다.

 

@Bean
public MemberDao memberDao() 
{
	return new MemberDao();
}
	
@Bean
public MemberRegisterService memberRegSvc()
{
	return new MemberRegisterService(memberDao());
}

 

MainForSpring 클래스에 스프링 컨테이너를 변경 및 적용한 소스코드

https://github.com/yonghwankim-dev/spring5/blob/master/sp5-chap03/src/main/java/spring/MainForSpring.java

 

yonghwankim-dev/spring5

최범균 저, 초보 웹 개발자를 위한 스프링5 프로그래밍 입문 예제. Contribute to yonghwankim-dev/spring5 development by creating an account on GitHub.

github.com

 

6.2 DI 방식 1 : 생성자 방식

MemberRegisterService 클래스의 일부에서 보면 생성자를 통해 의존 객체를 주입받아 필드(this.memberDao)에 할당하였다.

package spring;

import java.time.LocalDateTime;

public class MemberRegisterService {
	private MemberDao memberDao;

	public MemberRegisterService(MemberDao memberDao) {
		this.memberDao = memberDao;
	}
	
	public Long regist(RegisterRequest req)
	{
		Member member = memberDao.selectByEmail(req.getEmail());
		if(member!=null)
		{
			throw new DuplicateMemberException("dup email " + req.getEmail());
		}
		Member newMember = new Member(
										req.getEmail(),
										req.getPassword(),
										req.getName(),
										LocalDateTime.now()
									);
		memberDao.insert(newMember);
		return newMember.getId();
	}
	
}

 

스프링 자바 설정에서는 생성자를 이용해서 의존 객체를 주입하기 위해 해당 설정을 담은 메서드를 호출했다.

@Bean
public MemberDao memberDao() 
{
	return new MemberDao();
}
	
@Bean
public MemberRegisterService memberRegSvc()
{
	return new MemberRegisterService(memberDao());
}

생성자에 전달할 의존 객체가 두 개 이상이어도 동일한 방식으로 주입하면 된다.

생성자 파라미터가 두개인 예제를 살펴보기 전에 예제를 실행하는 필요한 코드를 추가한다. 추가할 코드는 MemberDao 클래스의 selectAll() 메서드이다.

 

MemberDao 클래스에 selectAll() 메서드 추가

package spring;

import java.util.Map;
import java.util.Collection;
import java.util.HashMap;

public class MemberDao {

	... 생략
	public Collection<Member> selectAll()
	{
		return map.values();
	}
}

 

MemberPrinter 클래스 구현

https://github.com/yonghwankim-dev/spring5/blob/master/sp5-chap03/src/main/java/spring/MemberPrinter.java

 

yonghwankim-dev/spring5

최범균 저, 초보 웹 개발자를 위한 스프링5 프로그래밍 입문 예제. Contribute to yonghwankim-dev/spring5 development by creating an account on GitHub.

github.com

MemberListPrinter 클래스 구현

https://github.com/yonghwankim-dev/spring5/blob/master/sp5-chap03/src/main/java/spring/MemberListPrinter.java

 

yonghwankim-dev/spring5

최범균 저, 초보 웹 개발자를 위한 스프링5 프로그래밍 입문 예제. Contribute to yonghwankim-dev/spring5 development by creating an account on GitHub.

github.com

AppCtx 클래스, 두 개 이상의 인자를 받는 생성자를 사용하는 설정 추가

https://github.com/yonghwankim-dev/spring5/blob/master/sp5-chap03/src/main/java/config/AppCtx.java

 

yonghwankim-dev/spring5

최범균 저, 초보 웹 개발자를 위한 스프링5 프로그래밍 입문 예제. Contribute to yonghwankim-dev/spring5 development by creating an account on GitHub.

github.com

MainForSpring 클래스, MemberListPrinter 관련 코드 추가

https://github.com/yonghwankim-dev/spring5/blob/master/sp5-chap03/src/main/java/spring/MainForSpring.java

 

yonghwankim-dev/spring5

최범균 저, 초보 웹 개발자를 위한 스프링5 프로그래밍 입문 예제. Contribute to yonghwankim-dev/spring5 development by creating an account on GitHub.

github.com

 

실행결과

6.3 DI 방식 2 : 세터 메서드 방식

생성자 오이ㅔ 세터 메서드를 이용해서 객체를 주입받기도 한다. 일반적인 세터(setter) 메서드는 자바빈 규칙에 따라 다음과 같이 작성한다.

  • 메서드 이름이 set으로 시작
  • set 뒤에 첫 글자는 대문자로 시작
  • 파라미터가 1개
  • 리턴 타입이 void

세터 메서드를 이용해서 의존 객체를 주입받는 코드를 작성한다.

MemberInfoPrinter 클래스

package spring;

public class MemberInfoPrinter {
	private MemberDao memDao;
	private MemberPrinter printer;
	
	public void printMemberInfo(String email)
	{
		Member member = memDao.selectByEmail(email);
		if(member==null)
		{
			System.out.println("데이터 없음\n");
			return;
		}
		printer.print(member);
		System.out.println();
	}
	
	public void setMemDao(MemberDao memDao) {
		this.memDao = memDao;
	}
	public void setPrinter(MemberPrinter printer) {
		this.printer = printer;
	}
	
	
}

 

AppCtx 클래스에 세터 메서드를 이용해서 의존을 주입하는 설정 코드 추가

import spring.MemberInfoPrinter

@Configuration
public class AppCtx{
	... 생략
	
	@Bean
	public MemberInfoPrinter infoPrinter()
	{
		MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
		infoPrinter.setMemDao(memberDao());
		infoPrinter.setPrinter(memberPrinter());
		return infoPrinter;
	}
}

위 코드에서 infoPrinter 빈은 세터 메서드를 이용해서 memberDao 빈과 memberPrinter 빈을 주입한다.

 

MainForSpring 클래스에 MemberInfoPrinter 관련 코드 추가

https://github.com/yonghwankim-dev/spring5/blob/master/sp5-chap03/src/main/java/spring/MainForSpring.java

 

yonghwankim-dev/spring5

최범균 저, 초보 웹 개발자를 위한 스프링5 프로그래밍 입문 예제. Contribute to yonghwankim-dev/spring5 development by creating an account on GitHub.

github.com

실행결과

 

생성자 VS 세터 메서드

생성자 주입 방식 장단점

  • 장점 : 빈 객체를 생성하는 시점에 모든 의존 객체가 주입된다.
  • 단점 : 생성자의 파라미터 개수가 많을 경우 각 인자가 어떤 의존 객체를 설정하는지 알아내려면 생성자의 코드를 확인해야 한다.

세터 메서드 주입 방식 장단점

  • 장점 : 세터 메서드 이름을 통해 어떤 의존 객체가 주입되는지 알 수 있다.
  • 단점 : 세터 메서드 방식은 세터 메서드를 사용해서 필요한 의존 객체를 전달하지 않아도 빈 객체가 생성되기 때문에 객체를 사용하는 시점에 NullPointerException이 발생할 수 있다.

 

6.4 기본 데이터 타입 값 설정

아래의 코드는 두 개의 int 타입 값을 세터 메서드로 전달받는다.

VersionPrinter 클래스 구현

package spring;

public class VersionPrinter {
	private int majorVersion;
	private int minorVersion;

	public void print()
	{
		System.out.printf("이 프로그램의 버전은 %d.%d입니다.\n\n", majorVersion,minorVersion);
	}
	
	public void setMajorVersion(int majorVersion) {
		this.majorVersion = majorVersion;
	}
	public void setMinorVersion(int minorVersion) {
		this.minorVersion = minorVersion;
	}
	
	
}

 

AppCtx 클래스에 값 타임 관련 설정 추가

package config;

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

import spring.VersionPrinter;

@Configuration
public class AppCtx{
	... 생략
	@Bean
	public VersionPrinter versionPrinter()
	{
		VersionPrinter versionPrinter = new VersionPrinter();
		versionPrinter.setMajorVersion(5);
		versionPrinter.setMinorVersion(0);
		return versionPrinter;
	}
}

MainForSpring 클래스에 VersionPrinter 관련 코드 추가

public class MainForSpring {
	private static ApplicationContext ctx = null;
	
	public static void main(String args[]) throws IOException
	{
		ctx = new AnnotationConfigApplicationContext(AppCtx.class);
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		
		while(true)
		{
			System.out.println("명령어를 입력하세요:");
			String command = br.readLine();
			if(command.equalsIgnoreCase("exit"))
			{
				System.out.println("종료합니다.");
				break;
			}
			if(command.startsWith("new "))
			{
				processNewCommand(command.split(" "));
				continue;
			}
			else if(command.startsWith("change "))
			{
				processChangeCommand(command.split(" "));
				continue;
			}
			else if(command.equals("list"))
			{
				processListCommand();
				continue;
			}
			else if(command.startsWith("info "))
			{
				processInfoCommand(command.split(" "));
				continue;
			}
			else if(command.equals("version"))
			{
				processVersionCommand();
				continue;
			}
			printHelp();
		}
        
	}
    
    ... 생략
    private static void processVersionCommand() {
        VersionPrinter versionPrinter = ctx.getBean("versionPrinter",VersionPrinter.class);
        versionPrinter.print();
    }
 }

실행결과