Chapter 03. Spring DI(Dependency Injection)

Posted by yunki kim on April 25, 2022

  의존성 주입을 통해 객체를 생성할 경우 의존성을 생성해 주입할 코드가 필요하다. 만약 이 로직을 별도의 클래스로 분리한다면 이 클래스를 서로 다른 두 객체를 조립하는(의존 객체를 주입하는) 클래스, 즉 조립기로 볼 수 있다.

  조립기의 예시는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Assembler {
 
    private final MemberDao memberDao;
    private final MemberRegisterService registerService;
    private final ChangePasswordService passwordService;
 
    public Assembler() {
        memberDao = new MemberDao();
        registerService = new MemberRegisterService(memberDao);
        passwordService = new ChangePasswordService();
        passwordService.setMemberDao(memberDao);
    }
 
    public MemberDao getMemberDao() {
        return memberDao;
    }
 
    public MemberRegisterService getRegisterService() {
        return registerService;
    }
 
    public ChangePasswordService getPasswordService() {
        return passwordService;
    }
}
 
cs

  만약 여기서 MemberDao의 인스턴스가 아닌, MemberDao를 상속한 CachedMemberDao를 사용해야 한다면 해당 부분만 바꾸어 주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
public class Assembler {
 
   ...
 
    public Assembler() {
        memberDao = new CachedMemberDao();
        ...
    }
 
    ...
}
 
cs

스프링의 DI 설정

  스프링은 DI를 지원하는 조립기이다. 따라서 위의 조립기 예제와 비슷한 기능을 제공한다. 다른 점은 Assembler 의 getMemberRegisterService() 메서드 처럼 특정 객체를 제공하는 것이 아닌 범용 조립기 라는 것이다.

  따라서 스프링을 사용해 조립기를 생성해 보자. 이를 위해서는 스프링이 어떤 객체를 생성하고, 의존을 어떻게 주입할지를 정의한 설정 클래스를 구현해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 스프링 설정 클래스로 사용함기 위해 사용되는 어노테이션
@Configuration
public class AppCtx {
 
    // 생성한 객체를 빈으로 등록하는 어노테이션
    @Bean
    public MemberDao memberDao() {
        return new MemberDao();
    }
 
    // 이름이 "memberRegisterService"인 @Bean 메서드를 설정.
    @Bean
    public MemberRegisterService memberRegisterService() {
        // 생성자를 통해 의존성 주입
        // 따라서 MemberRegisterService 객체는 내부에서 memberDao bean 객체를 사용
        return new MemberRegisterService(memberDao());
    }
 
    @Bean
    public ChangePasswordService changePasswordService() {
        ChangePasswordService passwordService = new ChangePasswordService();
        // setter 를 통해 의존성 주입
        passwordService.setMemberDao(memberDao());
        return passwordService;
    }
}
 
cs
 

  객체를 생성하고 의존 객체를 주입하는 것은 스프링 컨테이너다. 따라서 설정 클래스를 이용해 컨테이너를 생성해야 한다. ApplicationContext는 스프링 컨테이너다. 스프링 컨테이너를 생성했다면 getBean() 메서드를 통해 사용할 객체를 구할 수 있다.

1
2
3
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppCtx.class);
// 컨테이너에서 이름이 memberRegisterService 인 빈 객체를 구한다.
MemberRegisterService registerService = applicationContext.getBean("memberRegisterService", MemberRegisterService.class);
cs

DI 방식

생성자 방식

  생성자 방식은 말 그대로 생성자를 통해 의존 객체를 주입받아 필드에 할당하는 방식이다. 생성자에 전달할 의존 객체가 두 개 이상이여도 아래 예시와 같은 방식으로 주입하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MemberRegisterService {
 
    private final MemberDao memberDao;
 
    // 생성자를 통해 의존 객체를 주입받는다.
    public MemberRegisterService(MemberDao memberDao) {
        // 주입 받은 객체를 필드에 할당.
        this.memberDao = memberDao;
    }
 
    public Long register(RegisterRequest req) {
        // 주입 받은 의존 객체의 메서드를 사용.
        Member member = memberDao.selectByEmail(req.getEmail());
        ...
    }
}
cs

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class AppCtx {
 
    @Bean
    public MemberDao memberDao() {
        return new MemberDao();
    }
 
    @Bean
    public MemberRegisterService memberRegisterService() {
        // 생성자를 통해 의존성 주입
        return new MemberRegisterService(memberDao());
    }
 
    ...
}
cs

세터 메서드 방식

  세터(setter) 메서드를 이용해 객체를 주입할 떄 세터는 자바빈 규칙에 따라 다음과 같이 작성해야 한다.

    1. 메서드 이름을 set으로 시작한다.

    2. set 뒤에 첫 글자느 대문자로 시작한다.

    3. 파라미터가 1개 이다.

    4. 리턴 타입이 void이다.

  자바빈에서는 게터와 세터를 사용해 프로퍼티를 정의한다. 게터와 세터는 get, set으로 시작하고 뒤에는 사용할 프로퍼티 이름의 첫 글자를 대문자로 바꾼 글자를 사용한다. 세터는 프로퍼티 값을 변경하기 때문에 프로퍼티 설정 메서드라고 부른다.

생성자 vs 세터 메서드

  생성자와 세터 메서드를 사용한 DI방식의 차이는 다음과 같다.

생성자 방식

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

  장점: 빈 객체를 생성하는 시점에 필요한 모든 의존객체를 주입받기 떄문에 객체를 사용할 때 완전한 상태로 사용할 수 있다.

  단점: 파라미터 개수가 많으면 각 인자가 어떤 의존 객체를 설정하는지 알기 위해 생성자를 확인해야 한다.

설정 메서드 방식

  세터 메서드 이름을 통해 어떤 의존 객체가 주입되는지 알 수 있다.

  장점: 메서드 이름 만으로 어떤 의존 객체를 설정하는지 알 수 있다.

  단점: 필요한 의존 객체를 전달하지 않아도 객체가 생성되기 때문에 객체를 사용하는 시점에 NullPointerExceptino이 발생할 수 있다.

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

  위에서 사용한 스프링 설정 클래스를 다시 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class AppCtx {
 
    @Bean
    public MemberDao memberDao() {
        return new MemberDao();
    }
 
    @Bean
    public MemberRegisterService memberRegisterService() {
        return new MemberRegisterService(memberDao());
    }
 
    @Bean
    public ChangePasswordService changePasswordService() {
        ChangePasswordService passwordService = new ChangePasswordService();
        passwordService.setMemberDao(memberDao());
        return passwordService;
    }
 
    ...
}
cs

  여기서 memberRegisterService() 메서드와 changePsswordService() 메서드는 MemberDao 인스턴스를 생성하는 memberDao() 메서드를 호출하고 있다. 이 때, MemberRegisterService 인스턴스는 회원 등록을, ChangePasswordService는 회원의 비밀번호를 변경하는 객체다. 또 한, memberDao() 메서드는 로직상 매번 새로운 인스턴스를 생성하므로 "MemberRegisterService로 등록한 회원의 비밀번호가 ChangePasswordSersvice 인스턴스로 변경이 안될 것이다." 라는 추측을 할 수 있다(두 곳에서 사용하는 MemberDao 의 인스턴스가 다르므로).

  하지만, 스프링 컨테이너가 생성한 빈은 싱글통 객체다. 스프링 컨테이너는 @Bean 이 붙은 메서드에 대해 한 개의 객체만 생성한다. 이것이 가능한 이유는 스프링이 설정 클래스를 그대로 사용하지 않기 떄문이다. 스프링은 설정 클래스를 상속한 새로운 설정 클래스를 런타임에 만들어 사용한다. 이 설정 클래스는 다음과 유사한 방식으로 작동한다,

1
2
3
4
5
6
7
8
9
10
11
12
public class AppContextExtends extends AppContext {
 
    private Map<String, Object> beans = ...;
    
    @Override
    public MemberDao memberDao() {
        if (!beans.containsKey("memberDao")) {
            beans.put("mmeberDao"super.memberDao());
        }
        return (MemberDao) beans.get("memberDao");
    }
}
cs

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

  두 개 이상의 파일로 설정 클래스들을 나눠야 한다면, 다음과 같이 @Autowired 어노테이션을 사용해 의존성을 주입해 주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class AppCtx {
 
    @Bean
    public MemberDao memberDao() {
        return new MemberDao();
    }
    
    ...
}
 
@Configuration
public class AppConf {
 
    @Autowired
    private MemberDao memberDao;
 
    @Bean
    public MemberRegisterService memberRegisterService() {
        return new MemberRegisterService(memberDao);
    }
}
cs

  @Autowired 어노테이션을 필드에 사용하면, 해당 타입의 빈을 찾아 필드에 할당한다. 또 한, @Configuration 어노테이션이 붙은 설정 클래스도 스프링 빈으로 등록된다.

@Import 어노테이션 사용

  @Import 어노테이션을 사용해도 두 개 이상의 설정 파일을 갖이 사용할 수 있다. 예시는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@Import(AppConf.class)
public class AppCtx {
 
    @Bean
    public MemberDao memberDao() {
        return new MemberDao();
    }
    
    ...
}
cs

  @Import 어노테이션으로 지정한 클래스는 AppCtx 클래스를 사용할 때 같이 사용하기 때문에 별도로 설정 클래스로 지정할 필요가 없다. 즉, 두 설정을 함께 사용해 컨테이너를 초기화 한다.

  만약 두 개 이상의 설정 클래스를 지정하고 싶다면, 다음과 같이 배열을 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@Import({AppConf.class, AppConf2.class})
public class AppCtx {
 
    @Bean
    public MemberDao memberDao() {
        return new MemberDao();
    }
    
    ...
}
 
cs

getBean() 메서드 사용

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

1
VersionPrinter versionPrinter = context.getBean("versionPrinter", VersionPrinter.class);
cs

  여기서, 만약 구하고자 하는 빈 객체가 한 개만 존재한다면, 빈 이름을 지정하지 않고 타입만으로 빈을 구할 수 있다.

1
VersionPrinter versionPrinter = context.getBean(VersionPrinter.class);
cs

  getBean() 메서드는 BeanFactory 인터페이스에 정의되 있고, getBean() 메서드의 실제 구현은 AbstractApplicationContext 에 구현되 있다.

1
2
3
4
5
6
7
8
9
// BeanFactory 내에 정의된 getBean 메서드
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
 
// AbstractApplicationContext 내에 존재하는 BeanFactory의 구현
@Override
public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
    assertBeanFactoryActive();
    return getBeanFactory().getBean(name, requiredType);
}
cs

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

  주입할 객체를 모두 스프링 빈으로 등록할 필요는 없다. 객체를 스프링 빈으로 등록하지 않는다면 스프링 컨테이너가 객체를 관리하지 않는다. 따라서 스프링 컨테이너가 제공하는 기능인 자동 주입, 라이프사이클 관리, 객체 생성 등의 기능을 적용하지 않는다. 즉, 스프링 컨테이너가 제공하는 관리 기능과 getBean() 메서드를 사용할 일이 앖다면 반드시 빈 객체로 등록하지 않아도 된다.

  주입할 객체를 스프링 빈으로 사용하지 않는 예시는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class AppContextNoMemberPrinterBean {
    
    // 빈이 아닌 객체
    private MemberPrinter printer = new MemberPrinter();
    
    @Bean
    public MemberListPrinter listPrinter() {
        return new MemberListPrinter(printer);
    }
}
cs

 

출처 - 초보 웹 개발자를 위한 스프링 5 프로그래밍 입문