스프링 프레임워크에서 스프링 컨테이너를 구성하고 빈을 관리하기 위해 다양한 애노테이션들이 있다.
@Component 는 해당 클래스의 인스턴스가 컨테이너에 저장되도록 한다.
@Autowired 는 클래스를 필드로 선언을 할 때, 예를 들어 Book 의 인스턴스를 만들 때 Autowired 를 사용하면 컨테이너 안의 정보를 인스턴스에 주입할 수 있다.
이해하기가 어려운데, 예제를 통해서 알아보자.
Restaurant 라는 클래스 위에 @Component 를 붙여준다. 그러면 해당 클래스의 인스턴스가 자동 생성되어 스프링 컨테이너에 담아두게 된다.
자바에서 다형성이 있었는데, 의존성 주입도 같은 맥락이라고 할 수 있다. 객체의 역할과 구현을 분리하는데 목적이 있다. 레스토랑을 예시로 들어보자, 레스토랑에 쉐프가 필요하다고 할 때 쉐프 역할을 담당하는 사람으로 백종원과 이연복이 가능하다고 해보자. 백종원으로 하다가 이연복으로 바꾸어야 할 때 레스토랑 클레스에서 내가 필요한 쉐프를 넣어주려면 2가지 방법이 있다.
첫 번째는 내가 필요한 클래스를 직접 생성하는 방법이다. 그런데, 이렇게 하면 바꿔 쓸 때 불편하다. 두 번째는 컨테이너에서 꺼내쓰는 방법이 있다. 이렇게 하면 교체가 용이하다. 왜냐하면 첫 번째는 new 연산자를 통해서 대입했다가 바꾸려고 하면 직접 변경해야 한다. 두 번째 방시을 사용하면 autowired 로 자동주입이 되기 때문에 나는 실제로 내가 사용하고 싶은 클래스 위에다가 component Annotation 만 붙여놓으면 된다. 만약 두 번째 클래스로 바꾸고 싶으면 Component Annotation 을 달아주기만 하면 된다. 아주아주 편리하다고 할 수 있다. 첫 번째 방식은 내가 사용하는 클래스마다 들어가서 고쳐야 하는데, 두 번째 방법은 Annotation 만 바꿔주면 다 바뀌는 놀라운 효과를 얻을 수 있는 것이다. 예제를 우선 보자. 예제에 이어서 뒤이어 나오는 기능들을 다 이해해야 이해할 수 있는 내용이니 지금은 개념만 알아가도 좋다.
예제1. 레스토랑 & 쉐프 ( @Component @Autowired 의존성주입 이해하기 )
package com.example.demo.di;
import org.springframework.stereotype.Component;
// 스프링컨테이너는 @Component 가 붙은 클래스를 찾아내서 저장한다.
@Component
public class Chef {
}
package com.example.demo.di;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Restaurant {
Chef chef1 = new Chef(); // 필요한 객체를 직접 생성함
// 필요한 객체를 컨테이너에서 꺼내서 사용함.
@Autowired
Chef chef2;
public Chef getChef() {
return chef2;
}
}
package com.example.demo.di;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class RestaurantTest {
@Autowired
Restaurant restaurant;
@Autowired
Chef chef;
@Autowired
Chef chef2;
@Test
void Test() {
System.out.println("restaurant: " + restaurant);
System.out.println("chef: " + chef);
System.out.println("getChef(): " + restaurant.getChef());
}
}
코드 실행 결과를 보기 전에 재밌는 점이 하나 있는데, AutoWired 를 통해서 컨테이너에 담긴 객체를 바라보는 객체를 여러 개를 만들 수 있다는 것이다. Chef 를 보면 Autowired 를 통해서 chef1 과 chef2 를 만든 것을 볼 수 있다. 여기서 두 객체는 서로 다른 객체를 바라보는 것이 아니라 같은 객체의 주소를 갖게 된다.
출력 결과를 보면 예상한 대로 주소 값이 출력된 것을 알 수 있다. 즉, @Component 애노테이션은 스프링 컨테이너에 해당 클래스의 객체를 생성하고 @Autowired 애노테이션은 스프링 컨테이너의 객체 주소를 넣어주게 되고 이것을 우리는 '의존성 주입' 이라고 부른다.
한 가지 더 알아야 하는 점이 있는데 @Component 는 모든 클래스에서 사용하지는 않는다. 예를 들어보자, 계산기와 사람 클래스가 있을 때 계산기는 @Component 화 시켜도 되지만 사람은 @Component 화 시키면 안된다. 차이점은 계산기는 고유한 값이 있다는 것이고 사람 클래스는 고유한 값이 없다는 차이가 있습니다.
예제2. Dog 클래스와 테스트 클래스 (의존성 주입)
package com.example.demo.di;
import org.springframework.stereotype.Component;
@Component
public class Dog {
static void sound() {
System.out.println("왕왕 짖는다.");
}
}
package com.example.demo.di;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class DogTest {
@Autowired
Dog dog1;
@Test
void test() {
dog1.sound();
}
}
Test 클래스를 생성할 때 주의할 점이 있는데, 단위 테스트할 메서드를 Static 메서드를 사용하면 안된다는 것이다. 왜냐하면 static 으로 만들면 함수가 실행될 때 바로 메모리에 생성된다는 것을 의미하는데 스프링의 동작 순서를 보면 static 으로 메모리에 함수가 생성되는 것이 컨테이너에 담긴 인스턴스를 가져오는 것보다 먼저이기 때문에 의존성 주입이 사용 불가능해진다. 그런데, 재밌는 것은 반대로 @Component 를 사용한 클래스에서는 static 을 사용해도 정상적으로 동작한다는 것이다. 하지만 static 을 활용한다는 것은 클래스를 객체를 만들지 않고 사용한다는 것을 의미하기 때문에 적절하게 사용한 것은 아니라고 볼 수 있다. 아무튼 클래스를 컨테이너의 빈으로 담기 위해서는 클래스 위에 @Component 라는 Annotation 을 달아주어야 한다.
예제3. Cat 클래스와 테스트 클래스 (의존성 주입)
package com.example.demo.di;
import org.springframework.stereotype.Component;
@Component
public class Cat {
void eat() {
System.out.println("쥐를 먹는다.");
}
}
앞선 Dog 와 동일한 로직의 Cat 클래스이다. Annotation 과 관련하여 유의해야하는 점이 하나 있다. Annotation 은 항상 먼저 작성하고 나머지를 작성해야 한다는 점이다. Annotation 특성상 작성을 안하더라도 컴파일 오류가 발생하지 않는 경우가 많다. 게다가 나중에 추가하려면 까먹는 경우가 많다고 하니 먼저 작성을 하고 작업을 하는 것이 추천된다. 또한 Autowired 를 사용해 같은 Component 를 받은 경우 두 개의 참조 변수는 같은 bean 을 가리킨다는 것도 알고있도록 하자.
예제4. Cafe 클래스와 테스트 클래스 (의존성 주입)
package com.example.demo.di;
import org.springframework.stereotype.Component;
@Component
public class Manager {
@Override
public String toString() {
return "매니저입니다.";
}
}
package com.example.demo.di;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Cafe {
@Autowired
Manager manager;
}
예제5. Teacher 클래스와 테스트 클래스 (의존성 주입과 Constructor)
package com.example.demo.di;
import org.springframework.stereotype.Component;
@Component
public class Teacher {
String name;
public Teacher(String name) {
super();
this.name = name;
}
@Override
public String toString() {
// TODO Auto-generated method stub
return "Teacher : " + name;
}
}
package com.example.demo.di;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Subject {
String subjectName;
public Subject(String subjectName, Teacher teacher) {
super();
this.subjectName = subjectName;
this.teacher = teacher;
}
@Autowired
Teacher teacher;
@Override
public String toString() {
// TODO Auto-generated method stub
return teacher + this.subjectName;
}
}
이번에는 오류가 발생하고 있는 것을 볼 수 있다. 일단 컨테이너에 bean 을 생성할 때 기본적으로 Default 생성자가 사용되는데, 지금 우리가 사용하고 있는 방법을 제외하고 다른 방법을 사용하면 다른 생성자를 사용할 수 있다고 한다. 그런데 우선 의존성 주입을 사용하는 경우에는 이전에 예를 들었던 것처럼 계산기와 사람의 차이라고 볼 수 있는데, 즉 계산기는 인스턴스마다 바뀌어야 하는 것이 없다는 것이다. 그래서 생성자를 통해서 초기화를 시켜줄 일이 에당초에 없을 뿐더러 그렇게 해야할 이유도 없는 것이다. 즉, 생성자를 통해서 초기화가 필요하다면 의존성 주입을 사용할 이유가 없다는 것이다. 만약 더 공부하고 싶다면 '의존성주입 느슨한 결합'을 찾아서 공부해보자. 다른 방식의 의존성 주입에 대해서 공부해볼 수 있을 것 같다.