Spring Boot 의존성 주입(Dependency Injection) 완벽 가이드
의존성 주입이란?
의존성 주입(Dependency Injection)은 객체가 필요로 하는 다른 객체를 직접 생성하지 않고 외부에서 주입받는 설계 패턴이다. Spring Boot는 이를 프레임워크 차원에서 자동으로 처리하여 개발자가 객체 간의 결합도를 낮추고 유연한 코드를 작성할 수 있도록 지원한다.
전통적인 자바 개발 방식에서는 객체가 필요한 의존 객체를 new 키워드로 직접 생성한다. 이는 두 객체 간의 강한 결합을 만들어 코드 변경 시 연쇄적인 수정이 발생하고 테스트가 어려워지는 문제를 야기한다.
기존 방식의 문제점
다음은 의존성 주입을 사용하지 않은 전통적인 코드 예시이다.
@Controller
public class MemberController {
private MemberService memberService;
public MemberController() {
// MemberController가 MemberService를 직접 생성
this.memberService = new MemberServiceImpl();
}
@PostMapping("login.me")
public String login(Member member) {
Member loginMember = memberService.getMemberById(member.getMemberId());
return "index";
}
}
이 코드는 다음과 같은 문제를 가진다:
- 강한 결합: MemberController가 MemberServiceImpl의 구체적인 구현에 강하게 결합되어 있다
- 확장성 부족: 서비스 로직을 변경하려면 MemberController 내부 코드를 직접 수정해야 한다
- 테스트 어려움: 테스트 시 실제 데이터베이스 접근이 발생하여 단위 테스트가 불가능하다
Spring Boot의 의존성 주입 방식
Spring Boot는 의존성 주입을 통해 이러한 문제를 해결한다.
@Controller
public class MemberController {
private final MemberService memberService;
// 생성자를 통한 의존성 주입
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
@PostMapping("login.me")
public String login(Member member) {
Member loginMember = memberService.getMemberById(member.getMemberId());
return "index";
}
}
@Service
public class MemberServiceImpl implements MemberService {
private final MemberMapper memberMapper;
@Autowired
public MemberServiceImpl(MemberMapper memberMapper) {
this.memberMapper = memberMapper;
}
@Override
public Member getMemberById(String memberId) {
return memberMapper.getMemberById(memberId);
}
}
실행 결과:
Member{memberNo=0, memberId='user01', memberPwd='pass01', memberName='홍길동', ...}
Spring Boot 컨테이너가 MemberService와 MemberMapper 객체를 생성하고 각각의 생성자에 자동으로 주입한다. 개발자는 new 키워드 없이 필요한 객체를 사용할 수 있으며, 결합도가 낮아져 코드 변경과 테스트가 용이해진다.
의존성 주입의 세 가지 방법
1. 생성자 주입 (권장)
생성자 주입은 객체 생성 시점에 의존성을 주입받는 방식으로, Spring Boot에서 가장 권장되는 방법이다.
@Controller
public class MemberController {
private final MemberService memberService;
// 생성자 주입 (Spring 4.3부터 @Autowired 생략 가능)
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
@PostMapping("login.me")
public String login(Member member) {
Member loginMember = memberService.getMemberById(member.getMemberId());
return "index";
}
}
장점:
- 객체의 불변성을 보장한다
final키워드를 사용할 수 있어 의존성이 변경되지 않음을 컴파일 타임에 보장- 순환 참조 문제를 애플리케이션 시작 시점에 발견할 수 있다
2. 필드 주입 (비권장)
필드 주입은 @Autowired 어노테이션을 필드에 직접 적용하는 방식이다.
@Service
public class MemberServiceImpl implements MemberService {
@Autowired
private MemberMapper memberMapper;
@Override
public Member getMemberById(String memberId) {
return memberMapper.getMemberById(memberId);
}
}
단점:
- 테스트 시 의존성을 주입하기 어렵다
- 불변성을 보장할 수 없다
- 순환 참조가 발생해도 런타임까지 발견되지 않는다
3. Setter 주입
Setter 주입은 Setter 메서드를 통해 의존성을 주입받는 방식이다.
@Service
public class MemberServiceImpl implements MemberService {
private MemberMapper memberMapper;
@Autowired
public void setMemberMapper(MemberMapper memberMapper) {
this.memberMapper = memberMapper;
}
@Override
public Member getMemberById(String memberId) {
return memberMapper.getMemberById(memberId);
}
}
특징:
- 선택적 의존성이나 재구성이 필요한 경우에 적합
- 객체가 불완전한 상태로 생성될 수 있어 일반적인 경우에는 생성자 주입을 사용하는 것이 바람직
의존성 주입이 제공하는 실무적 이점
1. 테스트 용이성 향상
의존성 주입을 사용하면 실제 구현체 대신 테스트용 Mock 객체를 주입하여 단위 테스트를 수행할 수 있다.
@SpringBootTest
class MemberControllerTest {
@MockBean
private MemberService memberService;
@Autowired
private MemberController memberController;
@Test
void 로그인_테스트() {
// Given
Member member = new Member();
member.setMemberId("user01");
member.setMemberPwd("pass01");
Member expectedMember = new Member();
expectedMember.setMemberId("user01");
expectedMember.setMemberName("홍길동");
when(memberService.getMemberById("user01")).thenReturn(expectedMember);
// When
String result = memberController.login(member);
// Then
assertEquals("index", result);
verify(memberService, times(1)).getMemberById("user01");
}
}
실제 데이터베이스 접근 없이도 로그인 처리 로직만을 독립적으로 테스트할 수 있다. 이는 테스트 속도를 향상시키고 외부 시스템에 대한 의존성을 제거한다.
2. 코드 변경의 유연성
인터페이스 기반의 의존성 주입을 사용하면 구현체를 쉽게 교체할 수 있다.
public interface MemberService {
Member getMemberById(String memberId);
}
@Service
@Profile("database")
public class MemberServiceImpl implements MemberService {
private final MemberMapper memberMapper;
@Autowired
public MemberServiceImpl(MemberMapper memberMapper) {
this.memberMapper = memberMapper;
}
@Override
public Member getMemberById(String memberId) {
return memberMapper.getMemberById(memberId);
}
}
@Service
@Profile("cache")
public class CachedMemberService implements MemberService {
private final Map<String, Member> memberCache = new HashMap<>();
@Override
public Member getMemberById(String memberId) {
return memberCache.getOrDefault(memberId, new Member());
}
}
프로필 설정만 변경하면 전체 데이터 접근 방식을 손쉽게 전환할 수 있다. MemberController의 코드는 전혀 수정할 필요가 없으며, 새로운 데이터 접근 방식 추가도 기존 코드에 영향을 주지 않는다.
3. MyBatis Mapper의 자동 의존성 주입
@Mapper
public interface MemberMapper {
Member getMemberById(@Param("memberId") String memberId);
}
MyBatis는 @Mapper 어노테이션을 통해 인터페이스를 자동으로 Spring Bean으로 등록한다. 개발자는 복잡한 JDBC 코드 없이 인터페이스만 정의하면 MyBatis가 동적 프록시를 통해 구현체를 자동 생성하고 의존성 주입을 처리한다.
결론
이러한 의존성 주입 패턴을 통해 Spring Boot 애플리케이션은 높은 결합도에서 낮은 결합도로, 강한 의존성에서 약한 의존성으로 전환되어 유지보수성과 확장성이 크게 향상된다.
핵심 포인트
- 생성자 주입을 우선적으로 사용
- 인터페이스 기반 설계로 유연성을 확보
- 테스트 용이성을 고려한 설계
- Spring의 자동 의존성 주입 기능을 적극 활용
'Spring' 카테고리의 다른 글
| Spring_06)Spring Boot 웹 애플리케이션 개발 가이드 (0) | 2025.10.31 |
|---|---|
| Spring_05) Spring Boot BCrypt 비밀번호 암호화 (0) | 2025.10.29 |
| Spring_04) Aspect-Oriented Programming (0) | 2025.10.28 |
| Spring_03) Filter와 Interceptor (0) | 2025.10.23 |
| Spring_02) JSP → MyBatis → Spring 계층별 구조 MVC패턴의 변화 (0) | 2025.10.22 |