스프링을 사용하여 얻는 이점 중 하나는 의존성을 자동으로 해결해 준다는 점이다.
그렇다면 스프링을 사용해서 의존성을 갖는 모든 객체를 스프링 빈으로 등록하면 더 편하게 사용할 수 있지 않을까?
보편적으로 스프링으로 웹 서버를 만들 때 스프링 빈으로 등록하는 객체들이 있다.
Controller
, Service
, Repository
등 MVC 패턴에서 사용되는 계층에 관한 객체들일 것이다.
그렇지만 이런 객체들만 스프링 빈으로 등록해야 할 이유는 없다.
왜 다른 객체들은(ex: 도메인 객체) 스프링 빈으로 등록해서 사용하지 않는 걸까?
우선 하나의 이유를 들어보자면, 스프링 빈은 기본적으로 싱글턴이다.
따라서 내부 필드에 상태가 있는 스프링 빈을 사용하게 된다면 의도치 않은 결과가 발생할 수 있다.
MyService myService1 = ac.getBean("myService", MyService.class);
myService1.setValue(4885);
MyService myService2 = ac.getBean("myService", MyService.class);
System.out.println(myService2.getValue()); // myService1에서 변경한 값이 출력된다.
그렇다면 상태가 있는 객체는 스프링 빈으로 등록하면 안 될 것 같다.
하지만 빈 스코프를 prototype으로 설정하면 새로운 인스턴스를 반환하게 할 수 있다.
즉, 상태가 공유되지 않는 스프링 빈을 생성할 수 있다.
MyService myService1 = ac.getBean("myService", MyService.class);
myService1.setValue(4885);
MyService myService2 = ac.getBean("myService", MyService.class);
System.out.println(myService2.getValue()); // myService1에서 변경한 값이 출력되지 않는다.
그렇다면 상태가 있는 객체라도 스프링 빈으로 등록할 수 있다.
다음과 같이 복잡한 의존 관계가 설정된 클래스가 있다고 가정해보자
public class Car {
private final Engine engine;
private final FuelTank fuelTank;
private final SomePart somePart;
private int position;
public Car(Engine engine, FuelTank fuelTank, SomePart somePart) {
this.engine = engine;
this.fuelTank = fuelTank;
this.somePart = somePart;
}
public void drive() {
this.position += engine.throttle();
}
public int getPosition() {
return position;
}
}
public class Engine {
private final Random random = new Random();
private final EnginePart enginePart;
public Engine(EnginePart enginePart) {
this.enginePart = enginePart;
}
public int throttle() {
return random.nextInt(5);
}
}
public class FuelTank {
private final FuelTankPart fuelTankPart;
public FuelTank(FuelTankPart fuelTankPart) {
this.fuelTankPart = fuelTankPart;
}
}
위와 같은 의존 관계가 설정된 객체를 만드려면 다음과 같이 코드를 작성해야 한다.
Car car = new Car(new Engine(new EnginePart()), new FuelTank(new FuelTankPart()), new SomePart());
하지만 스프링이 제공하는 의존성 주입을 사용하면 다음과 같다.
Car car = ac.getBean("car", Car.class);
즉, 스프링이 제공하는 강력한 의존성 주입을 제공받고, 상태가 있는 객체라도 빈 스코프를 prototype
으로 설정하여 싱글턴이 아닌 새로운 인스턴스를 생성할 수 있다!
ApplicationContext
에서 빈을 꺼내는 것 보다ObjectProvider
를 사용하면 더 편하게 빈을 꺼내올 수 있다.
따라서 스프링을 사용하면 더 이상 사용자가 직접 인스턴스를 생성하는 일은 없어 보인다.
그렇다면 모든 객체를 스프링 빈으로 등록해도 괜찮을까?
반은 맞고, 반은 틀렸다.
반이 맞은 이유는 위와 같이 복잡한 의존성을 스프링이 전부 처리해 줄 수 있어서이다.
하지만 반이 틀린 이유는 사용자가 생성자를 사용할 수 없다.
무조건 스프링이 제공하는 객체를 사용해야 한다.
다음과 같이 Car
클래스에 이름을 정의하는 name
필드가 추가됐을 때를 가정해 보자.
public class Car {
...
private String name; // 이름은 Unique해야 한다.
@Autowired
public Car(Engine engine, FuelTank fuelTank, SomePart somePart) {
this.engine = engine;
this.fuelTank = fuelTank;
this.somePart = somePart;
}
public void setName(String name) {
this.name = name;
}
...
}
public void startRacing(String name) {
Car car = carProvider.getObject();
car.setName(name); // setter를 해주지 않으면 값이 null이 된다!
car.drive();
System.out.println(car.getName() + " 이동거리: " + car.getPosition());
}
의존성이 있는 객체가 아닌 사용자의 입력 같은 예측할 수 없는 값은 스프링이 주입해 줄 수 없다.
따라서 객체의 생성이 끝나고 또 한 번 객체의 속성을 초기화해야 한다.
이럴 경우 final
키워드를 사용할 수 없으므로, 컴파일 단계에서 실수를 찾을 수 없다.
또한 스프링을 사용하지 않고 테스트 코드를 작성할 수 없다.
왜냐하면 의존성을 해결해 주는 모든 것을 스프링에 맡겼기 때문이다.
어차피 스프링 프레임워크를 바꿀 일이 없다면 테스트 코드를 작성할 때도 스프링을 사용하면 된다.
하지만 테스트를 돌릴 때 스프링을 사용하게 된다면 매우 느리게 테스트가 동작할 것이다.
이것은 FIRST 원칙 중F
를 위반한다.
따라서 모든 객체를 스프링 빈으로 등록하는 것은 불합리하다.
필요성에 따라, 어느 객체를 스프링 빈으로 등록해야 할지 적절한 판단이 필요하다.
스프링을 사용하면 복잡한 의존성이 있는 객체라도 쉽게 생성이 가능하다.
또한, 빈 스코프를 prototype
으로 설정하면 싱글턴이 아닌, 매번 새로운 객체를 받아 올 수 있다.
따라서 상태가 있는 객체라도 스프링 빈으로 등록하여 사용할 수 있다.
하지만 이 경우 객체의 생성을 모두 스프링에 맡겼기 때문에 그만큼 제약이 많아진다.
이러한 제약이 직접 객체를 생성해서 얻는 이점보다 적다면 스프링 빈으로 등록해서 사용하는 것이 바람직해 보인다.
따라서 필요성에 따라 적절한 판단으로 스프링 빈에 등록하여 사용하자.
끝