지난 작성에 이어 바로
DI에 대해 좀 더 자세히 다뤄보고자 한다.
(가장 헷갈리는 개념ㅠㅠ)
java에서 공통적으로 사용되는 개념인 IoC를
spring에서 구체화 시킨 것으로
객체 간의 의존 관계를 객체가 직접 생성하는 것이 아니라
외부로 부터 주입 받아 사용하는 방식을 의미한다.
DI는 객체간의 결합도를 낮춰
유지보수에 유리한 코드를 작성하기 위함
여러 방식을 통해(getter/setter 등) 의존 관계를 주입받지만,
보통 생성자 주입 방식을 사용하며 지향한다.
또한, 생성자 주입의 경우 보통
private 접근 제어자와 final keyword를 함께 사용한다
값의 변경을 원천적으로 차단하기도 하고,
값이 들어오지 않는다면, 에러가 발생하기 때문에
오류를 방지할 수 도 있다.
class A {
...
}
class B {
private A a = new A();
...
}
A와 B 서로 강하게 결합된 상태
public class A {
...
}
public class B {
private A a;
// 생성자 주입 방식
public void B(A a){
this.a = a;
}
}
A와 B가 서로 느슨하게 결합된 상태
이 때 외부에서 받는 의존 객체는
보통 private으로 선언
DI를 통해 설정된 의존 관계를 변경할 일이 없기 때문.
public class AppConfigurer {
private B bObject = new B(a());
public A a() { return new A(); }
public B b() { return bObject; }
}
위와 같이 제어 클래스에서 DI을 통해
각 객체들 간 의존관계를 연결한다.
이 지점에서 IoC가 일어나게 됨
public class A {
...
}
public class B {
private A a;
public void B(A a){
this.a = a;
}
}
import ort.springframework.context.annotation.Bean;
import ort.springframework.context.annotation.Configuration;
@Configuration
public class AppConfigurer {
@Bean
public A a() { return new A(); }
@Bean
public B b() { return new B(a());}
}
제어 클래스에서 사용되며,
Configuration annotation은
해당 클래스를 Spring Container의
구성 정보로 사용한다는 의미
Bean annotation은
Spring application이 실행 되었을 때
Bean annotaion이 붙은 메서드들을 모두 호출하여
반환된 객체들을 Spring Container에
등록하고 관리한다는 의미이다.
import org.springframework.context.ApplicationContext;
import org.springframework.context.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
// Spring Container 생성
ApplicationContext appContext = new AnnotationConfigApplicationContext(AppConfigurer.class);
// Bean 조회
A a = appContext.getBean("a", A.class);
B b = appContext.getBean("b", B.class);
}
}
ApplicationContext는 BeanFactory를 상속하기 때문에
각각 구분해서 사용하지만,
BeanFactory를 직접 사용하는 일은 별로 없기에
일반적으로 ApplicationContext를 SpringContainer라고 함
Singleton 패턴은 특정 클래스의 인스턴스가
'하나'만 생성되도록 보장되는 디자인 패턴으로
Bean의 default 패턴이기도 하다.
Unit test를 통해 진행,
cart1과 cart2를 getBean을 통해 가져오고,
두 객체를 비교
테스트 결과는 정상이고, 두 객체의 주소값 또한 같은 것을 확인할 수 있었다.
이 Singleton 패턴은
객체지향적 관점에서 볼 때 객체 간 결합도를 높이는 과정에 속하므로 지양해야하는 방법이다.
하지만, Spring Container에서 객체 인스턴스를 Singleton으로 관리하기 때문에
일반적인 Singleton패턴이 지닌 단점을 극복할 수 있다.
import org.springframework.context.ApplicationContext;
import org.springframework.context.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
// Spring Container 생성
ApplicationContext appContext = new AnnotationConfigApplicationContext(AppConfigurer.class);
// Bean 조회
A a = appContext.getBean("a", A.class);
B b = appContext.getBean("b", B.class);
...
appContext.close();
}
}
Spring Container는
ApplicationContext 객체를 생성하면서 Container를 초기화하고,
.close()를 통해 종료한다.
보통 이 .close()를 통해 Container가 종료되면,
Bean 객체들이 소멸된다.
Bean 객체들은 기본적으로 초기화 메서드와 종료 메서드가
내부적으로 존재하지만,
public class C {
public void init() {
sout("init");
}
public void close() {
sout("close")
}
}
@Configuration
public class AppConfigurer {
@Bean(initMethod = "init", destroyMethod="close")
public C testC() {
return new C();
}
}
위와 같은 방법으로 Bean객체 초기화 메서드와 종료 메서드를
사용자 정의로 지정할 수 있다.
public class C {
@PostContsturct
public void init() {
sout("init");
}
@PreDestroy
public void close() {
sout("close")
}
}
최신 Spring에서 권장하는 방법으론
@PostConstruct와 @PreDestroy Annotation활용 방법이 있다.
(Bean객체 초기화 인터페이스와 종료 인터페이스를 구현하는 방법도 있지만, 잘 사용하지 않는다.)
Bean 객체의 관리는 Default가 Singleton이라고 했었다.
Default가 있다는 뜻은 다른 옵션도 존재한다는 뜻.
sigleton, prototype, session, request, global session 등
이 존재하며, 대부분 signleton을 사용하긴 한다.
@scope annotation을 통해 scope 관리가 가능하다.
Configuration, Bean annotaion을 이용해
직접 의존 관계를 설정해줄 수 도 있지만,
Component Scan기능을 통해
Bean 객체를 Spring Container에 자동으로 등록하고,
Autowired annotation을 통해
Bean의 의존관계를 설정해줄 수 도 있다.
(그렇다고 Component와 Autowired annotation만 스캔하는 것은 아니다.)
package ...
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class AppConfigurer {
}
놀랍게도 제어 클래스는 이게 끝이다.
이 때 AppConfigurer class의 package가
Component Scan의 범위가 된다.
물론 범위 변경이 가능하다.
@ComponentScan(basePackages="~")
이제 Bean객체로 사용할 클래스로 가서
@Component annotation만 달아주고,
생성자(생성자 주입 방식이기 때문)에
@Autowired annotation을 달아주면 끝이다.
(생성자가 없거나 하나인 경우 생략가능)
ComponentScan이후, Bean 객체가 생성된 이후
Autowired annotaion을 단 메서드에 인스턴스를 파라미터로 넣어
자동으로 호출한다.
public interface A {...}
@Component
public class B implements A {...}
@Component
public class C implements A {...}
@Component
public class D {
private A a;
@Autowired
public void D(A a){
this.a = a;
}
}
만약 A라는 인터페이스를 구현한 B와 C 클래스가 있고,
생성자에서 A를 받아 Bean 객체를 사용한다면,
Spring Container 입장에선 이 A를 구현한 B인지, C인지 알 수 없기 때문에 오류가 발생하게 된다.
@Component
public class D {
@Autowired
private A a;
...
}
B와 C에 Bean 객체를 주입 하는 D 클래스에서 생성자를 지운 뒤, 필드명을 달리하는 방법이 있고,
(하지만 위와 같은 상황에선 의도대로는 안되지 않을까 싶다)
public interface A {...}
@Component
@Qualifier("b")
public class B implements A {...}
@Component
@Qualifier("c")
public class C implements A {...}
@Component
public class D {
private A a;
@Autowired
public void D(Qualifier("b") A a){
this.a = a;
}
}
Qualifier annotation을 사용해 구분하는 방법도 있으며,
Primary annotation을 통해
Bean 객체들 간 우선순위를 설정할 수 도 있다.
(Primary annotation을 사용한 Bean객체 만 사용되는거 같다.
이후 객체들은 몰루?)
후..
블로깅을 통해
이제 이해가 좀 되는거 같다.
어떻게 사용하는지, 왜 사용하는지...
Spring에서 핵심 개념 중 가장 어려운..
담에 또 봐야지