@Controller
public class ExampleController {
private final ExampleService exampleService = new exampleService();
}
위 코드를 보면, ExampleController
는 ExampleService
를 사용하기 위해 직접 객체를 생성한다.
이 경우, ExampleService
와의 "의존 관계"가 형성되며 ExampleService
가 다른 컨트롤러에서도 필요하다면 각 컨트롤러에서 매번 new
키워드로 객체를 생성해야 한다.
즉, 각 인스턴스는 독립적으로 메모리와 자원을 소비할 것이므로 비효율적으로 볼 수 있다.
복잡한 의존성 관리와 객체 생명 주기를 어떻게 효율적으로 처리할까 ?
공용 저장 공간에서 객체들을 1번씩만 생성해서 외부에 뿌려주자 !
스프링 컨테이너(공용 저장 공간)
는 애플리케이션 내에서 사용될 객체들(스프링 빈)을 생성
하고, 이 객체들 사이의 의존 관계를 관리
합니다.
그렇다면 스프링 빈은 어떻게 생성할까요 ?
스프링 빈에 등록할 객체들을 찾기 위해 스프링은 컴포넌트 스캔을 사용합니다. 만약, 클래스에 @Component
애너테이션이 있다면 자동적으로 스프링 빈에 등록됩니다.
스프링 프레임워크에서는 @Controller
, @Service
, @Repository
등을 포함해 컴포넌트를 정의하고, 이러한 컴포넌트들은 스프링 컨테이너에 의해 관리되는 스프링 빈(Bean)
으로 등록됩니다.
예로, @Controller
라는 애너테이션을 클래스에 적용하면, 스프링이 해당 클래스를 컨트롤러로 인식하고, 스프링 컨테이너에서 자동으로 인스턴스를 생성해서 관리하는거죠.
스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록
한다. (유일하게 하나만 등록해서 공유)
즉, 같은 스프링 빈이면 모두 같은 인스턴스이다.
객체를 직접 생성해서 의존 관계를 형성하는 방법은 여러 문제점을 가지고 있다.
각 컴포넌트가 서로를 직접 생성하는 방식은
코드의 중복
,낭비적인 자원 사용
,유지보수의 어려움
을 초래한다.
설정으로 싱글톤이 아니게 설정할 수 있지만, 특별한 경우일 뿐, 대부분 싱글톤을 사용한다.
아래 코드를 통해 이해해 보자.
@Controller
public class ExampleController {
private final ExampleService exampleService;
@Autowired
public ExampleController(ExampleService exampleService) {
this.exampleService = exampleService;
}
}
위 코드에서 @Autowired
애너테이션은 생성자 주입 방식을 사용하여 ExampleService
객체를 ExampleController
에 주입한다. 이때, ExampleService
는 스프링 컨테이너에서 관리되는 스프링 빈으로 이미 등록되어 있는 상태이다.
이러한 방식은 스프링이 자동으로 ExampleService
타입의 빈을 찾아 생성자에 전달하기 때문에, 개발자는 의존 객체의 생성과 생명 주기를 직접 관리할 필요가 없어진다.
스프링 빈이 등록되는 과정은
ExampleController
-> ExampleService
-> ExampleRepository
이렇게 구성되고 ExampleService
와 ExampleRepository
는 스프링 컨테이너에 스프링 빈으로 등록되어 있다.
@Controller
Public class ExampleController {
private final ExampleService exampleService;
@Autowired
public ExampleController(ExampleService exampleService) {
this.ExampleService = exampleService;
}
}
이그젬플 컨트롤러가 생성될 때 스프링빈이 등록되어있는
이그젬플 서비스 객체를 넣어주는데 이것이 DI 의존관계 주입이다.
밖에서 넣어주는 느낌 즉, 스프링이 넣어주는 느낌
@Service
public class ExampleService {
private final ExampleRepository exampleRepository
@Autowired
public ExampleService(ExampleRepository exampleRepository) {
this.exampleRepository = exampleRepository;
}
}
ExampleController
-> ExampleService
-> ExampleRepository
즉, 이그젬플 서비스는 이그젬플 레포시토리가 필요하고,
이그젬플 컨트롤러는 이그젬플 서비스가 필요하다.
Ctrl + N을하면 클래스를 검색함과 동시에 이동한다. (네비게이터)
@Controller
, @Service
, @Repository
-> 컴포넌트 스캔 방식
@Component
를 해야하는데 그냥 @Service
를 해도 된다. 왜 그러지 ?Service안에 들어가면 @Component가 등록되어있다. 컨트롤러, 리포지토리도 동일하다. 그렇기 때문에 컴포넌트 스캔이라고 한다. 스프링이 컴포넌트 애너테이션이 있는 것들은 전부다 스프링이 객체로 생성해 스프링 컨테이너에 빈으로 등록한다.
이렇게 등록된 것들을 스프링이 @Autowired
로 연결을 해주는 것. DI 의존관계 설정(주입)
즉, @Component
애너테이션이 있으면 스프링 빈으로 자동 등록되고 @Controller
이 있으면 컴포넌트 스캔으로 컨트롤러가 스프링 빈으로 자동으로 등록된다.
스프링 빈으로 등록 코드
@Service
public class ExampleService {
private final ExampleRepository exampleRepository
@Autowired
public ExampleService(ExampleRepository exampleRepository) {
this.exampleRepository = exampleRepository;
}
}
그렇다면 아무런 클래스에 @Component
가 등록이 되냐 ? NO!
기본적으로 스프링부트가 실행되는 @SpringBootApplication
이 있는 자바 클래스의 패키지와 동일한 경로나 하위 경로에 있어야 한다.
상위 루트의 패키지나 클래스는 등록되지 않는다.(추가적으로 설정하면 가능)
@SpringBootApplication
에 들어가면 @ComponentScan
이 있다.
싱글톤 등록 기억하기. (정말 특수한 경우에만 싱글톤을 사용하지 않음)
예로, SpringConfig
라는 자바 클래스를 생성하고
@Configuration // -> 애너테이션 추가
public class SpringConfig {
@Bean
public ExampleService exampleService() {
return new ExampleService(exampleRepository);
}
@Bean
public ExampleRepository exampleRepository() {
return new ExampleRepository();
}
}
이렇게 @Autowired
없이 서로의 의존관계를 해결해준다.
이렇게 스프링 빈으로 등록하고, ExampleService()
에 리포지토리를 넣어주는 것.
그리고 컨트롤러는 어차피 스프링이 관리하기 때문에 그냥 @Autowired
로 한다.
당연히 컴포넌트 스캔 및 자동 의존관계가 편하지만 장단점이 있다.
===========================================
DI는 방법이 3가지가 있다.
1. 생성자 주입
private final MemberService memberService;
@Autowired
public ExampleController(ExampleService exampleService) {
this.exampleService = exampleService;
}
}
@Autowired
private ExampleService exampleService;
private ExampleService exampleService;
@Autowired
public void setExampleService(ExampleService exampleService) {
this.exampleService = exampleService;
}
세터의 단점은 외부에서 호출할 때 public로 열려있어야한다. 중간에 바꿔치기 할 이유가 없어도 public하게 노출이 되야함.
개발은 최대한 호출하지 않을 수 있는 메서드는 호출되지 않아야하는데
private ExampleService exampleService;
@Autowired
public void setExampleService(ExampleService exampleService) {
this.exampleService = exampleService;
memberService.setMemberRepository(); -> 호출이 된다.
}
가장 권장하는 것은 생성자 주입. 애플리케이션이 처음 실행되면 조립되듯 생성자들이 생성되고, @Autowired
로 묶인다.
의존관계는 실행 중에 동적으로 변하는 경우는 거의(아예) 없기 때문에 결론적으로 생성자 주입을 쓰자.
실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용한다.
그리고 정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다.
ㄴ> 어떤 상황 ? 만약 리포지토리 인터페이스의 구현체가 내부적인 DB를 교체할 때 내부적인 DB만 교체하고 다른 건 수정하지 않아도 되도록 !
즉 아래 코드를 보면서 이해해보자
@Configuration
public class SpringConfig {
@Bean
public ExampleService exampleService() {
return new ExampleService(exampleRepository);
}
@Bean
public ExampleRepository exampleRepository() {
// 다른 코드는 전혀 수정할 필요 없이
return new ExampleRepository(); // 이 부분만 return new DbExmapleRepository();로 바꿔주면 된다.
}
}
주의할 부분은 오토와일드를 통한 DI는 이그젬플 컨트롤러, 이그젬플 서비스 등과 같이 스프링이 관리하는 객체에서만 동작하므로 스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다.
@Service
, @Controller
, @Repository
같은 어노테이션이 있어야함. 스프링 컨테이너에 올라가야만함.
이야 너무 잘 설명 되어있습니다..