computer_study

[Spring] 03. 스프링 DI 본문

스터디/스프링5 프로그래밍 입문

[Spring] 03. 스프링 DI

knowable 2022. 3. 31. 23:56

목차

1. 의존이란?

2. DI를 통한 의존 처리

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

4. 예제 프로젝트 만들기

5. 객체 조립기

6. 스프링의 DI설정

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

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

9. getBean() 메서드 사용

10. 주입 대상 객체를 모두 빈 객체로 설정해야 하나?

1. 의존이란?

한 클래스가 다른 클래스의 메서드를 실행할 때를 의존이라 표현.

 

예시

  • MemberRegisterService라는 class에서 memberDao라는 객체를 생성
  • MemberRegisterService에서 현재 요청이 들어온 사용자가 이미 존재하는지 여부를 확인하고, 없다면 DB에 추가하기 위해 memberDao의 selectByEmail(), insert() 메서드를 사용
  • 이 때 MemberRegisterService 클래스가 MemberDao클래스에 의존한다고 한다.

위와같은 경우처럼 클래스 안에서 의존 할 객체를 직접 생성해서 할당할 수 있음.

하지만 유지보수 관점에서 문제 발생 가능성이 존재.

 

해결방법

  •  DI (Dependency Injection) -> 스프링 관련
  • 서비스 로케이터

2. DI를 통한 의존 처리

DI 사용 안했을 때 (위의 객체 생성 예시)

public class MemberRegisterService {
    private MemberDao memberDao = new MemberDao();
    
    public void regis(RegisterRequest req){
        Member member = memberDao.selectByEmail(req.getEmail());
    }
}

DI 사용 했을 때 

public class MemberRegisterService {
    private MemberDao memberDao;
    
    public MemberReigsterService(MemberDao memberDao){
        this.memberDao = memberDao;
    }
    
    public void regis(RegisterRequest req){
        Member member = memberDao.selectByEmail(req.getEmail());
    }
}

이후 MemberRegisterService 클래스를 사용한다면 아래와 같이 의존 객체를 생성자를 통해 주입해야 된다.

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

 

복잡해보이지만 수정의 유연함 때문에 DI를 사용한다.

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

DI를 사용하면, 코드 수정 시 수정 할 부분이 줄어든다.

 

예시

  • MemberRegisterService , ChangePasswordService가 있고 각각 MemberDao객체를 생성했다 가정.
  • 캐시 기능 적용을 위해 MemberDao를 상속받은 CachedMemberDao클래스가 생성됐다면
  • MemberRegisterService , ChangePasswordService 두 곳 모두 코드를 변경해주어야 된다.
public class MemberRegisterService{
    private MemberDao memberDao = new MemberDao();
}
public class ChangePasswordrService{
    private MemberDao memberDao = new MemberDao();
}

/////////////// 아래처럼 변경해야됨 ////////////////////////


public class MemberRegisterService{
    private MemberDao memberDao = new CachedMemberDao();
}
public class ChangePasswordrService{
    private MemberDao memberDao = new CachedMemberDao();
}

DI를 사용했다면 다음처럼 바꿀 수 있다.

MemberDao memberDao = new MemberDao();
MemberRegisterService regSvc = new MemberRegisterService(memberDao);
ChangePasswordrService pwdSvc = new ChangePasswordrService(memberDao);

/////////////// 아래처럼 변경하면 됨 /////////////////////

MemberDao memberDao = new CachedMemberDao();  // <- 얘만 변경
MemberRegisterService regSvc = new MemberRegisterService(memberDao);
ChangePasswordrService pwdSvc = new ChangePasswordrService(memberDao);

4. 예제 프로젝트 만들기

 

 

5. 객체 조립기

위 3번에서 클래스 변경 시 객체를 주입하는 곳 하나만 변경하면, 쉽게 변경할 수 있다고 하였다.

이렇게 변경을 할 수 있는 곳이 객체 조립기(assembler)이다.

main 메서드에서 의존 대상 객체를 생성하고 주입 할 수도 있지만, assembler 객체를 따로 작성하는 것이 더 좋은 방법이다.

 

위 예제들을 assembler class에 따로 작성할 수 있다는 것이다.

public class Assmebler {
    private MemberDao memberDao;
    private MemberRegisterService regSvc;
    private ChangePasswordService pwdSvc;
    
    public Assembler(){
        memberDao = new MemberDao();
        regSvc = new MemberRegisterService(memberDao);
        pwdeSvc = new ChangePasswordService();
    }
}

 

캐시 기능 적용을 위해 MemberDao를 상속받은 CachedMemberDao클래스가 생성됐다면

public class Assmebler {
    private MemberDao memberDao;
    private MemberRegisterService regSvc;
    private ChangePasswordService pwdSvc;
    
    public Assembler(){
        memberDao = new CachedMemberDao();  // -> 해당 부분만 바꾸면 되는 것
        regSvc = new MemberRegisterService(memberDao);
        pwdeSvc = new ChangePasswordService();
    }
}

 

6. 스프링의 DI설정

스프링은 위 조립기와 유사한 기능을 제공하고있다.(범용 조립기 같은 느낌)

 

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

스프링 사용 전 간단 개념

  • @Configuration 애노테이션을 붙여야 스프링 설정 클래스를 사용할 수 있다
  • @Bean 애노테이션은 해당 메서드가 생성한 객체를 스프링 빈이라 설정한다.
    • @Bean
      public MemberDao memberDao(){
          ...
      }
      라면 memberDao라는 이름으로 스프링에 등록된다.
  • 스프링 컨테이너에서 객체 생성, 의존객체 주입 등을 해주는 것이므로 스프링 컨테이너를 생성해주어야 된다
    AnnotationConfigApplicationContext 클래스 사용
  • getBean()메서드를 이용해서 사용할 객체를 구할 수 있다.

5번에서 작성했던 객체 조립기 코드를 스프링 스타일로 작성하면

package main;
...
import java.io.IOException

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

import config.AppCtx

public class MainForSpring{
    private static ApplicationContext ctx = null;
    
    public static void main(String[] args) throws IOException{
        ctx = new AnnotationConfigApplicationContext(AppCtx.class);
        ...
    }
    
    privavte static void processNewCommand(String[] arg){
        MemberRegisterService regSvc = ctx.getBean("memberRegSvc", MemberRegisterService.class);
        ...
    }
    
    private static void processChangeCommand(String[] arg){
        ChangePasswordService changePwdSvc = ctx.getBean("changePwdSvc", ChangePasswordService.class);
        ...
    }

}

이 떄 AppCtx는

package config

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

import spring.ChangePawwrodService;
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;
    }
}

 

DI방식

  • 생성자 방식
    • 위 예제들에서 주입한 방식들이다. (2번, 3번에서 DI를 사용한 경우)
    • 생성자에 전달 할 의존 객체가 두 개 이상이어도 동일한 방식으로 주입할 수 있다.
    • 장점
      • 빈 객체를 생성하는 시점에 모든 의존 객체가 주입된다. (객체를 사용할 때 완전한 상태로 사용 가능)
    • 단점
      • 생성자의 파라미터 개수가 많을 경우 각 인자가 어떤 의존 객체를 설정하는지 알아내려면 생성자 코드를 확인해야 한다.
private MemberDao memberDao;
private MemberPrinter printer;

public MemberListPrinter(MemberDao memberDao, MemberPrinter printer){
    this.memberDao = memberDao;
    this.printer = printer;
}
  • 세터(setter) 메서드 방식
    • 세터 메서드, 자바빈 규칙
      • 메서드 이름이 set으로 시작한다.
      • set 뒤에 첫 글자는 대문자로 시작한다.
      • 파라미터가 1개이다.
      • 리턴 타입이 void이다. 
    • 장점
      • 세터 메서드 이름을 통해 어떤 의존 객체가 주입되는지 알 수 있다.(생성자의 파라미터 개수가 많을 때 유리)
    • 단점
      • 의존객체를 전달하지 않아도 빈 객체가 생성되기에 객체를 사용하는 시점에 NullPointerException이 발생할 수 있다.
public class MemberInfoPrinter{
    private MemberDao memDao;
    private MemberPrinter printer;
    
    public void setMemberDao(MemberDao memberDao){
        this.memDao = memberDao;
    }
    public void setPrinter(MemberPrinter printer){
        this.printer - printer;
    }

}
...
import sprint MemberInfoPrinter;

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

}

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

스프링 컨테이너는 @Bean이 붙은 메서드에 대해 한 개의 객체만 생성

예를들어

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

이 때, memberDao()를 볓 번 호출하더라도 항상 같은 객체를 리턴한다.

 

 

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

두 개 이상의 설정파일 일 때, 스프링 컨테이너를 생성하는 예시

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

각 설정파일 별 @Autowired 애노테이션을 사용해서 자동 주입을 할 수 있다.

자동주입 시 @Bean메서드에서 의존 주입을 위한 코드를 작성하지 않아도 된다.(알맞는 Bean을 자동 주입)

 

...
@Configuration
public class AppConf1{
    @Bean
    public MemberDao memberDao(){
        return new MemberDao();
    }
    @Bean
    public MemberPrinter memberPrinter(){
        return new MemberPrinter();
    }
}
...
import org.spring framework.beans.factory.annotation.Autowired;
...
@Configuration
public class AppConf2{
    @Autowired
    private MemberDao memberDao;
    @Autowired
    private MemberPrinter memberPrinter;
    ...
}

위 예시에서 스프링 컨테이너는 AppConf2객체를 빈으로 등록하고 @Autowired 애조테이션이 붙은 두 필드 memberDao와 memberPrinter에 해당 타입의 빈 객체를 주입한다.

 

 

@Import 애노테이션을 사용하여 두 개 이상의 설정파일을 사용 할 수도 있다.

@Configuration
@Import(AppConf2.class)
public class AppConfImport{
   @Bean
   public MemberDao memberDao(){
       return new MemberDao();
   }
   @Bean
   public MemberPrinter memberPrinter(){
       return new MemberPrinter();
   }
}

위와 같이 작성했다면, AppConfImport설정클래스 사용 시 AppConf2설정 클래스도 함께 사용할 수 있다.

ctx = new AnnotationConfigApplicationContext(AppConfImport.class);

@Import 애노테이션에는 두 개 이상의 설정클래스도 지정할 수 있다

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

9. getBean() 메서드 사용

getBean() 메서드로 사용할 빈 객체를 구할 수 있다.

VersionPrinter versionPrinter = ctx.getBean("versionPrinter", VersionPrinter.class);

 

빈 이름 지정 없이 타입만으로 구할 수도 있다.

VersionPrinter versionPrinter = ctx.getBean(VersionPrinter.class);

다만, 타입이 하나고 빈이 두 개로 설정되어있다면, 익셉션이 발생한다.

 

10. 주입 대상 객체를 모두 빈 객체로 설정해야 하나?

  • 빈으로 설정하지 않아도 동작은 한다.
  • 빈으로 등록한다는 의미 = 스프링 컨테이너가 객체를 관리한다.
  • 자동 주입, 라이프 사이클 관리 등 객체 관리를 제공받고 싶지 않고, getBean() 메서드로 구할 필요가 없다면 사용하지 않아도 된다.
  • 최근엔 의존 자동주입 기능을 거의 사용하는 추세기에 빈으로 등록
Comments