해당 포스트는 초보 웹 개발자를 위한 스프링 5 프로그래밍 입문 [최범균 저] 책 내용을 참고하였습니다.
한 클래스가 다른 클래스의 메서드를 실행할 때 이를 의존한다고 표현한다.
혹은 변경에 의해 영향을 받는 관계를 의미한다.
import java.time.LocalDateTime;
public class MemberRegisterService {
private MemberDao memberDao = new MemberDao();
public Long regist(RegisterRequest req) {
// 이메일로 회원 데이터(Member) 조회
Member member = memberDao.selectByEmail(req.getEmail()); //memberDao의 메소드를 사용하며 의존
if (member != null) {
// 같은 이메일을 가진 회원이 이미 존재하면 익셉션 발생
throw new DuplicateMemberException("dup email " + req.getEmail());
}
// 같은 이메일을 가진 회원이 존재하지 않으면 DB에 삽입
Member newMember = new Member(
req.getEmail(), req.getPassword(), req.getName(),
LocalDateTime.now());
memberDao.insert(newMember);
return newMember.getId();
}
}
DI는 의존하는 객체를 직접 생성하는 대신 의존 객체를 전달받는 방식을 사용한다.
// 직접 의존 객체를 생성하지 않고 전달 받음
private MemberDao memberDao;
public MemberRegisterService(MemberDao memberDao) {
this.memberDao = memberDao;
}
DI를 적용한 결과 MemberRegisterservice 객체를 생성할 때 생성자에게 MemberDao 객체를 전달해야한다.
의존 객체를 생성자를 통해 주입한다.
MemberDao dao = new MemberDao();
//의존 객체를 생성자를 통해 주입
MemberRegisterService svc = new MemberRegisterService(dao);
당장은 할 일이 많아보이지만 객체 지향 설계의 이점인 변경의 유연함을 얻을 수 있다.
예를 들어 MemberDao 클래스를 MemberDao 클래스를 상속받는 CachedMemberDao 클래스로 변경하고 싶다면 변경해야할 코드가 많을 수 있다.
이 때 DI를 적용하면 객체를 주입하는 코드 한 곳만 변경하면 된다.
MemberDao memberDao = new MemberDao();
MemberRegisterService regSvc = new MemberRegisterService(memberDao);
ChangePasswordService pwdSvc = new ChangePasswordService(memberDao);
// 아래 부분만 변경하면 모든 memberDao 객체가 변경된다!
MemberDao memberDao = new CachedMemberDao();
MemberRegisterService regSvc = new MemberRegisterService(memberDao);
ChangePasswordService pwdSvc = new ChangePasswordService(memberDao);
그렇다면 실제 객체를 생성하는 코드는 어디있을까?
main 메소드에서 직접 생성할 수도 있겠지만 객체를 생성하고 의존 객체를 주입해주는 클래스를 따로 작성하는 것이 더 나을 것이다.
이런 클래스를 조립기라고 한다.
package assembler;
import spring.ChangePasswordService;
import spring.MemberDao;
import spring.MemberRegisterService;
public class Assembler {
// 객체 생성
private MemberDao memberDao;
private MemberRegisterService regSvc;
private ChangePasswordService pwdSvc;
public Assembler() {
memberDao = new MemberDao();
// 의존 주입
regSvc = new MemberRegisterService(memberDao);
pwdSvc = new ChangePasswordService();
// 의존 주입
pwdSvc.setMemberDao(memberDao);
}
// MemberDao 객체를 가져오기 위한 get 메서드
public MemberDao getMemberDao() {
return memberDao;
}
// MemberRegisterService 객체를 가져오기 위한 get 메서드
public MemberRegisterService getMemberRegisterService() {
return regSvc;
}
// ChangePasswordService 객체를 가져오기 위한 get 메서드
public ChangePasswordService getChangePasswordService() {
return pwdSvc;
}
}
Assembler 클래스는 Assembler 객체를 생성하고 get 메서드로 필요한 객체를 가져와서 사용하는 방식으로 사용된다.
의존 객체를 변경할 때 Assembler에서 객체를 초기화 하는 코드만 변경하면 되므로 수정이 용이하다
즉, 조립기는 객체를 생성하고 의존 객체를 주입하는 기능을 제공하고 객체 제공도 한다.
// 객체 제공
MemberRegisterService regSvc = assembler.getMemberRegisterService();
ChangePasswordService changePwdSvc = assembler.getChangePasswordService();
위 방법으로 Assembler를 직접 작성하고 관리해주는 것은 꽤나 손가고 귀찮은 일이다.
이렇게 스프링 자체가 아닌 의존, DI, 조립기에 대해서 먼저 알아본 이유는 스프링이 DI를 지원하는 조립기 이기 때문이다!
스프링은 앞서 구현한 조립기와 유사한 기능을 제공한다.
필요한 객체를 생성하고 생성한 객체에 의존을 주입하고 객체도 제공한다(get).
차이점은 Assembler는 특정 타입의 클래스만 생성한 반면 스프링은 범용 조립기라는 점이다.
위에 구현된 Assembler.java 대신 스프링을 이용해 객체를 조립하려면 스프링 설정 정보를 작성해야한다.
아래 코드는 스프링 설정 정보를 담은 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;
}
}
이렇게 작성한 설정 정보를 이용해 객체를 생성하고 의존 객체를 주입하는 것은 스프링 컨테이너이므로 위 설정 정보로 컨테이너를 생성해야한다.
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);
컨테이너를 생성하면 getBean() 메서드를 이용해 사용할 객체를 구할 수 있다.
// 컨테이너에서 이름이 memberRegSvc이고 타입이 MemberRegisterService인 빈 객체를 구한다.
MemberRegisterService regSvc = ctx.getBean("memberRegSvc", MemberRegisterService.class);
생성자를 통해 의존 객체를 주입받고 주입 받은 객체를 생성자 안에서 필드에 할당하는 방식이다.
public class MemberRegisterService {
private MemberDao memberDao;
// 생성자를 통해 의존 객체를 주입 받음
public MemberRegisterService(MemberDao memberDao) {
// 주입 받은 객체를 필드에 할당
this.memberDao = memberDao;
}
public Long regist(RegisterRequest req) {
// 주입 받은 의존 객체의 메서드를 사용
Member member = memberDao.selectByEmail(req.getEmail());
...
// 주입 받은 의존 객체의 메서드를 사용
memberDao.insert(newMember);
return newMember.getId();
}
}
일반적인 setter 메서드는 자바빈 규칙에 따라 다음과 같이 작성한다.
세터 메서드 방식으로 의존 주입을 받는 방법은 생성자 방식에서 생성자 대신 setter를 사용하는 그대로이다.
public class MemberInfoPrinter {
...
// setter로 의존 객체 주입
public void setMemberDao(MemberDao memberDao) {
this.memDao = memberDao;
}
// setter로 의존 객체 주입
public void setPrinter(MemberPrinter printer) {
this.printer = printer;
}
}
@Configuration
public class AppCtx {
...
@Bean
public MemberInfoPrinter infoPrinter() {
MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
// setter로 의존 관계 주입
infoPrinter.setMemberDao(memberDao());
infoPrinter.setPrinter(memberPrinter());
return infoPrinter;
}
}
정답은 없다. 상황에 따라 두 방식을 혼용하여 사용한다.
각 방식의 장점
각 방식의 단점
설정 정보를 담고있는 AppCtx.java를 살펴보면 호출될 때마다 매번 새로운 객체를 생성하는지, 그리고 서로 참조하고 있는 객체가 다를텐데 어떻게 서비스가 되는지 등의 의문이 들 것이다.
하지만 스프링 컨테이너는 Bean 객체를 싱글톤 객체로 생성하고 관리한다!
즉, 다른 설정 메서드에서 memberDao()를 호출 할 때는 항상 같은 객체를 리턴한다는 의미이다.
이것이 가능한 이유는 스프링은 설정 클래스를 그대로 사용하지 않기 때문이다.
대신 설정 클래스를 상속한 새로운 설정 클래스를 만들어서 사용한다.
개발을 하다보면 수백여 개의 빈을 설정하게 된다.
그렇다면 한 개의 클래스 파일에 설정하는 것 보다는 영역별로 설정 파일로 나누어서 관리하는 것이 편할 것이다.
@Configuration
public class AppConf1 {
@Bean
public MemberDao memberDao() {
return new MemberDao();
}
@Bean
public MemberPrinter memberPrinter() {
return new MemberPrinter();
}
}
@Configuration
public class AppConf2 {
// @Autowired 애노테이션으로 AppConf1에서 설정한 빈이 자동 주입되어 할당됨
@Autowired
private MemberDao memberDao;
@Autowired
private MemberPrinter memberPrinter;
@Bean
public MemberRegisterService memberRegSvc() {
// 필드로 주입받은 빈 객체를 생성자를 이용해 주입
return new MemberRegisterService(memberDao);
}
...
}
// 여러개의 설정 클래스를 스프링 컨테이너에 전달
ctx = new AnnotationConfigApplicationContext(AppConf1.class, AppConf2.class);
@Configuration
@Import({AppConf2.class})
public class AppConfImport {
@Bean
public MemberDao memberDao() {
return new MemberDao();
}
@Bean
public MemberPrinter memberPrinter() {
return new MemberPrinter();
}
}
여러개의 설정 클래스를 지정하고 싶을 때는 이렇게 배열을 이용하면 된다.
@Configuration
@Import( { AppConf1.class, AppConf2.class } )
public class AppConfImport {
}
@Autowired 애노테이션은 스프링 빈에 의존하는 다른 빈을 자동으로 주입하고 싶을 때 사용한다.
public class MemberInfoPrinter {
@Autowired
private MemberDao memDao;
@Autowired
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();
}
...
}
@Bean
public MemberInfoPrinter infoPrinter() {
MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
// 세터 메서드를 사용해서 의존 주입을 하지 않아도
// 스프링 컨테이너가 @Autowired를 붙인 필드에
// 자동으로 해당 타입의 빈 객체를 주입
return infoPrinter;
}
AppConf 클래스에서 의존 주입을 직접 해주지 않아도 자동으로 그 타입의 빈 객체를 주입한다.
@Configuration 애노테이션을 붙인 설정 클래스도 스프링 빈으로 등록한다.
그러므로 두 개이상의 설정 정보를 설정하는 방법 1에서, 다른 빈과 마찬가지로 필드 주입한 @Autowired를 붙인 대상에 대해 알맞은 빈을 자동으로 주입한다.
// 첫 번째 인자는 빈의 이름이고 두 번째 인자는 빈의 타입
VersionPrinter versionPrinter = ctx.getBean("versoinPrinter", VersionPrinter.class);
빈 이름을 지정하지 않고 타입만으로 빈을 구할 수 있다.
해당 타입의 빈 객체가 한 개만 존재하면 해당 빈을 구해서 리턴한다.
VersionPrinter versionPrinter = ctx.getBean(MemberPrinter.class);
같은 타입의 빈 객체가 두 개 이상 존재할 수도 있다.
그 때 위와 같은 타입만으로 빈을 구한다면 NoUniqueBeanDefinitionException이 발생하게 된다.
주입할 객체가 꼭 스프링 빈이어야 할 필요는 없다.
@Configuration
public class AppCtxNoMemberPrinterBean {
private MemberPrinter printer = new MemberPrinter(); // 빈이 아니다!
@Bean
public MemberDao memberDao() {
return new MemberDao();
}
@Bean
public MemberListPrinter listPrinter() {
return new MemberListPrinter(memberDao(), printer);
}
...
}
이렇게 해도 MemberListPrinter 객체는 정상적으로 동작한다.
개체를 스프링 빈으로 등록할 떄와 등록하지 않을 때의 차이는 스프링 컨테이너가 객체를 관리하는지 여부이다.
MemberPrinter를 빈으로 등록하지 않으므로 스프링 컨테이너에서 MemberPrinter를 구할 수 없다.
//MemberPrinter를 빈으로 등록하지 않았으므로 익셉션 발생
MemberPrinter printer = ctx.getBean(MemberPrinter.class);
스프링 컨테이너는 객체 관리를 위한 다양한 기능을 제공하는데 빈으로 등록한 객체에만 기능을 적용한다.
하지만 이런 스프링 컨테이너가 제공하는 기능들이 필요없고 getBean() 메서드로 구할 필요가 없다면 꼭 빈 객체로 등록해야하는 건 아니다.
그래도 보통은 의존 자동 주입 기능을 전반적으로 사용하는 추세이므로 의존 주입 대상은 스프링 빈으로 등록하는게 좋다.
Spring 1
EDITOR: OJO
[스프링1] 5장. 컴포넌트 스캔 (0) | 2022.11.05 |
---|---|
[스프링1] 4장. 의존 자동 주입 (0) | 2022.11.05 |
[스프링 1] 2장.스프링 시작하기 (0) | 2022.10.27 |
[스프링1] 빈 생명주기 콜백, 빈 스코프 (0) | 2022.10.21 |
[스프링1] 컴포넌트 스캔, 의존관계 자동 주입 (0) | 2022.10.13 |