Post

스프링 의존성 주입 동작원리

스프링이 인터페이스 타입의 필드에 구현체를 자동으로 주입하는 과정을 단계별로 정리합니다.

의문에서 출발하기

컨트롤러에 private final MemberRepository memberRepository;만 작성해두면, MemoryMemberRepository가 구현체로 들어오는 걸까?

결론부터 말하면, 그렇다. 스프링이 자동으로 적절한 구현체를 찾아서 주입해준다.


동작 원리

1. 스프링 컨테이너의 빈 등록 과정

1
2
3
4
5
6
7
애플리케이션 시작
    ↓
스프링이 @Component, @Repository, @Service, @Controller 스캔
    ↓
해당 클래스들을 스프링 빈으로 등록
    ↓
의존성 주입이 필요한 곳에 자동으로 주입

2. 실제 예시 코드

MemoryMemberRepository (구현체)

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
package hello.login.domain.member;

import org.springframework.stereotype.Repository;
import java.util.*;

@Repository  // 이 애노테이션으로 스프링 빈에 등록된다
public class MemoryMemberRepository implements MemberRepository {

    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;

    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByLoginId(String loginId) {
        return store.values().stream()
                .filter(m -> m.getLoginId().equals(loginId))
                .findFirst();
    }

    // ... 기타 메서드
}

MemberController (사용하는 쪽)

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
package hello.login.web.member;

import hello.login.domain.member.Member;
import hello.login.domain.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller
@RequiredArgsConstructor  // final 필드에 대한 생성자를 자동 생성
public class MemberController {

    // 인터페이스 타입으로 선언
    private final MemberRepository memberRepository;

    // @RequiredArgsConstructor가 아래 생성자를 자동 생성해준다:
    // public MemberController(MemberRepository memberRepository) {
    //     this.memberRepository = memberRepository;
    // }

    @PostMapping("/members/add")
    public String save(@ModelAttribute Member member) {
        memberRepository.save(member);
        return "redirect:/";
    }

    @GetMapping("/members/{id}")
    public String findMember(@PathVariable Long id) {
        Member member = memberRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("회원 없음"));
        // ...
        return "member";
    }
}

3. 스프링의 자동 주입 흐름

1
2
3
4
5
6
7
8
9
10
11
1. MemberController 생성 시도
   ↓
2. 생성자에 MemberRepository 타입 파라미터 발견
   ↓
3. 스프링 컨테이너에서 MemberRepository 타입의 빈 검색
   ↓
4. MemoryMemberRepository 발견 (@Repository가 붙어있고, MemberRepository를 구현)
   ↓
5. MemoryMemberRepository 인스턴스를 생성자에 주입
   ↓
6. MemberController 생성 완료

여러 구현체가 있을 때는?

상황: 구현체가 2개 이상

1
2
3
4
5
6
7
8
9
@Repository
public class MemoryMemberRepository implements MemberRepository {
    // 메모리 구현
}

@Repository
public class JdbcMemberRepository implements MemberRepository {
    // DB 구현
}

이렇게 되면 스프링이 어떤 구현체를 주입해야 할지 판단할 수 없기 때문에 오류가 발생한다.

1
2
3
4
5
6
7
8
9
10
***************************
APPLICATION FAILED TO START
***************************

Description:

Field memberRepository in hello.login.web.member.MemberController required a single bean,
but 2 were found:
	- memoryMemberRepository: defined in file [...]
	- jdbcMemberRepository: defined in file [...]

해결 방법 1: @Primary 사용

1
2
3
4
5
6
7
8
9
10
@Repository
@Primary  // 이 구현체를 기본으로 사용
public class MemoryMemberRepository implements MemberRepository {
    // 메모리 구현
}

@Repository
public class JdbcMemberRepository implements MemberRepository {
    // DB 구현
}

@Primary가 붙은 MemoryMemberRepository가 우선적으로 주입된다.

해결 방법 2: @Qualifier 사용

1
2
3
4
5
6
7
8
9
10
11
@Repository
@Qualifier("memory")
public class MemoryMemberRepository implements MemberRepository {
    // 메모리 구현
}

@Repository
@Qualifier("jdbc")
public class JdbcMemberRepository implements MemberRepository {
    // DB 구현
}

컨트롤러에서 명시적으로 선택할 수 있다.

1
2
3
4
5
6
7
8
9
@Controller
public class MemberController {

    private final MemberRepository memberRepository;

    public MemberController(@Qualifier("memory") MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

해결 방법 3: 하나만 @Repository 붙이기

1
2
3
4
5
6
7
8
9
@Repository  // 이것만 빈으로 등록
public class MemoryMemberRepository implements MemberRepository {
    // 메모리 구현
}

// @Repository 없음 — 빈으로 등록되지 않음
public class JdbcMemberRepository implements MemberRepository {
    // DB 구현
}

가장 단순한 방법이다. 학습 단계에서는 이 방법이 적합하다.

해결 방법 4: @Profile 사용

실무에서는 환경별로 구현체를 분리하는 경우가 많다. @Profile을 사용하면 활성화된 프로파일에 따라 등록되는 빈이 달라진다.

1
2
3
4
5
6
7
8
9
10
11
@Repository
@Profile("dev")  // 개발 환경에서만 활성화
public class MemoryMemberRepository implements MemberRepository {
    // 메모리 구현
}

@Repository
@Profile("prod")  // 운영 환경에서만 활성화
public class JdbcMemberRepository implements MemberRepository {
    // DB 구현
}

application.properties에서 프로파일을 지정한다.

1
2
spring.profiles.active=dev  # 개발 시
# spring.profiles.active=prod  # 운영 시

전체 흐름 정리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 구현체에 @Repository 붙이기
@Repository
public class MemoryMemberRepository implements MemberRepository {
    // 구현...
}

// 2. 컨트롤러에서 인터페이스 타입으로 의존성 선언
@Controller
@RequiredArgsConstructor
public class MemberController {
    private final MemberRepository memberRepository;  // 인터페이스 타입

    // 3. 스프링이 자동으로 MemoryMemberRepository를 주입
}

왜 이렇게 동작하는가?

  1. @Repository — 스프링에게 “나를 빈으로 등록해달라”고 알리는 애노테이션이다. 실제로는 @Component의 특수한 형태이다.

  2. @RequiredArgsConstructor — Lombok이 final 필드에 대한 생성자를 자동으로 만들어준다. 스프링은 생성자가 하나만 있으면 @Autowired 없이도 의존성을 주입한다.

  3. 타입 기반 매칭 — 스프링은 MemberRepository 타입의 빈을 컨테이너에서 찾는다. MemoryMemberRepository가 이 인터페이스를 구현하고 있으므로 매칭에 성공한다.


로그로 확인하기

application.properties에 다음을 추가하면 주입 과정을 로그로 확인할 수 있다.

1
logging.level.org.springframework.beans=DEBUG

애플리케이션 시작 시 아래와 같은 로그가 출력된다.

1
2
3
4
Creating shared instance of singleton bean 'memoryMemberRepository'
...
Autowiring by type from bean name 'memberController'
via constructor to bean named 'memoryMemberRepository'

핵심 정리

  • 구현체가 1개만 있으면 스프링이 자동으로 찾아서 주입한다.
  • 구현체가 여러 개이면 @Primary, @Qualifier, @Profile 등으로 어떤 구현체를 사용할지 명시해야 한다.
  • 컨트롤러는 인터페이스에만 의존하기 때문에, 나중에 구현체가 바뀌어도 컨트롤러 코드를 수정할 필요가 없다.

이것이 스프링 의존성 주입(Dependency Injection)의 핵심이다.

This post is licensed under CC BY 4.0 by the author.