본문 바로가기

스프링 부트

스프링의 DI 방법 (생성자 주입 VS 필드 주입)

2021-04-20글

스프링의 DI 방법

DI

  • 변경에 의해 영향을 받는 관계

1. 생성자 주입(Constructor Injection)

@Service
public class StationConstructorService {
    private StationRepository stationRepository;

    public StationConstructorService(StationRepository stationRepository) {
        this.stationRepository = stationRepository;
    }

    public String sayHi() {
        return stationRepository.sayHi();
    }
}

스프링 4.3 부터는 단일 생성자인 경우 생성자에 @Authowired를 붙이지 않아도 된다.

2. 필드 주입(Field Injection)

@Service
public class StationFieldService {
    @Autowired
    private StationRepository stationRepository;

    public String sayHi() {
        return stationRepository.sayHi();
    }
}

필드에 @Autowired 어노테이션을 붙여준다.

3. 수정자 주입(Setter Injection)

@Service
public class StationSetterService {
    private StationRepository stationRepository;

    public String sayHi() {
        return stationRepository.sayHi();
    }

    @Autowired
    public void setStationRepository(StationRepository stationRepository) {
        this.stationRepository = stationRepository;
    }
}

setter를 통해 의존성을 주입하는 방법으로 setter에 @Autowired 어노테이션을 붙여준다.

필드 주입대신 생성자 주입을 권고하는 이유

인텔리제이에서 필드 주입을 사용할 경우 생성자 주입으로 변경할 것을 권고한다.
그 이유는 무엇일까?

순환 참조를 방지할 수 있다.

극단적인 예로 객체 A가 객체 B를 참조하고, 다시 객체 B가 객체 A를 참조한다고 하자.

먼저 필드 주입의 경우 순환 참조에서 어떤 문제를 일으키는지 보겠다.

@FunctionalInterface
public interface GameService {
    void gameMethod();
}
@Service
public class GameServiceImpl implements GameService {
    @Autowired
    private PieceService pieceService;

    @Override
    public void gameMethod() {
        pieceService.pieceMethod();
    }
}
@FunctionalInterface
public interface PieceService {
    void pieceMethod();
}
@Service
public class PieceServiceImpl implements PieceService {
    @Autowired
    private GameServiceImpl gameServiceImpl;

    @Override
    public void pieceMethod() {
        gameServiceImpl.gameMethod();
    }
}

테스트

위와 같이 간단한 테스를 해보았을 때, 애플리케이션 구동은 잘 되지만 서로의 메소드를 계속해서 호출하고 있기 때문에 StackOverflowError 가 발생한다.
어쨌든 순환 참조가 일어났음에도 스프링 컨테이너가 동작하는 애플리케이션 자체는 문제없이 구동된다.

그렇다면 생성자 주입의 경우는 어떠할까?

@Service
public class GameServiceImpl implements GameService {
    private final PieceServiceImpl pieceService;

    public GameServiceImpl(PieceServiceImpl pieceService) {
        this.pieceService = pieceService;
    }

    @Override
    public void gameMethod() {
        pieceService.pieceMethod();
    }
}

 

@Service
public class PieceServiceImpl implements PieceService {
    private final GameServiceImpl gameServiceImpl;

    public PieceServiceImpl(GameServiceImpl gameServiceImpl) {
        this.gameServiceImpl = gameServiceImpl;
    }

    @Override
    public void pieceMethod() {
        gameServiceImpl.gameMethod();
    }
}

 

로그로 순환참조가 일어나고 있음을 보여주면서 컨테이너가 빈들을 등록하지도 못한채, 애플리케이션 구동 자체도 실패하였다.

여기서 이런 차이점을 보이는 이유는 필드 주입과 생성자 주입은 빈을 주입하는 순서에 차이가 있기 때문이다.

필드 주입은 빈을 생성 후 어노테이션이 붙은 필드에 해당하는 빈을 찾아서 주입한다.
빈 생성이 먼저 일어나고, 필드에 대한 주입을 수행하는 것이다.

생성자 주입은 생성자로 객체를 생성하는 시점에 필요한 빈을 주입한다.
먼저 빈을 생성하지 않고, 생성자의 인자에 사용되는 빈을 찾거나 빈 팩터리에서 만드는 순서이다.

때문에 객체 생성 시점에 빈을 주입하는 생성자 주입은 순환 참조에 대한 오류를 겪을 수 있다.
순환된 참조 관계를 가지는 객체들이 생성되지 않은 시점에서 빈을 참조하기 때문이다.
이렇게 보면 어찌됐든 애플리케이션을 구동 시키는 필드 주입이 더 좋다고 생각할 수 있다.

하지만 객체의 순환 참조가 일어난다는 것은 애초에 잘못된 설계라고 할 수 있다.
때문에 오히려 생성자 주입을 사용하여 순환 참조가 되는 설계를 막을 수 있도록 하자.

Immutable

필드 주입과 수정자 주입은 해당 필드를 final로 선언할 수 없다.
즉 가변 객체로만 사용이 가능한 것이다.

하지만 생성자 주입은 필드를 final로 선언할 수 있다.
이로 인해 가변 객체로 인해 발생할 수 있는 오류를 사전에 막는다.


참고

스프링 - 생성자 주입을 사용해야 하는 이유, 필드인젝션이 좋지 않은 이유