본문 바로가기
Java & 스프링

[Spring] 스프링의 DI를 알아보자 (Dependency Injection)

by hjhello423 2020. 12. 12.

 

DI에 대해 간략하게 정리해 보겠습니다.

 

우선 DIDependency Injection의 앞글자를 딴 말로, 의존 주입이라고 해석할 수 있습니다.

들어가기에 앞서 DI는 스프링의 핵심 개념 중 하나이지만 스프링에서만 사용하는 개념이 아니란 것을 알아 두시면 좋겠습니다.

 

아래의 모든 코드는 toy project에서 추려내고 수정을 가한 코드로, 예시를 위한 코드입니다.


의존 이란?

위에서 DI가 '의존 주입'이라고 설명했는데, 먼저 '의존'이 무엇인지 간단하게 알아보겠습니다.

의존의 사전적인 의미는 다음과 같습니다. (네이버 사전 참조)

명사, 다른 것에 의지하여 존재함.

이 의미를 객체에 적용시켜보면 어떻게 될까요?

예를 들자면 '어떤 객체 A가 B에 의지하여 존재한다.'라고 생각해 볼 수 있습니다.

간단한 예를 들어보겠습니다.

 

여기 간단한 Member class가 있습니다.

그리고 MemberService에서 Member 객체를 생성하고 DB에 등록한다고 한다면 대략 아래와 같은 코드가 나오겠죠?

(아래 코드는 제 toy project에서 일부분만 추린 코드입니다. 간단하게 예시로만 봐주세요.)

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    private String username;
    private int age;

    public Member() {
    }

}

 

@Service
@Transactional(readOnly = true)
public class MemberService {

    private Member member = new Member();

    @Transactional
    public void join() {
        save(member);
    }

}

 

위의 코드를 봤을 때 MemberService가 회원을 등록하는 로직을 수행하기 위해 Member를 사용하고 있습니다.

회원을 등록하는 로직을 수행하기 위해서 MemberService는 Member를 반드시 필요로 하며, 사용하고 있습니다.

이 관계를 class 간의 의존 관계라고 설명할 수 있으며, 'MemberService가 Member를 의존한다.'라고 표현합니다.

 


의존 주입

의존에 대해 알아보았으니 의존성 주입, DI에 대해 알아 정리해 보겠습니다.

의존 주입은 말 그대로 의존을 주입해주는 행위를 말합니다.

위의 MemberService는 의존 객체인 Member를 직접 생성하고 있습니다. 이 직접 생성 과정을 외부에서 주입해주는 행위를 말합니다.

MemberService에서 사용하는 Member를 외부에서 주입해주도록 변경하여 코드로 간단하게 살펴보겠습니다.

@Service
@Transactional(readOnly = true)
public class MemberService {

    private Member member;

    @Transactional
    public Long join() {
        save(member);
    }
	
    public void setMember(Member member) {
    	this.member = member;
    }
}

Member의 setter()를 추가하여 외부에서 member를 세팅할 수 있도록 하였습니다.

만약 join()을 실행하려 한다면 대략 아래와 같은 로직이 실행되겠죠?

@Controller
@RequireArgumentConstructor
public class MemberController {

    private final MemberService service;
    
    @PostMapping("/join")
    public void join() {
        Member member = new Member();
        service.setMember(member)
        serivce.join();
    }

}

 

이처럼 service 외부에서 member 객체를 생성 후, setter()를 이용하여  전달하고 있습니다.  
setter나 기타 방법으로 의존성 객체를 주입해주는 행의를 DI(의존 주입)이라고 합니다.

 


의존 주입을 하는 방법 3가지

먼저 정리해 보자면 아래의 3가지 방법이 있습니다.

  • 필드에 주입
  • setter를 이용한 주입
  • 생성자를 이용한 주입

필드에 주입을 하는 방법은 spring의 @Autowired를 이용한 경우에 예시가 가능하여 아래에서 설명하기로 하고, setter와 생성자를 이용한 injection에 대해 정리해 보겠습니다.

setter injection

setter를 이용한 injection은 이미 위에서 살펴보았습니다.

injection을 setter 메서드를 이용하여 처리하는 방법입니다.

아래 코드처럼 setter를 이용하여 의존 객체를 주입해 주면 됩니다.

@Service
@Transactional(readOnly = true)
public class MemberService {

    private Member member;

    @Transactional
    public Long join() {
        save(member);
    }
	
    public void setMember(Member member) {
    	this.member = member;
    }
}

constructor injection

두 번째, 생성자를 이용한 injection 방법입니다.

말 그대로 생성자를 이용하여 객체를 주입해 주면 됩니다.

@Service
@Transactional(readOnly = true)
public class MemberService {

    private Member member;

    public MemberService(Member member) {
        this.member = member
    }
    
    @Transactional
    public void join() {
        save(member);
    }

}

 


스프링의 @Autowired를 이용한 자동 의존 주입

이제 스프링의 DI에 대해 정리해 보겠습니다.

우선 스프링에서는 ApplicionContext가 관리하는 Bean으로 등록된 객체만 의존 주입이 가능합니다. (정확하게는 @Autowired를 이용할 때)

applicationContext가 관리하는 bean은 @Autowired가 선언된 type을 비교하고 bean을 자동으로 주입합니다.

생성자를 주입 방식만 사용해 정리해 보도록 하겠습니다.

다른 주입 방식에 대해선 이전에 DI를 정리한 글을 읽어보시면 더 좋을 거예요.

[스프링/기본] - [Spring] tip- 생성자 주입 방식(Construct Injection)

 

우선 Member가 bean으로 등록되어 있다고 가정해 보겠습니다.(해당 도메인을 bean으로 등록하는 건 예시를 위해서입니다.) 

bean으로 등록된 객체를 생성자에서 아래와 같이 @Autowired를 선언해 주면 스프링 컨테이너가 자동으로 해당 객체를 주입해주게 됩니다.

@Service
@Transactional(readOnly = true)
public class MemberService {

    private Member member;

    @Autowired
    public MemberService(Member member) {
        this.member = member
    }
    
    @Transactional
    public void join() {
        save(member);
    }

}

 

스프링 4.3부터는 생성자가 1개일 경우 @Autowired 생략이 가능합니다. (doc 참조)

@Autowired는 컨테이너가 관리하는 bean들을 자동으로 주입해주게 됩니다.

 


실제 bean을 주입해보자

@Autowired는 type을 비교하여 관리하는 bean을 주입해 주게 됩니다.

등록된 bean의 상태에 따른 상황별로 DI처리를 정리해 보겠습니다.

 

등록된 bean이 없을 경우

만약 주입하려는 type이 bean으로 등록되지 않았다면 어떻게 될까요?

Member만 bean으로 등록되어 있고, Team은 bean등록이 안된 상태라고 가정해 본다면

@Service
@Transactional(readOnly = true)
public class MemberService {

    private Member member;
    private Team team;

    @Autowired
    public MemberService(Member member, Team team) {
        this.member = member
        this.team = team;
    }
    
    @Transactional
    public void join() {
        save(member);
    }

}

 

위 코드 실행 시 아래와 같은 error 메시지가 나타납니다.

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 1 of constructor in toy.application.member.MemberService required a bean of type 'toy.domain.mamber.Team' that could not be found.

 

Team이라는 type을 가진 bean을 찾을 수 없다는 메시지를 보여주고 application 실행이 실패합니다.

 

만약 bean이 없더라도 application이 동작하도록 처리하려면 Optional, @Nullable을 이용하면 됩니다.

@Service
@Transactional(readOnly = true)
public class MemberService {

    private Member member;
    private Team team;

    @Autowired
    public MemberService(Member member, @Nullable Team team) {
        if (Objects.isNull(team)) {
            log.debug("team is null!!!");
        }
        
        this.member = member
        this.team = team;
    }
    
    @Transactional
    public void join() {
        save(member);
    }

}

@Nullable을 이용하여 applicaion 실행 시 이상 없이 수행되는 것을 확인할 수 있습니다.

같은 방법으로 Optional<Team> Team team과 같이 선언하여 bean의 null 처리를 할 수 있습니다.

이 외에 @Autowired(required = false)를 이용한 방법도 있습니다.

 

이때 required=false 속성을 이용할 경우 해당 bean이 없으면 메서드 자체가 실행이 안되지만

@Nullable이나 Optional을 이용할 경우 bean이 없더라도 해당 메서드가 실행된다는 차이점이 있습니다.

 

등록된 bean의 type이 여러 개 일 때

스프링은 DI를 수행할 때 type을 이용하여 injection 할 객체를 결정합니다.

그런데 만약 같은 type의 bean을 여러 개 등록한다면 어떻게 될까요?

@Configuration
public class BeanTest {

    @Bean
    public Team team1() {
        return new Team("team1");
    }

    @Bean
    //@Qualifier("team2")
    public Team team2() {
        return new Team("team2");
    }
}

Team type을 사용하는 bean 2개를 등록하였습니다.

이 상태에서 application을 실행하면 아래와 같은 error 메시지를 확인할 수 있습니다.

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 1 of constructor in toy.application.member.MemberService required a single bean, but 2 were found:
	- team1: defined by method 'team1' in class path resource [/config/BeanTest.class]
	- team2: defined by method 'team2' in class path resource [/config/BeanTest.class]

1개의 bean을 예상했는데 2개의 bean을 찾았다고 에러 메시지를 출력한 뒤 실패하게 됩니다.

이 문제를 해결 하기 이전에 등록된 bean을 모두 구분자를 가지고 있다는 개념을 이해해야 합니다.

@Bean을 이용해 bean을 등록하면 구분자를 가지게 되는데  구분자는 @Qualifier 또는 메서드 이름으로 적용됩니다.

위의 예시에서는 @Qualifier를 선언하지 않았기 때문에 메서드 이름인 'team1', 'team2'가 구분자로 적용됩니다.

그리고 이 구분자를 이용하여 spring boot에서는 injection 할 bean을 선택합니다.

@Service
@Transactional(readOnly = true)
public class MemberService {

    private Member member;
    private Team team;

    @Autowired
    public MemberService(Member member, Team team1) { //구분자를 param 이름으로 지정
        this.member = member
        this.team = team;
    }
    
    @Transactional
    public void join() {
        save(member);
    }

}

이런 만약 @Qualifier는 메서드의 param에는 선언이 불가합니다.

만약 field injection을 사용하는 경우에는 아래와 같이 적용도 가능합니다. 

@Service
@Transactional(readOnly = true)
@NoArgsConstructor
public class MemberService {

    @Autowired
    private Member member;
    @Autowired
    @Qualifier("team1")
    private Team team;
    
    @Transactional
    public void join() {
        save(member);
    }

}

 

@Primary를 이용한 방법

@Qualifier를 이용하는 법 외에 @Primary를 이용하는 방법도 존재합니다.

@Primary를 선언하게 되면 injection시에 높은 우선순위를 가지게 됩니다. (doc 참조)

다만 @Qualifier를 선언하게 되면 @Primary 보다 높은 우선순위를 가지게 됩니다. (@Qualifier > @Primary)

 


왜 의존성 주입을 사용해야 하는가

의존성 주입을 이용하면 결합도를 낮출 수 있습니다.

느슨한 결합이 가능하도록 유도할 수 있게 됩니다. 이 과정에서 OCP 원칙을 끌어낼 수 있게 됩니다.

 

스프링은 IoC를 기반으로 동작합니다.

IoC를 이용하면 로직 실행과 구현의 분리가 가능해지며, 구현 간의 전환이 쉬워집니다.

또한 컨테이너가 bean의 라이프 사이클을 관리하므로 개발자의 관리 포인트가 줄어든다는 장점이 있습니다.

이 IoC에서 사용하는 패턴이 바로 DI입니다.

 


참조

 

반응형

댓글