[Spring] 09. @Autowired와 생성자 주입

[Spring] 09. @Autowired와 생성자 주입

이전 글에서는 @Controller, @Service, @Repository에 대해 살펴봤습니다. 이 어노테이션들은 Spring이 클래스를 Bean으로 등록할 수 있게 해주고, 각 클래스의 역할을 명확하게 구분해줍니다.

이번 글에서는 등록된 Bean들이 실제로 어떻게 연결되는지 알아보겠습니다. Spring에서 의존성을 주입할 때 자주 등장하는 어노테이션이 @Autowired입니다. 그리고 의존성 주입 방식 중 가장 많이 권장되는 방식이 생성자 주입입니다.

핵심은 간단합니다. @Autowired는 Spring Container 안에 있는 Bean을 찾아서 필요한 곳에 자동으로 넣어주는 역할을 합니다.


1. @Autowired란?

@Autowired는 Spring이 필요한 Bean을 자동으로 주입해주도록 요청하는 어노테이션입니다. 여기서 주입이란 필요한 객체를 외부에서 넣어주는 것을 의미합니다.

예를 들어 BoardController가 BoardService를 필요로 한다고 생각해보겠습니다.

BoardController
 → BoardService 필요

일반 Java 방식이라면 Controller 안에서 Service 객체를 직접 만들 수 있습니다.

public class BoardController {

    private BoardService boardService = new BoardService();
}

하지만 Spring에서는 이렇게 직접 객체를 생성하기보다, Spring Container가 관리하는 Bean을 주입받아 사용합니다. 이때 사용할 수 있는 어노테이션이 @Autowired입니다.

@Controller
public class BoardController {

    @Autowired
    private BoardService boardService;
}

위 코드에서 Spring은 BoardController가 BoardService를 필요로 한다는 것을 확인합니다. 그리고 Spring Container 안에 등록된 BoardService Bean을 찾아서 boardService 필드에 넣어줍니다.


2. @Autowired가 동작하려면 Bean이 필요하다

@Autowired가 동작하려면 주입받을 객체가 Spring Bean으로 등록되어 있어야 합니다. Spring Container 안에 없는 객체는 자동으로 주입할 수 없습니다.

예를 들어 BoardService를 주입받고 싶다면 BoardService가 Bean으로 등록되어 있어야 합니다.

@Service
public class BoardService {
}

위 클래스는 @Service가 붙어 있으므로 Component Scan을 통해 Bean으로 등록될 수 있습니다.

Component Scan
 → @Service 발견
 → BoardService Bean 등록
 → BoardController에 주입 가능

반대로 BoardService에 아무 어노테이션도 없다면 Spring이 Bean으로 관리하지 못할 수 있습니다.

public class BoardService {
}

이 상태에서 BoardService를 주입받으려고 하면 Spring이 해당 Bean을 찾지 못해 오류가 발생할 수 있습니다.


3. 의존성 주입 방식의 종류

Spring에서 의존성을 주입하는 방식은 대표적으로 세 가지가 있습니다.

필드 주입 필드 위에 @Autowired를 붙여 주입받는 방식
Setter 주입 setter 메서드를 통해 주입받는 방식
생성자 주입 생성자를 통해 필요한 객체를 주입받는 방식

이 중에서 처음 Spring을 공부할 때는 생성자 주입을 기준으로 익히는 것이 좋습니다. 생성자 주입은 필요한 의존성이 명확하게 보이고, 객체가 만들어질 때 반드시 필요한 값을 받을 수 있기 때문입니다.


4. 필드 주입

필드 주입은 필드 위에 @Autowired를 붙이는 방식입니다. 코드가 짧아서 처음에는 편해 보입니다.

@Controller
public class BoardController {

    @Autowired
    private BoardService boardService;
}

하지만 필드 주입은 의존성이 겉으로 잘 드러나지 않습니다. BoardController를 만들기 위해 BoardService가 반드시 필요하다는 사실이 생성자에 나타나지 않습니다.

또한 테스트 코드를 작성할 때 직접 객체를 생성하고 의존성을 넣기 불편할 수 있습니다. 그래서 실무에서는 필드 주입보다 생성자 주입을 더 많이 사용합니다.


5. Setter 주입

Setter 주입은 setter 메서드를 통해 의존성을 주입받는 방식입니다.

@Controller
public class BoardController {

    private BoardService boardService;

    @Autowired
    public void setBoardService(BoardService boardService) {
        this.boardService = boardService;
    }
}

Setter 주입은 객체가 생성된 후에 의존성을 넣을 수 있습니다. 필요에 따라 의존성을 나중에 바꿀 수 있다는 장점이 있습니다.

하지만 반드시 필요한 의존성이라면 Setter 주입보다 생성자 주입이 더 안전합니다. Setter 주입은 의존성이 주입되지 않은 상태의 객체가 만들어질 가능성을 열어둘 수 있기 때문입니다.


6. 생성자 주입

생성자 주입은 생성자를 통해 필요한 객체를 전달받는 방식입니다. Spring에서 가장 많이 권장되는 방식입니다.

@Controller
public class BoardController {

    private final BoardService boardService;

    @Autowired
    public BoardController(BoardService boardService) {
        this.boardService = boardService;
    }
}

위 코드에서 BoardController는 BoardService를 직접 생성하지 않습니다. 생성자를 통해 BoardService를 전달받습니다.

Spring은 BoardController Bean을 만들 때 생성자를 확인합니다. 그리고 생성자에 필요한 BoardService Bean을 찾아 넣어줍니다.

1. BoardService Bean 생성
2. BoardController 생성 시 BoardService 필요 확인
3. Spring Container에서 BoardService Bean 찾기
4. 생성자에 BoardService 주입
5. BoardController Bean 생성 완료

이 흐름이 생성자 주입입니다.


7. 생성자가 하나라면 @Autowired를 생략할 수 있다

Spring에서는 생성자가 하나만 있는 경우 @Autowired를 생략할 수 있습니다. 그래서 실제 코드에서는 다음과 같이 작성하는 경우가 많습니다.

@Controller
public class BoardController {

    private final BoardService boardService;

    public BoardController(BoardService boardService) {
        this.boardService = boardService;
    }
}

생성자 위에 @Autowired가 없지만 Spring은 생성자가 하나라는 것을 보고 자동으로 의존성을 주입할 수 있습니다.

처음 공부할 때는 @Autowired를 붙여서 흐름을 이해하고, 익숙해지면 생성자 하나일 때는 생략 가능하다고 정리하면 좋습니다.


8. 생성자 주입을 권장하는 이유

생성자 주입을 권장하는 이유는 의존성이 명확하고, 객체를 더 안전하게 만들 수 있기 때문입니다.

의존성 명확 생성자만 봐도 어떤 객체가 필요한지 알 수 있습니다.
final 사용 가능 한 번 주입된 의존성이 중간에 바뀌지 않도록 만들 수 있습니다.
누락 방지 필요한 의존성이 없으면 객체 생성 자체가 어렵습니다.
테스트 용이 테스트할 때 필요한 객체를 직접 생성자에 넣기 쉽습니다.
순환 참조 파악 객체들이 서로 물고 있는 구조를 더 빨리 확인할 수 있습니다.

특히 final을 사용할 수 있다는 점이 중요합니다.

private final BoardService boardService;

이렇게 작성하면 BoardController가 생성될 때 BoardService가 반드시 들어와야 합니다. 그리고 한 번 들어온 값은 중간에 다른 값으로 바뀌지 않습니다.


9. Controller, Service, Mapper 연결 예시

게시판 목록 조회 기능을 기준으로 생성자 주입 흐름을 보겠습니다.

@Controller
public class BoardController {

    private final BoardService boardService;

    public BoardController(BoardService boardService) {
        this.boardService = boardService;
    }

    @GetMapping("/board/list")
    public String list(Model model) {
        List<BoardDto> boardList = boardService.findBoardList();
        model.addAttribute("boardList", boardList);
        return "board/list";
    }
}
@Service
public class BoardService {

    private final BoardMapper boardMapper;

    public BoardService(BoardMapper boardMapper) {
        this.boardMapper = boardMapper;
    }

    public List<BoardDto> findBoardList() {
        return boardMapper.findAll();
    }
}
@Mapper
public interface BoardMapper {

    List<BoardDto> findAll();
}

이 구조에서 BoardController는 BoardService를 주입받고, BoardService는 BoardMapper를 주입받습니다. 각 클래스는 필요한 객체를 직접 만들지 않습니다. Spring이 Bean을 찾아 생성자에 넣어줍니다.

BoardController
 → BoardService 주입

BoardService
 → BoardMapper 주입

BoardMapper
 → DB 쿼리 실행

이렇게 연결하면 Controller, Service, Mapper가 각자 자기 역할에 집중할 수 있습니다.


10. Lombok의 @RequiredArgsConstructor

생성자 주입을 사용하다 보면 생성자 코드가 반복됩니다. 이때 Lombok을 사용하면 생성자 코드를 줄일 수 있습니다.

@Controller
@RequiredArgsConstructor
public class BoardController {

    private final BoardService boardService;

    @GetMapping("/board/list")
    public String list(Model model) {
        List<BoardDto> boardList = boardService.findBoardList();
        model.addAttribute("boardList", boardList);
        return "board/list";
    }
}

@RequiredArgsConstructorfinal이 붙은 필드를 대상으로 생성자를 자동으로 만들어줍니다. 즉, 아래 생성자를 직접 작성하지 않아도 됩니다.

public BoardController(BoardService boardService) {
    this.boardService = boardService;
}

그래서 Spring Boot 프로젝트에서는 생성자 주입과 Lombok을 함께 사용하는 경우가 많습니다.

생성자 주입 기본 형태
 → 직접 생성자 작성

실무에서 자주 보는 형태
 → final 필드 + @RequiredArgsConstructor

다만 처음 공부할 때는 Lombok 없이 생성자를 직접 작성해보는 것이 좋습니다. 그래야 Spring이 어떤 방식으로 Bean을 주입하는지 더 정확히 이해할 수 있습니다.


11. @Autowired가 실패하는 경우

@Autowired가 항상 성공하는 것은 아닙니다. Spring이 주입할 Bean을 찾지 못하거나, 반대로 같은 타입의 Bean이 여러 개 있으면 문제가 생길 수 있습니다.

1) Bean이 없는 경우

@Controller
public class BoardController {

    private final BoardService boardService;

    public BoardController(BoardService boardService) {
        this.boardService = boardService;
    }
}

위 코드에서 BoardService를 주입받으려면 BoardService가 Bean으로 등록되어 있어야 합니다. 그런데 BoardService에 @Service가 없거나 Component Scan 범위 밖에 있으면 Spring이 Bean을 찾지 못할 수 있습니다.

원인
 → @Service 없음
 → Component Scan 범위 밖
 → 설정 누락
 → Mapper 등록 누락

2) 같은 타입의 Bean이 여러 개인 경우

같은 타입의 Bean이 여러 개 있으면 Spring은 어떤 Bean을 주입해야 할지 판단하지 못할 수 있습니다.

public interface PayService {
}
@Service
public class KakaoPayService implements PayService {
}
@Service
public class NaverPayService implements PayService {
}

이 상태에서 PayService를 주입받으면 Spring 입장에서는 KakaoPayService를 넣어야 할지, NaverPayService를 넣어야 할지 애매할 수 있습니다.

@Service
public class OrderService {

    private final PayService payService;

    public OrderService(PayService payService) {
        this.payService = payService;
    }
}

이런 경우에는 @Qualifier@Primary 같은 방법으로 어떤 Bean을 사용할지 지정할 수 있습니다. 이 내용은 다음 글에서 더 자세히 정리하겠습니다.


12. 자주 하는 실수

@Autowired를 사용할 때 자주 하는 실수는 Spring이 관리하지 않는 클래스에 주입하려고 하는 것입니다.

public class BoardUtil {

    @Autowired
    private BoardService boardService;
}

위 클래스가 Bean으로 등록되어 있지 않다면 Spring이 BoardUtil을 관리하지 않습니다. Spring이 관리하지 않는 객체 안에서는 의존성 주입도 정상적으로 동작하지 않을 수 있습니다.

즉, 의존성 주입을 받는 클래스도 Spring Bean이어야 합니다.

주입하는 객체
 → Bean이어야 함

주입받는 객체
 → 보통 Bean이어야 함

또 다른 실수는 DTO나 VO에 @Autowired를 사용하려는 것입니다. DTO와 VO는 보통 데이터를 담는 객체입니다. Spring Container가 계속 관리하는 Bean이 아니므로, 일반적으로 의존성 주입 대상이 아닙니다.

좋은 사용 위치
 → Controller
 → Service
 → Repository
 → Mapper
 → 설정 클래스

피해야 할 사용 위치
 → DTO
 → VO
 → 단순 데이터 객체

13. 정리

@Autowired는 Spring Container 안에 있는 Bean을 찾아 필요한 곳에 자동으로 주입해주는 어노테이션입니다. 의존성 주입 방식에는 필드 주입, Setter 주입, 생성자 주입이 있습니다.

필드 주입 코드는 짧지만 의존성이 잘 드러나지 않습니다.
Setter 주입 선택적인 의존성을 주입할 때 사용할 수 있습니다.
생성자 주입 필수 의존성을 명확하게 표현할 수 있어 가장 많이 권장됩니다.

생성자 주입을 사용하면 필요한 의존성이 명확해지고, final을 사용할 수 있으며, 테스트하기 좋은 구조를 만들 수 있습니다. 생성자가 하나뿐이라면 @Autowired를 생략해도 Spring이 자동으로 의존성을 주입할 수 있습니다.

Spring에서 중요한 흐름은 다음과 같습니다.

Component Scan
 → Bean 등록
 → 생성자 확인
 → 필요한 Bean 탐색
 → 의존성 주입
 → 객체 사용

이 흐름을 이해하면 Spring이 객체를 어떻게 연결하는지 훨씬 명확해집니다.


연습해보기

아래 구조를 생성자 주입 방식으로 작성해보세요.

MemberController
 → MemberService
 → MemberMapper

각 클래스의 역할은 다음처럼 나눌 수 있습니다.

MemberController 회원 관련 요청을 받습니다.
MemberService 회원가입, 로그인 같은 기능 흐름을 처리합니다.
MemberMapper 회원 정보를 DB에서 조회하거나 저장합니다.

다음 글에서는 같은 타입의 Bean이 여러 개 있을 때 사용하는 @Qualifier@Primary에 대해 정리하겠습니다.