본문 바로가기

Java/Spring

Spring 5 입문: Chapter 03A. Spring 의존 주입(DI, Dependency Injection)

더보기

이 프로젝트의 개발 환경

  • 개발 언어 및 개발 환경
    • OpenJDK 17
  • 기타 환경
    • macOS Sonoma 14.1.1
    • IntelliJ IDEA 2020.3 Ultimate Edition

의존 주입(DI, Dependency Injection)이란?

Spring의 의존성(Dependency)은 객체 간의 의존입니다.

다음 예시는 객체 간 의존성을 설명하기 위한 이메일로 회원가입을 진행하는 코드입니다.

import java.time.LocalDateTime;

public class MemberRegisterService
{
	private MemberDao memberDao = new MemberDao();
	
	public void regist(RegisterRequest req)
	{
		// 이메일로 회원 데이터(Member) 조회
		Member member = memberDao.selectByEmail(req.getEmail());
		// 같은 이메일을 가진 회원이 이미 존재하면 실패
		if (null != member)
		{
			throw new DuplicateMemberException();
		}
		
		// 같은 이메일을 가진 회원이 존재하지 않으면 DB에 저장
		Member newMember = new Member(req.getEmail(), req.getPassword(), req.getName(), LocalDateTime.now());
		memberDao.insert(newMember);
	}
}

예시에서 MemberRegisterServiceMemberDao에 엑세스하여 다음 작업을 처리합니다.

  • 동일한 이메일을 가진 회원이 존재하는지 확인합니다.
  • 새로운 회원을 DB에 저장합니다.

이처럼 어떤 클래스가 다른 클래스의 메소드를 실행하는 것을 의존이라고 부릅니다.

예시에서는 MemberRegisterService MemberDao에 의존하고 있습니다.

더보기

의존 관계에서 의존 대상에 대한 객체를 구하는 방법이 필요합니다.

가장 쉬운 방법은 예시처럼 의존 대상에 대한 객체를 직접 생성하는 것입니다.

private MemberDao memberDao = new MemberDao();

클래스 내부에서 의존 대상에 대한 객체를 직접 생성하는 방법은 간단하지만 유지 보수 관점에서 문제가 있습니다.

의존 대상을 인스턴싱하기 위한 또 다른 방법으로는 의존 주입(DI)이 있습니다.

DI를 사용한 의존 처리

의존 주입(DI)은 의존 객체를 직접 생성하는 대신 의존 객체를 전달 받아 사용합니다. 위 예시를 DI 방식으로 수정하면 다음과 같습니다.

import java.time.LocalDateTime;

public class MemberRegisterService
{
	private MemberDao memberDao;
	
	public MemberRegisterService(MemberDao memberDao)
	{
		this.memberDao = memberDao;
	}
	
	public void regist(RegisterRequest req)
	{
		// 이메일로 회원 데이터(Member) 조회
		Member member = memberDao.selectByEmail(req.getEmail());
		// 같은 이메일을 가진 회원이 이미 존재하면 실패
		if (null != member)
		{
			throw new DuplicateMemberException();
		}
		
		// 같은 이메일을 가진 회원이 존재하지 않으면 DB에 저장
		Member newMember = new Member(req.getEmail(), req.getPassword(), req.getName(), LocalDateTime.now());
		memberDao.insert(newMember);
	}
}

예시의 코드는 다음과 같이 호출 될 수 있습니다.

MemberDao memberDao = new MemberDao();
MemberRegisterService service = new MemberRegisterService(memberDao);

의존 객체를 직접 생성하는 방식과의 차이점은 인자로 전달 받음으로써 코드 유연성이 높아진다는 점입니다.

예를 들어 의존 객체를 상속 받는 CachedMemberDao를 생성하고 새로운 의존 객체를 참조하더라도 MemberRegisterService 클래스 내부 코드는 수정 될 내용이 없습니다.

// 기존 코드: MemberDao memberDao = new MemberDao();
MemberDao memberDao = new CacheMemberDao();
MemberRegisterService service = new MemberRegisterService(memberDao);
더보기

의존 객체를 직접 생성하면 다음과 같이 의존 객체를 참조하는 모든 클래스 내부 코드를 수정해야합니다.

// 기존 코드: private MemberDao memberDao = new MemberDao();
private MemberDao memberDao = new CachedMemberDao();

예제 프로젝트 생성

sp5-chap03 프로젝트를 생성하고 chap03 패키지를 추가합니다. 예제 프로젝트는 다음 클래스를 포함합니다.

  • Member: 회원 데이터입니다.
  • MemberDao: 회원 데이터가 저장 및 관리되는 컨테이너입니다.
  • RegisterRequest: 회원 데이터를 등록하는 요청입니다.
  • MemberRegisterService: 회원 데이터를 등록합니다.
  • ChangePasswordService: 회원 비밀번호를 변경합니다.
  • Assembler: 각 서비스를 관리합니다.
  • Main: 사용자 키 입력에 따라서 각 서비스를 실행합니다.

다음 코드를 참고하여 Member.java 클래스를 생성합니다.

package chap03;

import java.time.LocalDateTime;

public class Member
{
	private Long id;
	private String email;
	private String password;
	private String name;
	private LocalDateTime registerDateTime;
	
	public Member(String email,
	              String password,
	              String name,
	              LocalDateTime registerDateTime)
	{
		this.email = email;
		this.password = password;
		this.name = name;
		this.registerDateTime = registerDateTime;
	}
	
	public void changePassword(String oldPassword,
	                           String newPassword)
	{
		if (!this.password.equals(oldPassword))
			throw new RuntimeException();
		this.password = newPassword;
	}
	
	protected void setId(Long id)
	{
		this.id = id;
	}
	
	public Long getId()
	{
		return id;
	}
	
	public String getEmail()
	{
		return email;
	}
	
	public String getPassword()
	{
		return password;
	}
	
	public String getName()
	{
		return name;
	}
	
	public LocalDateTime getRegisterDateTime()
	{
		return registerDateTime;
	}
}

다음 코드를 참고하여 MemberDao.java 클래스를 생성합니다.

package chap03;

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

public class MemberDao
{
	private static long nextId = 0L;
	private Map<String, Member> mapMember = new HashMap<>();
	
	public Member selectByEmail(String email)
	{
		return mapMember.get(email);
	}
	
	public void insert(Member member)
	{
		member.setId(++nextId);
		mapMember.put(member.getEmail(), member);
	}
	
	public void update(Member member)
	{
		mapMember.replace(member.getEmail(), member);
	}
}

다음 코드를 참고하여 RegisterRequest.java 클래스를 생성합니다.

package chap03;

public class RegisterRequest
{
	private String email;
	private String password;
	private String confirmPassword;
	private String name;
	
	public boolean isPasswordEqualToConfirmPassword()
	{
		return password.equals(confirmPassword);
	}
	
	public void setEmail(String email)
	{
		this.email = email;
	}
	
	public void setPassword(String password)
	{
		this.password = password;
	}
	
	public void setConfirmPassword(String confirmPassword)
	{
		this.confirmPassword = confirmPassword;
	}
	
	public void setName(String name)
	{
		this.name = name;
	}
	
	public String getEmail()
	{
		return email;
	}
	
	public String getPassword()
	{
		return password;
	}
	
	public String getConfirmPassword()
	{
		return confirmPassword;
	}
	
	public String getName()
	{
		return name;
	}
}

다음 코드를 참고하여 MemberRegisterService.java 클래스를 생성합니다.

package chap03;

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 (null != member)
			throw new RuntimeException(req.getEmail());
		member = new Member(req.getEmail(), req.getPassword(), req.getName(), LocalDateTime.now());
		memberDao.insert(member);
		return member.getId();
		
	}
}

다음 코드를 참고하여 ChangePasswordService.java 클래스를 생성합니다.

package chap03;

public class ChangePasswordService
{
	private MemberDao memberDao;
	
	public void changePassword(String email,
	                           String oldPassword,
	                           String newPassword)
	{
		Member member = memberDao.selectByEmail(email);
		if (null == member)
			throw new RuntimeException();
		member.changePassword(oldPassword, newPassword);
		memberDao.update(member);
	}
	
	public void setMemberDao(MemberDao memberDao)
	{
		this.memberDao = memberDao;
	}
}

다음 코드를 참고하여 Assembler.java 클래스를 생성합니다.

 

package chap03;

public class Assembler
{
	private MemberDao memberDao;
	private MemberRegisterService registerService;
	private ChangePasswordService changePasswordService;
	
	public Assembler()
	{
		memberDao = new MemberDao();
		registerService = new MemberRegisterService(memberDao);
		changePasswordService = new ChangePasswordService();
		changePasswordService.setMemberDao(memberDao);
	}
	
	public MemberDao getMemberDao()
	{
		return memberDao;
	}
	
	public MemberRegisterService getRegisterService()
	{
		return registerService;
	}
	
	public ChangePasswordService getChangePasswordService()
	{
		return changePasswordService;
	}
}

다음 코드를 참고하여 Main.java 클래스를 생성합니다.

package chap03;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main
{
	private static Assembler assembler = new Assembler();
	
	public static void main(String[] args) throws IOException
	{
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
		
		while (true) {
			System.out.println("명령어를 입력하세요: ");
			String command = reader.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
			{
				printHelp();
			}
		}
	}
	
	private static void processNewCommand(String[] args)
	{
		if (5 != args.length)
		{
			printHelp();
			return;
		}
		
		MemberRegisterService registerService = assembler.getRegisterService();;
		RegisterRequest req = new RegisterRequest();
		req.setEmail(args[1]);
		req.setName(args[2]);
		req.setPassword(args[3]);
		req.setConfirmPassword(args[4]);
		
		if (!req.isPasswordEqualToConfirmPassword())
		{
			System.out.println("암호와 확인이 일치하지 않습니다.\n");
			return;
		}
		
		try {
			registerService.regist(req);
			System.out.println("등록했습니다.");
		} catch (RuntimeException e) {
			e.printStackTrace();
		}
	}
	
	private static void processChangeCommand(String[] args)
	{
		if (4 != args.length)
		{
			printHelp();
			return;
		}
		
		ChangePasswordService changePasswordService = assembler.getChangePasswordService();
		try {
			changePasswordService.changePassword(args[1], args[2], args[3]);
			System.out.println("암호를 변경했습니다.\n");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	private static void printHelp()
	{
		System.out.println();
		System.out.println("잘못된 명령입니다. 아래 명령어 사용법을 확인하세요.");
		System.out.println("명령어 사용법:");
		System.out.println("new 이메일 이름 암호 암호확인");
		System.out.println("change 이메일 현재암호 변경암호");
		System.out.println();
	}
}

Spring의 DI 설정

의존 주입의 핵심은 Assembler 클래스입니다. Assembler는 객체를 생성하고 객체 간 의존 주입으로 서로 연결합니다.

Spring의 DI에서도 유사한-필요한 객체를 생성하고 의존을 주입하는 기능을 제공합니다.

다음 코드를 참고하여 AppContext.java 클래스를 생성합니다.

package chap03;

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

@Configuration
public class AppContext
{
	@Bean
	public MemberDao memberDao() 
	{
		return new MemberDao();
	}
	
	@Bean
	public MemberRegisterService memberRegSvc()
	{
		return new MemberRegisterService(memberDao());
	}
	
	@Bean
	public ChangePasswordService changePwdSvc()
	{
		ChangePasswordService svc = new ChangePasswordService();
		svc.setMemberDao(memberDao());
		return svc;
	}
}
코드 비고
Line 6 @Configuration Spring 설정 클래스를 의미합니다.
Line 9 Line 15 Line 21 @Bean 메소드가 생성하는 객체를 Spring의 Bean 객체로 지정합니다.
메소드의 이름은 Bean 객체의 이름입니다.
Line 18 Line 25 memberDao() 앞서 작성한 Assembler 클래스 처럼 의존 주입을 처리합니다.

AppContext.java 클래스는 @Configuration 어노테이션을 등록해  Spring 설정 클래스로 사용합니다.

Spring의 설정 클래스를 사용하려면 컨테이너 ApplicactionContext 를 생성해야 합니다.

ApplicationContext ctx = new AnnotationConfigApplicationContext(AppContext.class);

새로 작성한 AppContext 클래스는 기존의 Assembler 클래스의 역할을 완전히 대체 할 수 있습니다.

기존 Main 클래스를 기반으로 새로운 MainForSpring.java 클래스를 생성하고 Assembler 대신 AppContext를 사용하도록 코드를 수정합니다.

package chap03;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class MainForSpring
{
	private static ApplicationContext ctx = null;
	
	public static void main(String[] args) throws IOException
	{
		ctx = new AnnotationConfigApplicationContext(AppContext.class);
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
		
		while (true) {
			System.out.println("명령어를 입력하세요: ");
			String command = reader.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
			{
				printHelp();
			}
		}
	}
	
	private static void processNewCommand(String[] args)
	{
		if (5 != args.length)
		{
			printHelp();
			return;
		}
		
		MemberRegisterService registerService = ctx.getBean("memberRegSvc", MemberRegisterService.class);
		RegisterRequest req = new RegisterRequest();
		req.setEmail(args[1]);
		req.setName(args[2]);
		req.setPassword(args[3]);
		req.setConfirmPassword(args[4]);
		
		if (!req.isPasswordEqualToConfirmPassword())
		{
			System.out.println("암호와 확인이 일치하지 않습니다.\n");
			return;
		}
		
		try {
			registerService.regist(req);
			System.out.println("등록했습니다.");
		} catch (RuntimeException e) {
			e.printStackTrace();
		}
	}
	
	private static void processChangeCommand(String[] args)
	{
		if (4 != args.length)
		{
			printHelp();
			return;
		}
		
		ChangePasswordService changePasswordService = ctx.getBean("changePwdSvc", ChangePasswordService.class);
		try {
			changePasswordService.changePassword(args[1], args[2], args[3]);
			System.out.println("암호를 변경했습니다.\n");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	private static void printHelp()
	{
		System.out.println();
		System.out.println("잘못된 명령입니다. 아래 명령어 사용법을 확인하세요.");
		System.out.println("명령어 사용법:");
		System.out.println("new 이메일 이름 암호 암호확인");
		System.out.println("change 이메일 현재암호 변경암호");
		System.out.println();
	}
}
코드 비고
Line 16 new AnnotationConfigApplicationContext() DI를 처리하는 AppContext를 사용하기 위한 컨테이너입니다.
Line 54 ctx.getBean("memberRegSvc") MemberRegisterService를 Bean 객체로 구합니다.
Line 83 ctx.getBean("changePwdSvc") ChangePasswordService를 Bean 객체로 구합니다.

정리 및 복습

  • 의존 주입(DI, Dependency Injection)객체 간의 의존성을 주입하는 과정입니다.
  • 의존(Dependency)은 어떤 객체가 다른 객체의 메소드를 호출하는 과정입니다.
  • Spring에서는 DI를 구현하기 위해 설정 클래스를 사용합니다.
  • 설정 클래스는 @Configuration 어노테이션을 등록합니다.
  • 설정 클래스를 사용하려면 컨테이너 ApplicactionContext를 생성합니다.
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppContext.class);
  • 설정 클래스에서 DI는 Bean 객체를 통해 처리됩니다.
  • 설정 클래스에서 Bean 객체를 생성하는 메소드는 @Bean 어노테이션을 등록합니다.
  • Bean 객체를 생성하는 메소드의 이름은 Bean 이름입니다.