이번에는 스프링의 기능들에 대해 중점적으로 학습할 예정이다.
컴포넌트 스캔
@ComponentScan을 붙여줘야 한다
(@Configuration도 컴포넌트 스캔이 되기 때문에 이번 예제에서는 AppConfig를 제외시키는 코드를 작성했다.)
@Component 에노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다.
@Component만 붙였을 때 의존관계를 주입할 방법이 사라지는데, 따라서 자동 의존 관계 주입이 필요하다.
@Configuration
@ComponentScan(
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {
//지금 아무것도 없음
}
Component
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
@Autowired //자동 의존 관계 주입
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
- 스프링이 @Component가 붙은 클래스들을 찾아서 스프링 빈으로 등록한다.
- 의존관계 자동 주입을 사용한다 -> 타입을 가지고 탐색하여 주입한다.
- 컴포넌트 스캔의 탐색 위치와 대상
- basePakages 사용하여 탐색 시작 위치 지정
- 파라미터로 여러 개 넣어서 시작 위치 여러 개 지정 가능
- basePakageClasses 로 클래스만 지정 가능
@ComponentScan(
basePackages = "hello.core.member", //여기서부터 찾게 스캔하도록 한다.
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
//예제를 위해 Configuration은 제외한다는 코드
)
- 지정하지 않는다 => @ComponentScan 이 작성된 설정 정보 클래스의 패키지가 시작 위치이다.
- 권장 방법: 프로젝트 루트에 AppConfig 같은 설정 정보 클래스를 주고, @ComponentScan을 붙여서 작성한다!
스프링 부트를 사용하면 프로젝트 시작 위치에 @springBootApplication을 두는 것이 관례이다.
- 스캔 기본 대상
- `@Component` : 컴포넌트 스캔에서 사용
`@Controller` : 스프링 MVC 컨트롤러에서 사용
`@Service` : 스프링 비즈니스 로직에서 사용
`@Repository` : 스프링 데이터 접근 계층에서 사용
`@Configuration` : 스프링 설정 정보에서 사용 - 위 에노테이션들로 스프링은 부가 기능을 수행하기도 한다.
- `@Component` : 컴포넌트 스캔에서 사용
`@Controller` : 스프링 MVC 컨트롤러로 인식
`@Service` : 특별한 처리 없음. 개발자들이 비즈니스 계층 로직을 인식할 수 있게함.
`@Repository` : 스프링 데이터 접근 계층으로 인식. 데이터 계층의 예외를 스프링 예외
`@Configuration` : 스프링 설정 정보에서 사용. 싱글톤 유지하도록 추가 처리.
- `@Component` : 컴포넌트 스캔에서 사용
- `@Component` : 컴포넌트 스캔에서 사용
- 컴포넌트 스캔의 필터 옵션
- includeFilters
- 컴포넌트 스캔에 추가할 대상을 지정
- excludeFilters
- 컴포넌트 스캔에서 제외할 대상을 지정
- 최근 스프링부트는 컴포넌트 스캔을 기본적으로 제공하기 때문에 기본으로 사용하자!
- includeFilters
- 중복 등록과 충돌
- 자동 빈 등록 vs 자동 빈 등록
- 컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우
- ConflictingBeanDefinitionException 예외가 터진다. -> 오류를 발생시킨다.
- 수동 빈 등록 vs 자동 빈 등록
- 수동 빈이 우선권을 가진다.
- 수동 빈이 자동 빈을 오버라이딩한다.
- 이렇게 되면 잡기 어려운 버그가 생길 가능성이 크다. 따라서 최근 스프링부트에서는
수동 빈과 자동 빈이 충돌하면 오류를 내도록 기본적으로 설정하였다.
- 자동 빈 등록 vs 자동 빈 등록
의존관계 주입
생성자 주입 / setter 주입 / 필드 주입 / 일반 메서드 주입
- 생성자 주입
- 생성자를 호출하는 시점에 딱 한 번만 호출하는 게 보장된다.
- 불변, 필수 의존관계에 사용한다.
- 객체를 변경하지 않는 설계에 용이
- 생성자가 하나 있으면 @Autowired 에노테이션을 생략해도 된다!
- 수정자 주입
- set함수를 만들어서 의존관계를 주입한다.
- 선택, 변경 관계에 있는 의존관계에 사용한다.
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
System.out.println("memberRepository = " + memberRepository);
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
System.out.println("discountPolicy = " + discountPolicy);
this.discountPolicy = discountPolicy;
}
- 자바 빈 프로퍼티
- 필드의 값을 직접 변경하지 않고, setXxx, getXxx 라는 메서드를 통해서 값을 읽거나 수정하는 규칙
- 필드 주입
- 클래스의 필드 부분에 @Autowired를 붙여 사용한다.
- 되도록 사용하지 않는 것이 좋다!
@Component
public class OrderServiceImpl implements OrderService {
@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;
- 일반 메서드 주입
- 일반 메서드를 통해 주입 받을 수 있어서 한 번에 여러 필드를 주입 받을 수 있다.
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
결론적으로 자동 의존관계 주입은 스프링 컨테이너가 관리하는 스프링 빈이여야만
@Autowired를 사용할 수 있다.
옵션 처리
주입할 스프링 빈이 없는 경우에도 동작해야 할 경우가 있다.
@Autowired만 사용하면 required: true 로 되어있어 주입 대상이 없으면 오류가 발생한다.
static class TestBean{
@Autowired(required = false)
public void setNoBean1(Member noBean1){ //Member는 스프링 빈이 아니다
System.out.println("noBean1 = " + noBean1);
}
@Autowired
public void setNoBean2(@Nullable Member noBean2){ //스프링이 제공하는 Nullable을 사용해야 함!
System.out.println("noBean2 = " + noBean2);
}
@Autowired
public void setNoBean3(Optional<Member> noBean3){
System.out.println("noBean3 = " + noBean3);
}
}
1번에서 required = false이면 메서드가 호출이 되지 않는다!
생성자 주입
- 불변
- 객체를 생성할 때 한 번만 호출되기 때문에 불변하게 설계 가능하다.
- 누락
- 프레임워크 없이 순수 자바 코드를 테스트하는 경우에
수정자 의존 관계라면 의존관계 주입이 누락될 가능성이 있다. - 생성자 주입을 사용하면 컴파일 오류를 통해 누락된 상황을 쉽게 인지할 수 있다.
- final 키워드를 사용할 수 있다! => 생성자 코드가 누락된 경우, final코드가 있으면
오류를 통해 발견할 수 있다.
- 프레임워크 없이 순수 자바 코드를 테스트하는 경우에
- 항상 생성자 주입 방식을 사용하고 수정자 주입 방식을 옵션으로 부여한다.
Lombok 라이브러리
@Getter / @Setter 같은 어노테이션을 작성하면 이 라이브러리가 자동으로 get/set 메서드를 만들어준다!
@Getter
@Setter
public class HelloLombok {
private String name;
private int age;
public static void main(String[] args) {
HelloLombok helloLombok = new HelloLombok();
helloLombok.setName("asdfs");
String name = helloLombok.getName();
System.out.println("name = " + name);
}
}
toString(), wait() 등 종류가 많다.
- 활용
- @RequiredArgsConstructor
- 생성자를 자동으로 만들어준다. => 필드에 final 이 있으면 값이 필수적으로 필요하기 때문에
이를 보고 롬복이 만든다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
코드가 상당히 줄어들었다!
임의의 의존관계가 추가될 때 굉장히 용이하다.
조회할 빈이 2개 이상일 때 @Autowired
@Autowired는 타입으로 조회하기 때문에
ac.getBean(DiscountPolicy.class); 이런 것과 유사하다.
따라서, 이름만 다르고 완전히 같은 타입의 스프링 빈 2개가 있을 때 해결이 되지 않는다.
- @Autowired에 필드명 매칭
- 처음엔 타입 매칭을 시도하고, 여러 빈이 조회되면 파라미터 이름으로 빈을 추가한다.
- 필드 명, 파라미터 명으로 빈 이름을 매칭한다.
- @Qualifier 사용
- 추가 구분자
- 주입 시 추가적인 방법을 제공한다.
- @Qualifier를 작성하고 옆에 작성된 이름을 찾는다.
- 그러나 @Qualifier는 @Qualifier를 찾는 용도로만 사용하는 것이 좋다.
- @Primary
- 우선순위를 지정하는 방법이다.
- @Autowired 시에 @Primary가 있으면 우선권을 가진다.
- 깔끔하게 사용할 수 있어서 자주 사용한다.
우선순위
`@Primary` 는 기본값 처럼 동작하는 것이고,
`@Qualifier` 는 매우 상세하게 동작한다.
스프링은 자동보다는 수동이, 넒은 범위의 선택권 보다는 좁은 범위의 선택권이 우선 순위가 높다.
따라서 여기서도 `@Qualifier` 가 우선권이 높다.
에노테이션 직접 만들기
package hello.core.annotation;
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
이런 식으로 작성하면 @Qualifier 안에 문자열로 생긴 오류를 잡지 못하다가
에노테이션을 만들면 컴파일 오류를 사용할 수 있다.
조회한 빈이 모두 필요할 때
static class DiscountService{
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
생성자 주입을 통해 모든 DiscountPolicy 타입을 주입받는다. 현재 rate와 fix discount policy가 있다!
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member, price);
이런 다형성 메서드를 유지하면서 사용이 가능하다.
int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
메서드의 discountCode로 rate 또는 fix discountPolicy가 넘어오면 Map에서 해당하는 스프링 빈을 찾아서 실행할 수 있다!
- Map: 스프링 빈의 이름이 키 값이고, 조회한 타입의 모든 스프링 빈을 가진다.
- List: 모든 스프링 빈의 인스턴스를 가진다.
결론
- 편리한 자동 등록과 자동 의존 관계 주입을 적극 활용
- 기술 지원 객체는 수동 등록 ex) DataSource,,
- 다형성을 사용하는 비즈니스 로직은 수동 등록도 고려해보아야 함! => 코드를 한 눈에 보기 쉽다.
<빈 생명주기>
데이터베이스 커넥션 풀, 네트워크 소켓처럼 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을
스프링이 제공한다 == 객체의 초기화와 종료 작업
스프링 빈은 객체 생성 -> 의존관계 주입 순으로 실행된다.
의존관계 주입이 다 끝난 후에 사용할 수 있는 데이터가 된다.
그렇다면 의존 관계 주입이 언제 완료되었는지 알 수 있을까?
스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해 초기화 시점을 알려주는
다양한 기능을 제공한다.
- 초기화 콜백: 빈 생성되고, 빈의 의존관계 주입이 완료된 후에 호출된다.
- 소멸전 콜백: 빈이 소멸되기 전에 호출된다.
객체의 생성과 초기화를 분리한다
생성자는 메모리를 할당해서 객체를 생성한다
초기화는 외부 커넥션을 연결하는 역할을 한다.
- 인터페이스로 초기화, 소멸전 콜백 받는 법
- InitializingBean, DisposableBean 인터페이스를 가져와서 구현하는 방법으로 사용한다.
- 인터페이스가 스프링 전용 인터페이스라 요즘에는 거의 사용하지 않는다.
public class NetworkClient implements InitializingBean, DisposableBean {
private String url;
public NetworkClient(){
System.out.println("생성자 호출, url = " + url);
}
public void setUrl(String url){
this.url = url;
}
//서비스 시작 시 호출
public void connect(){
System.out.println("connect: " + url);
}
public void call(String message){
System.out.println("call: " + url + " message = " + message);
}
//서비스 종료 시 호출
public void disconnect(){
System.out.println("close: " + url);
}
@Override
public void afterPropertiesSet() throws Exception { //의존관계 주입 후
System.out.println("NetworkClient.afterPropertiesSet");
connect();
call("초기화 연결 메시지");
}
@Override
public void destroy() throws Exception { //소멸 전
System.out.println("NetworkClient.destroy");
disconnect();
}
}
- 소멸 메서드 지정
- 메서드 이름을 자유롭게 할 수 있고, 스프링에 대해 의존하지 않는다.
- 외부 라이브러리에서도 초기화, 종료 메서드를 적용할 수 있다.
- @Bean의 `destroyMethod` 는 기본값이 `(inferred)` (추론)으로 등록되어 있다.
- `close` , `shutdown` 라는 이름의 메서드를 자동으로 호출해준다.
이름 그대로 종료 메서드를 추론해서 호출해준다.
@Bean(initMethod = "init", destroyMethod = "close")
- 에노테이션 지정
- @PostConstructor, @PreDestroy -> 스프링에서도 권장하는 방식
- jakarta.annotation에 있는 자바 표준이다!
- 외부 라이브러리에는 적용하지 못한다!
@PostConstruct
public void init() throws Exception {
System.out.println("NetworkClient.init");
connect();
call("초기화 연결 메시지");
}
@PreDestroy
public void close() throws Exception {
System.out.println("NetworkClient.close");
disconnect();
}
빈 스코프
스코프는 빈이 존재할 수 있는 범위를 뜻한다.
지금까지 스프링 컨테이너가 종료될 때 까지 유지된다고 학습했는데 그 이유는
스프링 빈은 기본적으로 싱글톤 스코프를 가지기 때문이다.
- 다양한 스코프 지원
- 싱글톤: 기본 스코프, 시작부터 종료까지 유지되는 가장 넓은 범위의 스코프
- 프로토타입: 빈의 생성, 의존관계 주입까지만 관여, 더는 관리하지 않는다.
- 웹 관련
- request: 웹 요청이 들어오고 나갈 때
- session: 웹 세션이 생성되고 종료될 때까지 유지
- application: 웹의 서블릿 컨텍스와 같은 범위로 유지
public class PrototypeTest {
@Test
void prototypeBeanFind(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
System.out.println("find prototypeBean1");
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
System.out.println("find prototypeBean2");
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
System.out.println("prototypeBean1 = " + prototypeBean1);
System.out.println("prototypeBean2 = " + prototypeBean2);
Assertions.assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
ac.close();
}
@Scope("prototype")
static class PrototypeBean{
@PostConstruct
public void init(){
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void destroy(){
System.out.println("PrototypeBean.destroy");
}
}
}
프로토타입 스코프는 스프링 컨테이너에서 스프링 빈을 조회할 때 생성되고 초기화 메서드가 실행된다.
종료 메소드는 실행되지 않는다.
따라서 위 예제는 다른 스프링 빈 2개가 생성된다.
- 프로토타입 스코프
- 스프링 컨테이너에 요청할 때 마다 새로 생성
- 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입, 초기화까지만 관여한다.
- 종료 메서드가 호출되지 않는다.
- 따라서 프로토타입 빈은 이 빈을 조회한 클라이언트가 관리해야 한다. 종료 메서드에 대한 호출도 직접 해야한다.
- 싱글톤에서 프로토타입 빈을 사용하는 경우
- 생성 시점에 프로토타입을 주입 받고 그것이 유지된다.
- 그러나, 프로토타입 스코프의 용도로 봤을 때 이런 경우보다 사용할 때 마다
새로 생성해서 사용하는 것을 원할 것이다.
@Test
void singletonClientUsePrototype(){
AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic();
assertThat(count1).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic();
assertThat(count2).isEqualTo(2);
}
@Scope("singleton")
static class ClientBean{
private final PrototypeBean prototypeBean; //생성 시점에 주입
@Autowired
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic(){
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
@Scope("prototype")
static class PrototypeBean{
private int count = 0;
public void addCount(){
count++;
}
public int getCount(){
return count;
}
카운트가 1이였다가 2로 증가한다.
프로토타입 스코프가 유지된다는 것을 알 수 있다.
- 웹 스코프
- request요청이 들어와서 나갈 때 까지 유지되고, 스프링이 관리한다.
- HTTP request 요청마다 각각 할당되는 웹 스코프이다.
package hello.core.common;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private String requestURL;
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
public void log(String message) {
System.out.println("["+uuid+"]"+ "[" + requestURL+"]" + message);
}
@PostConstruct
public void init(){
uuid = UUID.randomUUID().toString();
System.out.println("["+uuid+"] request scope bean create: " + this);
}
@PreDestroy
public void close(){
System.out.println("["+uuid+"] request scope bean close: " + this);
}
}
웹 스코프로 설정한 MyLogger를 활용하여 컨트롤러를 만들어서 웹 화면을 출력하고자 한다.
package hello.core.web;
import hello.core.common.MyLogger;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request){
String requestURL = request.getRequestURL().toString();
System.out.println("myLogger = " + myLogger.getClass());
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
우선 이렇게 되면 스코프가 활성화되지 않았다는 오류가 발생한다.
그 이유는 웹 스코프는 요청이 들어와야 빈이생성되는데
실제 고객의 아무런 요청이 없기 때문이다. 따라서 Provider로 해결할 수 있다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
logic을 실행하는 서비스 계층에도 Provider를 설정해주었다.
ObjectProvider 덕분에 `ObjectProvider.getObject()` 를 호출하는 시점까지
request scope 빈의 생성을 지연할 수 있다.
- 스코프와 프록시
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
private String uuid;
private String requestURL;
이렇게 proxyMode를 설정해주는 방법이 있다.
적용 대상이 구체 클래스이면 TARGET_CLASS
적용 대상이 인터페이스면 INTERFACES 로 작성한다.
이렇게 작성하면 코드를 Provider 사용 이전 상태에서 사용할 수 있다.
아까 작성했던 코드임에도 오류가 나지 않고 동작한다!
package hello.core.web;
import hello.core.common.MyLogger;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request){
String requestURL = request.getRequestURL().toString();
System.out.println("myLogger = " + myLogger.getClass());
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
- 프록시 모드를 사용하면 CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 주입한다.
- 이러한 가짜 프록시 객체는 요청이 오면 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
- 따라서 특정 객체 조회가 필요한 시점까지 지연처리가 가능하다!
- 이런 특별한 scope는 꼭 필요한 곳에만 사용하자
지금까지 스프링의 핵심 원리와 기술들에 대해 학습해보았다.
워낙 방대해서 모든 것을 알 수 없지만, 우선 배우는 자세로 임하였다.
그리고 이를 통해 직접 서버를 구축해보고 잘 모르는 것이 있으면
찾아가며 학습할 예정이다.
다음엔 HTTP 통신에 대해 정리하고 넘어갈 예정이다!
파이팅!