여니의 프로그래밍 study/Spring & Spring Boot

[Spring] 스프링 DI (의존, 의존주입, 조립기) - 2

여니's 2023. 8. 6. 13:34

참고 자료 : 초보 개발자를 위한 스프링5 프로그래밍 입문

해당 게시글은 초보 개발자를 위한 스프링5 프로그래밍 입문


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

(1) 스프링 사용 전, 설정 정보 클래스를 작성해야 함

-> 스프링이 어떤 객체를 생성하고

의존을 어떻게 주입할지에 관련하여 정의한 설정 정보들

 

@Configuration

: 스프링 설정 클래스를 의미,

해당 어노테이션을 붙여야 스프링 설정 클래스로 인식함

 

@Bean 

: 해당 메서드가 생성한 객체를 스프링 빈이라고 설정함

예를 들어 memberDao() 메서드를 이용해서 생성한 빈 객체는

memberDao라는 이름으로 스프링에 등록된다. 

@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;
    }
 }

 

스프링 빈 객체를 생성해야 하는 이유?

-> 스프링 컨테이너가 관리하는 대상으로 등록하기 위함. (아래 추가 설명)

 

 

(2) 스프링 컨테이너를 생성

-> 객체 생성 및 의존 객체를 주입하는 건 스프링 컨테이너.

따라서 설정 클래스를 이용하여 컨테이너를 생성해야 함

 

AnnotaionConfigApplicationContext 클래스를 이용한다

해당 클래스는 자바 어노테이션을 이용한 클래스로부터 객체 설정 정보를 가져온다.

// 스프링 컨테이너 생성
ApplicaionContext ctx = new AnnotationConfigApplicaiontContext(AppCtx.class);

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

 

 

-> 스프링 컨테이너(ctx)로부터 이름이 memberRegSvc인 빈 객체를 구하는 과정임

 

MainForAssembler.java

-> 해당 클래스를 스프링 컨테이너를 사용하도록 변경할 것.

package main;

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

import assembler.Assembler;
import spring.ChangePasswordService;
import spring.DuplicateMemberException;
import spring.MemberNotFoundException;
import spring.MemberRegisterService;
import spring.RegisterRequest;
import spring.WrongIdPasswordException;

public class MainForAssembler {

	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;
			}
			printHelp();
		}
	}

	private static Assembler assembler = new Assembler();

	private static void processNewCommand(String[] arg) {
		if (arg.length != 5) {
			printHelp();
			return;
		}
		MemberRegisterService regSvc = assembler.getMemberRegisterService();
		RegisterRequest req = new RegisterRequest();
		req.setEmail(arg[1]);
		req.setName(arg[2]);
		req.setPassword(arg[3]);
		req.setConfirmPassword(arg[4]);
		
		if (!req.isPasswordEqualToConfirmPassword()) {
			System.out.println("암호와 확인이 일치하지 않습니다.\n");
			return;
		}
		try {
			regSvc.regist(req);
			System.out.println("등록했습니다.\n");
		} catch (DuplicateMemberException e) {
			System.out.println("이미 존재하는 이메일입니다.\n");
		}
	}

	private static void processChangeCommand(String[] arg) {
		if (arg.length != 4) {
			printHelp();
			return;
		}
		ChangePasswordService changePwdSvc = 
				assembler.getChangePasswordService();
		try {
			changePwdSvc.changePassword(arg[1], arg[2], arg[3]);
			System.out.println("암호를 변경했습니다.\n");
		} catch (MemberNotFoundException e) {
			System.out.println("존재하지 않는 이메일입니다.\n");
		} catch (WrongIdPasswordException e) {
			System.out.println("이메일과 암호가 일치하지 않습니다.\n");
		}
	}

	private static void printHelp() {
		System.out.println();
		System.out.println("잘못된 명령입니다. 아래 명령어 사용법을 확인하세요.");
		System.out.println("명령어 사용법:");
		System.out.println("new 이메일 이름 암호 암호확인");
		System.out.println("change 이메일 현재비번 변경비번");
		System.out.println();
	}
}

 

MainForSpring.java

-> MainForAssembler.java 클래스와 다른점은

Assembler클래스 대신 ApplicationContext(스프링 컨테이너)를 사용했다는 것

 

Assembler는 직접 객체를 생성하지만

AnnotaionConfigApplicationContext는 설정파일(AppCtx클래스)로부터

생성할 객체와 의존 주입 대상을 정하는 방식

package main;

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

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

import config.AppCtx;
import spring.ChangePasswordService;
import spring.DuplicateMemberException;
import spring.MemberInfoPrinter;
import spring.MemberListPrinter;
import spring.MemberNotFoundException;
import spring.MemberRegisterService;
import spring.RegisterRequest;
import spring.VersionPrinter;
import spring.WrongIdPasswordException;

public class MainForSpring {

	private static ApplicationContext ctx = null;
	
	public static void main(String[] args) throws IOException {
		ctx = new AnnotationConfigApplicationContext(AppCtx.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 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 processNewCommand(String[] arg) {
		if (arg.length != 5) {
			printHelp();
			return;
		}
		MemberRegisterService regSvc = 
				ctx.getBean("memberRegSvc", MemberRegisterService.class);
		RegisterRequest req = new RegisterRequest();
		req.setEmail(arg[1]);
		req.setName(arg[2]);
		req.setPassword(arg[3]);
		req.setConfirmPassword(arg[4]);
		
		if (!req.isPasswordEqualToConfirmPassword()) {
			System.out.println("암호와 확인이 일치하지 않습니다.\n");
			return;
		}
		try {
			regSvc.regist(req);
			System.out.println("등록했습니다.\n");
		} catch (DuplicateMemberException e) {
			System.out.println("이미 존재하는 이메일입니다.\n");
		}
	}

	private static void processChangeCommand(String[] arg) {
		if (arg.length != 4) {
			printHelp();
			return;
		}
		ChangePasswordService changePwdSvc = 
				ctx.getBean("changePwdSvc", ChangePasswordService.class);
		try {
			changePwdSvc.changePassword(arg[1], arg[2], arg[3]);
			System.out.println("암호를 변경했습니다.\n");
		} catch (MemberNotFoundException e) {
			System.out.println("존재하지 않는 이메일입니다.\n");
		} catch (WrongIdPasswordException e) {
			System.out.println("이메일과 암호가 일치하지 않습니다.\n");
		}
	}

	private static void printHelp() {
		System.out.println();
		System.out.println("잘못된 명령입니다. 아래 명령어 사용법을 확인하세요.");
		System.out.println("명령어 사용법:");
		System.out.println("new 이메일 이름 암호 암호확인");
		System.out.println("change 이메일 현재비번 변경비번");
		System.out.println();
	}

	private static void processListCommand() {
		MemberListPrinter listPrinter = 
				ctx.getBean("listPrinter", MemberListPrinter.class);
		listPrinter.printAll();
	}

	private static void processInfoCommand(String[] arg) {
		if (arg.length != 2) {
			printHelp();
			return;
		}
		MemberInfoPrinter infoPrinter = 
				ctx.getBean("infoPrinter", MemberInfoPrinter.class);
		infoPrinter.printMemberInfo(arg[1]);
	}
	
	private static void processVersionCommand() {
		VersionPrinter versionPrinter = 
				ctx.getBean("versionPrinter", VersionPrinter.class);
		versionPrinter.print();
	}

}

 


2. DI 방식 (1) : 생성자 방식

: 두 개 이상의 인자를 받는 생성자를 사용하는 설정을 추가하는 방식

@Bean
public MemberListPrinter listPrinter() {
    return new MemberListPrinter(memberDao(), memberPrinter());
}

 

AppCtx 파일 내에 위와 같이 코드를 추가한다.

 

package config;

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 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;
	}
	
	@Bean
	public MemberPrinter memberPrinter() {
		return new MemberPrinter();
	}
	
	@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;
	}
}

3. DI 방식(2) :세터 메서드 방식 

-> 생성자를 이용하는 방식 외에

세터 메서드를 이용해서도 객체를 주입받을 수 있다.

 

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

 

setMemberDao와 setPrinter는

MemberDao 타입의 객체 및 MemberPrinter 타입의 객체에 대한 의존을 주입할 때 사용된다.

즉 세터 메서드를 이용하여 의존성 주입을 하는 것이다. 

 

 

 

생성자 방식은 빈 객체를 생성하는 시점에 모든 의존 객체가 주입된다.

설정 메서드 방식은 세터 메서드 이름을 통해 

어떤 의존 객체가 주입되는지 알 수 있다. 

 

 

(장점)

생성자 방식 : 객체 사용시 완전한 상태로 사용 가능

설정 메서드 방식 : 메서드 이름만으로 어떤 의존 객체를 설정하는지 유추 가능

 

 

(단점)

생성자 방식 : 파라미터 개수가 많을 경우 , 각 인자가 어떤 객체를 의존하는지 확인하려면

코드를 확인해야 함

설정 메서드 방식 : 필요한 의존 객체를 전달하지 않아도 빈 객체가 생성되기에

객체 사용 시점에 NullPointerExceptino이 발생할 수 있음

 

 


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

- 스프링 컨테이너가 생성한 빈은 싱글톤 객체,

@Bean이 붙은 메서드에 대해 한 개의 객체만 생성함

다른 설정 메서드에서 memberDao()를 몇 번을 호출하더라도

항상 같은 객체만을 리턴한다는 의미.

 

 

이게 가능한 이유는

스프링 컨테이너가 설정 클래스를 상속한

 새로운 설정 클래스를 만들어서 사용하기 때문이다. 

 

 


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

빈의 개수가 많아질수록

하나의 설정 파일에 기재하기 보다는

영역별로 설정 파일을 구분하여 사용하는 것이 좋다.

 

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();
	}
	
}
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;
	}
}

 

@Autowired 어노테이션은

스프링의 자동 주입 기능을 위함이다.

스프링 설정 클래스의 @Bean 메서드에서

의존 주입을 위한 코드를 작성하지 않아도 된다. 

 

세터메서드를 사용해서 의존 주입을 하지 않아도

@Autowired를 붙인 필드에

자동으로 해당 타입의 빈 객체가 주입이 된다. 

 

 

 

해당 타입의 빈을 찾아서

필드에 할당한다.

 

 

위 코드에서 예를 들면

MemberDao  타입의 빈을 memberDao 필드에 할당한다

AppConf2 클래스의 meberDao 필드에는

AppConf1 클래스에서 설정한 빈이 할당된다.

(싱글톤이니까 하나 이상의 객체를 생성할 수 없다)

 

 

설정 클래스가 두개 이상일 경우

방식은 두 가지

 

1. 파라미터로 전달

파라미터로 설정 클래스를 추가로 전달하면 된다.

 

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

 

2. @import 어노테이션 사용

@import(AppConf2.class)

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();
	}
}

이렇게 하면

스프링 컨테이너 생성시 AppConf2 클래스를 따로 지정할 필요가 없어진다.

즉 파라미터로 넘겨주지 않아도 된다는 말이다. 

 

배열을 이용해서 

2개 이상의 설정 클래스도 지정이 가능하다.

@import ({AppConf1.class, AppConfig2.class})

 

 

 

6. 객체를 스프링 빈으로 등록할 때와 등록하지 않을 때의 차이점

: 스프링 컨테이너가 객체를 관리하는지의 여부

 

 

스프링 컨테이너는

자동 주입, 라이프 사이클 관리 등

단순 객체 생성 외에도 객체 관리를 위한 다양한 기능을 제공하는데

빈으로 등록한 객체에만 해당 기능을 적용한다. 

 

 

그러나, 의존 주입 대상을 스프링 빈으로 등록하는 것이 보통이다.