[Spring] 의존주입, Dependency Injection, DI

스프링의 DI

의존이란

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

1
2
3
4
5
6
7
8
public class MemberService{
  private MemberDAO memberDao =  new MemberDAO();

  public void register(RegisterRequest req){
    Member member = memberDao.selectByEmail(req.getEmail());
    ...
  }
}

MemberService 클래스에서 MemberDAO 객체를 사용한다면 MemberService 클래스는 MemberDAO 클래스에 의존한다. 위 코드처럼 클래스 내부에서 직접 의존 객체를 생성해도 된다. 하지만 의존 주입을 사용하면 유지보수 관점에서 이득을 취할 수 있다.

의존 주입, Dependency Injection

아래는 생성자를 통해 의존하는 객체인 MemberDao를 주입받는 모습이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// MemberService.java
public class MemberService{
  private MemberDAO memberDao;

  public MemberService(MemberDao memberDao){
    this.memberDao = memberDao;
  }

  public void register(RegisterRequest req){
    Member member = memberDao.selectByEmail(req.getEmail());
    ...
  }
}

// main.java
MemberDao dao = new MemberDao();
MemberService service = new MemberService(dao);

위의 방식은 MemberService의 멤버 변수의 타입이 MemberDao에서 MemberDao를 상속받은 CachedMemberDao로 변경되어야 할 때, MemberDao를 사용하는 모든 Service에서 코드를 수정하지 않아도 된다.

1
2
3
// main.java
MemberDao dao = new CachedMemberDao(); // 유일한 변경 지점
MemberService service = new MemberService(dao);

위와 같이 실제 객체를 생성하는 곳에서만 변경을 하면, MemberDao 객체에 의존하는 모든 클래스에서 변경사항이 자동으로 반영된다.

의존 주입을 책임지는 Assembler 클래스

위에서는 main.java에서 의존객체를 생성하고 생성자에 전달을 해서 의존 주입을 진행했다. 하지만 의존 관계를 주입해주는 별도의 조립기 클래스를 사용하는 방법이 더 좋을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Assembler.java
public class Assembler{
  private MemberDao memberDao;
  private MemberService memberService;

  public Assembler(){
    memberDao = new MemberDao();
    memberService = new MemberService(memberDao);
  }

  public MemberService getMemberService(){
    return memberService;
  }
}

// main.java
Assembler assembler = new Assembler();
MemberService memberService = assembler.getMemberService();

스프링은 Assembler 클래스 역할을 수행하는 조립기(Assembler 클래스)

스프링의 Annotation을 활용하면 아래와 같이 의존 주입을 진행할 수 있다. AppContex.java에서는 의존 관계를 설정하고, main.java에서는 의존 관계를 가진 컨테이너를 생성한다.

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
28
29
30
// AppContext.java
@Configuration
public class AppContext{

  @Bean
  public MemberDao memberDao(){
    return new MemberDao();
  }

  @Bean
  public MemberService memberService(){
    // 생성자 방식을 사용해서 의존 관계 설정
    return new MemberService(memberDao());
  }
}

// main.java
public class Main(){
  private static ApplicationContext ctx = null;

  public static void main(String[] args) throws IOException{
    ctx = new AnnotationConfigApplicationConext(AppContext.class);
    ...
  }

  private static void foo(string[] args){
    MemberService memberService = ctx.getBean("memberService", MemberService.class);
    ...
  }
}

스프링이 설정 클래스를 이해하는 방식

설정 클래스를 상속한 새로운 설정 클래스를 만들어서 사용한다. 여러 개의 설정 클래스를 동시에 사용할 수도 있기 때문에 그대로 사용하지 않는다. 설정 클래스의 method들이 return하는 객체를 자료구조에 저장해두고, 이를 활용해서 싱글톤 패턴을 사용해서 객체를 제공한다.

@Autowired를 사용한 의존 자동 주입

Autowired를 사용하면 의존 관계 설정 클래스에서 의존 객체를 직접 명시하지 않아도 된다.

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// AppContext.java
@Configuration
public class AppContext{

  @Bean
  public MemberDao memberDao(){
    return new MemberDao();
  }

  @Bean
  public MemberService memberService(){
    // return new MemberService(memberDao()); // 생성자 방식을 사용해서 의존 관계 설정
    return new MemberService // 의존관계 설정을 명시하지 않아도 된다. 
  }
}

// MemberService.java
public class MemberService{
  @Autowired // @Autowired 추가
  private MemberDAO memberDao;

  public MemberService(MemberDao memberDao){
    this.memberDao = memberDao;
  }

  public void register(RegisterRequest req){
    Member member = memberDao.selectByEmail(req.getEmail());
    ...
  }
}

// main.java
// 변경사항 없음
public class Main(){
  private static ApplicationContext ctx = null;

  public static void main(String[] args) throws IOException{
    ctx = new AnnotationConfigApplicationConext(AppContext.class);
    ...
  }

  private static void foo(string[] args){
    MemberService memberService = ctx.getBean("memberService", MemberService.class);
    ...
  }
}

Method에서의 @Autowired

Bean 객체의 메서드에 @Autowired는 해당 메서드의 파라미터 타입에 해당하는 Bean 객체를 찾아서 인자로 주입한다.

파라미터의 의존 객체가 null일 수도 있는 경우

세 가지 방법이 있다.

  1. method에 붙은 @Autowired를 @Autowired(required=false)로 변경한다.
  2. method에 붙은 @Autowired는 그대로 두고, 인자를 Optional로 감싼다.
  3. method에 붙은 @Autowired는 그대로 두고, 인자를 앞에 @Nullable을 추가한다.

Annotation

이름 설명
@Configuration 해당 클래스를 스프링 설정 클래스로 지정
@Bean 해당 메서드가 생성한 객체를 스프링이 관리하는 빈 객체로 등록
@Autowired 의존 관계에 해당하는 타입의 빈을 찾아서 필드에 할당한다.
의존 관계를 설정 클래스에서 명시하지 않아도 된다.
메서드에 붙은@Autowired는 해당 메서드의 파라미터 타입에 해당하는 Bean 객체를 찾아서 인자로 주입한다.
@Qualifier @Autowired에 의해 자동 주입되는 객체가 여러 개인 경우 하나를 특정하기 위해서 사용한다.
A 타입의 객체를 DI해야하는 경우, A 타입의 객체뿐만 아니라 A타입을 상속한 AA타입의 객체도 DI의 대상이 되어 충돌이 난다.
이러한 경우 하나를 특정하기 위해서 사용한다.

Reference

  • 초보 웹 개발자를 위한 스프링 5 프로그래밍 입문, 최범균

Leave a comment