# 스프링
- 스프링은 좋은 객체지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크이다.
- 객체지향의 다양한 특성들 중 다형성을 극대화해서 이용할 수 있도록 도와준다.
- SOLID 원칙을 기억하자.
- SRP(Single Responsibility Principle): 단일 책임 원칙
- 한 클래스는 하나의 책임만 갖는다.
- 변경의 크기보다는, 변경이 있을때 파급효과가 적은 것에 집중.
- OCP(Open/Closed Principle): 개방-폐쇄 원칙
- 확장에는 열려있으나 변경에는 닫혀있어야 함
- 다형성을 활용
- LSP(Liskov Substitution Principle): 리스코프 치환 원칙
- 프로그램 객체는 프로그램 정확성을 깨지 않으면서 하위타입 인스턴스로 바꿀 수 있어야 함.
- 하위 클래스는 인터페이스 규약을 모두 지켜야한다는 의미.
- 자동차 클래스에서 액셀은 느리더라도 앞으로 가야함. 뒤로 가게 구현하면 안됨
- ISP(Interface Segregation Principle): 인터페이스 분리 원칙
- 특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.
- DIP(Dependency Inversion Principle): 의존관계 역전 원칙
- 프로그래머는 추상화에 의존해야 하며 구체화에 의존하면 안된다.
- SRP(Single Responsibility Principle): 단일 책임 원칙
- 다형성만으로는 OCP, DIP 원칙을 지킬 수 없다.
- OCP의 경우 new 키워드로 직접 생성하면, 구현체를 바꿀 때 해당 클래스 내부 코드를 수정해야 하므로 OCP를 위반한다
- DIP의 경우 new로 구현체를 직접 생성하는 순간, 해당 클래스는 인터페이스가 아닌 구현 클래스를 직접 알게 되므로 추상이 아닌 구체에 의존하게 된다.
- 스프링은 DI(Dependency Injection)으로 의존성을 주입하도록 컨테이너를 제공한다.
- 클라이언트 코드 변경이 없이 기능을 확장해준다.
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
@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);
}
}
- 위와 같이 주문생성 및 관리 객체가 있을때 저장소 정책과 할인정책이 변경되는 경우 구현체를 교체해줘야 하는데 이는 클라이언트 코드의 변경이 필요하다.
- 현재 위의 서비스 객체가 구현체를 주입하는 역할까지 하고 있기 때문에, 구현 객체를 생성하고 연결하는 책임을 갖는 객체를 별도로 생성한다.
- 이때 AppConfig가 등장한다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository);
}
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy(),
);
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
- 위와 같이 프로그램 제어의 책임을 객체 자신이 갖지않고 외부가 갖게 되는 것을 제어의 역전이라고 한다. (IoC)
- Impl 객체들은 자신의 로직을 실행하는 역할만 한다.
- 프레임워크가 내가 작성한 코드를 제어하고 대신 실행하면 그것은 프레임워크이다.
- 내가 작성한 코드가 직접 제어 흐름을 담당한다면 그것은 라이브러리이다.
의존관계 주입
- 의존관계는 정적인 클래스 의존 관계와 실행 시점에 결정되는 동적 객체 의존관계를 구분해야 한다.
OrderServiceImpl클래스는 추상화 인터페이스MemberRepository,DiscountPolicy에 의존한다.- 이 의존관계만으로는 실제로 어떤 객체가
OrderServiceImpl에 주입될지 알수없다.
- 이 의존관계만으로는 실제로 어떤 객체가
- 위의 AppConfig와 같이 객체를 생성, 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(
memberRepository()
);
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy()
);
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
- 위와 같이 스프링 기반으로 DI 컨테이너를 정의할 수 있다.
- 메인함수에서 DI 컨테이너를 생성하고 호출하는 코드는 다음과 같다.
public class MemberApp {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
// ..
}
}
ApplicationContext를 스프링 컨테이너라 한다.- 기존에는 개발자가 직접
AppConfig를 사용하여 직접 객체를 생성하고 DI를 했지만, 스프링 컨테이너를 통해 사용한다. - 스프링 컨테이너는
@Configuration이 붙은 AppConfig 클래스를 설정 정보로 사용한다.@Bean이라 적힌 메서드를 모두 호출하여 반환된 객체를 스프링 컨테이너에 등록한다.- 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
- 스프링 빈은
@Bean애노테이션이 붙은 메서드명을 스프링 빈 이름으로 사용한다.
# 스프링 컨테이너와 스프링 빈
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
ApplicationContext를 스프링 컨테이너라 하며, 인터페이스에 해당한다.- AppConfig 방식이 애노테이션 기반 자바 설정 클래스로 스프링 컨테이너를 만든것과 같다.
AnnotationConfigApplicationContext는 스프링 컨테이너 인터페이스의 구현체이다.- 스프링 컨테이너 생성 뒤에는 스프링 빈을 등록한다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(
memberRepository()
);
}
// ..
}
- Bean의 이름은 메서드이름을 사용하고, Bean 객체는 return하는 객체를 사용한다.
- Bean의 이름은 항상 다른 이름을 부여해야 한다.
- 의존관계 설정은 생성자 파라미터를 활용하여 처리한다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(
memberRepository()
);
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
- 위와 같이 의존관계를 설정하면, memberService가 memberRepository에 의존하는 관계가 된다.
- Bean을 조회하는 방법은 다음과 같다.
..applicationContext.getBean(Bean이름, 타입);..applicationContext.getBean(타입)- 조회 대상 빈이 없으면
NoSuchBeanDefinitionException예외가 발생한다.
@Component public class MemoryMemberRepository implements MemberRepository { }
@Component public class JdbcMemberRepository implements MemberRepository { }
- 위와 같이 같은 타입의 빈이 둘 이상이면
NoUniqueBeanDefinitionException타입으로만 빈을 조회할때 예외가 발생한다.- 이때는 빈의 이름까지 지정하여 조회하면 된다.
getBeansOfType(타입.class)메서드를 사용하면 파라미터에 전달된 타입을 구현한 모든 Bean들을 조회할 수 있다.
BeanFactory와 ApplicationContext
- 빈팩토리는 스프링 컨테이너의 최상위 인터페이스이다.
- 스프링 빈을 관리하고 조회하는 역할
- getBean 제공
- ApplicationContext
- 빈 팩토리 기능을 모두 상속받아 제공한다.
- 빈 관리기능에 더불어 여러 부가기능들을 제공한다.
- 환경변수, 메시지 소스를 활용한 로컬라이제이션, 파일 등의 리소스 조회, 애플리케이션 이벤트 발행 및 구독하는 기능 등
- 빈 팩토리를 직접 사용할 일은 거의 없다.
- 스프링 컨테이너는 다양한 형식의 설정 정보를 받아들일 수 있게 설계되어 있다.
- 자바 코드, XML, 그루비 등
- 스프링 설정은 스프링 컨테이너에게 빈 등록과 의존관계를 알려주는 것을 의미한다.
- AnnotationConfigApplicationContext: 자바 코드 기반
- GenericXmlApplicationContext: XML 기반
- 기타 등등
- 위와 같이 다양한 설정 형식 지정이 가능한 이유는
BeanDefinition추상화가 존재하기 때문이다.- 스프링 컨테이너는 BeanDefinition에 의존하고, 무엇으로 구현되어있는지는 모른다.
- BeanDefinition은 빈 설정 메타정보라고 한다.
- 빈 메타정보는 빈 클래스명, 싱글턴 여부, 실제 빈 사용 전까지 지연처리 여부 등에 대한 내용을 상세히 담고 있다.
AnnotationConfig..,GenericXml..등 각 형식별로 Reader가 존재하는데, 이들이 설정 정보들을 읽어들인다.AnnotatedBeanDefinitionReader,XmlBeanDefinitionReader등이 존재한다.- 이 메타정보를 스프링 컨테이너 구현체가 읽어들인다.
# 싱글톤 컨테이너
- 일반적인 웹 애플리케이션은 여러 유저들이 동시에 요청을 하게 된다.
- 이때 스프링없이 DI 컨테이너를 참조하게 되는 경우 컨테이너에 요청을 할때마다 새로 객체를 생성하게 된다.
- 이로 인한 메모리 낭비가 심해지게 된다.
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance() {
return instance;
}
private SingletonService() { }
private void logic() {
System.out.println("싱글턴 호출");
}
}
- 생성자 함수를 private으로 만들고, 해당 객체를 생성 후 static 영역의 멤버로 할당하는 구조이다.
- 직접 구현한 싱글턴에는 테스트의 어려움, 구현체에 의존하는 DIP 위반 등의 많은 문제들을 갖는다.
- 반면 스프링 컨테이너를 사용하게 되면 싱글턴 패턴 적용 없이도 빈 객체를 싱글톤으로 관리한다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
- 위 코드에서 memberService1과 2는 같은 참조값을 갖는다.
- 싱글턴은 항상 하나의 객체를 공유하기 때문에
stateless하게 설계해야 한다.- 가급적 읽기만 가능해야하며, 공유되지 않는 지역변수 및 파라미터 등만 사용해야 한다.
- 아래 코드를 보자.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(
memberRepository()
);
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy()
);
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
// ..
}
- 스프링 컨테이너는 싱글턴 레지스트리로서, 내부적으로는 스프링 빈이 싱글톤이 되도록 보장해준다.
- 하지만
memberRepository를 멤버 서비스와 오더 서비스에서 각자 new 키워드와 함께 생성자를 호출하고 있기 때문에 독립적인 객체를 호출하고 있는 것으로 보인다. - 스프링은 내부적으로 클래스 바이트코드를 조작하는 라이브러리를 사용한다.
@Configuration을 적용한AppConfig가 이를 처리해준다.AnnotationConfigApplicationContext(AppConfig.class)생성자 함수에서 파라미터로 넘긴 값AppConfig.class도 스프링 빈으로 등록된다.
name= appConfig object=hello.core.AppConfig$$SpringCGLIB$$0@64da2a7
- 실제로 AppConfig 스프링 빈을 출력해보면
..CGLIB..라는 이름으로 클래스 이름에 접미사가 복잡하게 추가로 붙는 것을 볼 수 있다. - 스프링이 런타임에
AppConfig클래스를 직접 활용하는 것이 아닌CGLIB라는 바이트코드 조작 라이브러리를 사용하여AppConfig를 상속받는 임의의 클래스를 만들게 된다. 이후 해당 클래스를 스프링 빈으로 등록하게 된다.- 해당 클래스가 싱글톤이 되도록 보장한다.
@Configuration애노테이션이 빈을 싱글턴으로 접근할 수 있게 보장해주며,@Bean애노테이션만 사용하는 경우에는 보장하지 않는다.
# 컴포넌트 스캔
- 스프링 빈 등록시 지금까지는
@Bean애노테이션 등을 사용하여 직접 스프링 빈들을 컨테이너 내에 나열했다. - 스프링에서는 설정정보 없이도 빈을 등록해주는 컴포넌트 스캔 기능을 제공한다.
- 의존관계도 자동으로 주입하는
@Autowired기능도 제공한다.
- 의존관계도 자동으로 주입하는
import static org.springframework.context.annotation.ComponentScan.*;
@Configuration
@ComponentScan(excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
}
- 컴포넌트 스캔 사용을 위해서는
@Component애노테이션을 설정 정보에 붙여준다.- 컴포넌트 스캔 사용시
@Configuration이 붙은 다른 설정 정보들도 함께 등록되는데, 이는@Configuration내부에@Component가 포함되어 있기 때문이다. - 위 코드는
Configuration이름으로 애노테이션이 붙은 대상은 컴포넌트 스캔 정보에서 제외하는 코드이다. - 보통 설정 정보를 컴포넌트 스캔 대상에서 제외하지는 않는다.
- 컴포넌트 스캔 사용시
- 아래와 같이 클래스들이 컴포넌트 스캔 대상이 되도록 애노테이션을 붙여줘야 한다.
@Component
public class MemoryMemberRepository implements MemberRepository { }
@Autowired애노테이션은 의존관계를 자동으로 주입해준다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
// ..
}
- 생성자에서 여러 의존관계도 한번에 주입 가능하다.
# 컴포넌트 스캔 동작 과정
@ComponentScan은@Component가 붙은 모든 클래스들을 스프링 빈으로 등록한다.- 빈 이름: 클래스명을 그대로 사용하되 첫글자만 소문자로 사용한다.
- 빈 이름 직접 지정: `@Component("myComponent")``
- 생성자에
@Autowired를 지정하면 스프링 컨테이너가 해당 스프링 빈을 찾아 주입해준다.- 위 예시 코드에서
MemberRepository스프링 빈을 찾아서 주입 - 기본적으로는 타입이 같은 빈을 찾아 주입한다.
- 위 예시 코드에서
- 여러 파라미터가 있더라도 직접 찾아 주입한다.
컴포넌트 탐색 위치 지정
@ComponentScan(
basePackages = "hello.core"
// basePackages = {"hello.core", "hello.service"}
)
- 모든 자바 클래스에 대해 컴포넌트 스캔을 시도하면 시간이 오래 걸리기 때문에 필요한 위치부터 탐색하도록 위치 지정이 가능하다.
- 위 코드의 경우
hello.core패키지를 비롯하여 하위 패키지 전체를 스캔한다. - 중괄호와 함께 여러 패키지 위치 지정도 가능하다.
- 위 코드의 경우
- 실제로는
basePackages지정 없이 설정정보 클래스를 프로젝트 최상단에 두는 방법이 일반적으로 사용된다.
# 컴포넌트 스캔 대상
- 컴포넌트 스캔은 컴포넌트 애노테이션 외에 다른 내용들도 스캔 대상에 포함한다.
@Component: 컴포넌트 스캔에서 사용@Controller: 스프링 MVC 컨트롤러에서 사용@Service: 스프링 비즈니스 로직에서 사용- 별도의 처리는 없고, 핵심 비즈니스 로직이 여기 있음을 개발자들이 알기 쉽게 해줌.
@Repository: 스프링 데이터 접근 계층에서 사용- 데이터 계층 예외를 스프링 예외로 변환도 해줌
@Configuration: 스프링 설정 정보에서 사용- 스프링 빈이 싱글턴을 유지하도록 추가 처리해줌
- 위 대상들은 내부적으로
@Component를 이미 포함하고 있다. - 애노테이션 자체로는 상속기능이랄게 없기 때문에 위와 같은 기능은 자바가 아닌 스프링이 자체적으로 지원하는 기능이다.
- 필터를 통해 컴포넌트 스캔 대상 필터링 조건을 추가할 수 있다.
includeFilters: 스캔 대상을 추가로 지정excludeFilters: 스캔 제외 대상을 지정@ComponentScan(includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class))
- 필터타입 옵션은 5가지가 존재한다.
ANNOTATION: 기본값, 애노테이션 인식 후 동작ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식하여 동작ASPECTJ: AspectJ 패턴 사용REGEX: 정규 표현식CUSTOM: TypeFilter 인터페이스 구현 후 처리
- 자동 빈 등록과 수동 빈 등록의 이름 충돌 시 최근 버전 스프링에서는 에러가 나도록 개선되었다.
- 자동 빈 등록끼리는 이름 충돌 시 항상 예외가 발생한다.
# 의존관계 자동 주입
- 의존관계 주입 방법은 네가지가 있다.
- 생성자를 통한 주입
- 수정자(setter)를 통한 주입
- 필드 주입
- 일반 메서드 주입
# 생성자 주입
- 생성자 호출 시점에 한번만 호출되는 것이 보장
- 불변 및 필수 의존관계 사용
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
..
- 스프링 빈의 생성자가 단 하나이면
@Autowired를 생략해도 자동으로 주입된다.
# 수정자(setter) 주입
- 필드값을 변경하는 수정자 메서드를 통해 의존관계를 주입한다.
- 선택 / 변경 가능성이 있을때 사용한다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
Autowired는 주입할 대상 빈이 컨테이너에 등록되어있지 않은 경우 오류가 발생한다.- 자바빈 프로퍼티 규약은
setXX,getXX라는 메서드를 통해 값 읽고 쓰기 작업을 처리하는 규칙을 만들었는데, 이를 자바빈 프로퍼티 규약이라 한다.- 함수명 작성시 접두사를 위와같이 붙여야한다.
- 위 규약을 지키지 않는 경우 스프링에서 수정자 주입으로 인식하지 못한다.
필드 주입, 일반 메서드 주입
- 필드 주입
- 필드에 바로 주입하는 방식이다.
- 사용하지 않는 것이 좋다.
- 일반 메서드 주입
- 일반 메서드를 통해 주입받는다.
- 여러 필드를 주입받을 수 있지만, 잘 사용하지 않는다.
롬복 라이브러리
@RequiredArgsConstructor기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
/// 생성자 정의 없이도 호출 가능
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
- 최근에는 생성자 하나만 두고
@Autowired를 생략하고 롬복 라이브러리를 적용하는 패턴을 자주 사용한다.
# 조회된 빈이 2개 이상일때
- 같은 타입의
@Component로 선언한 두 빈이 있고,@Autowired로 해당 타입을 주입받으려 하면NoUniqueBeanDefinitionException오류가 발생한다. - 같은 타입의 빈이 2개 이상일때의 문제를 해결하는 방법들이다.
@Autowired필드명 매칭@Qualifier끼리 매칭하여 빈 이름 매칭@Primary
@Autowired필드명 매칭- 타입매칭을 시도하고, 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가로 매칭한다.
- 빈이 컨테이너에 등록될때 첫글자를 소문자로 하여 등록하기 때문에 가능한 동작이다.
// 빈 이름: memoryMemberRepository
@Component
public class MemoryMemberRepository implements MemberRepository { }
// 파라미터명이 빈 이름과 일치 → 매칭 성공
@Autowired
public OrderService(MemberRepository memoryMemberRepository) { }
@Qualifier- 추가 구분자를 붙여주는 방식이다.
- 생성자 주입 시 지정한 구분자를 파라터에 명시해야 한다.
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy { }
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy { }
//..
@Autowired
public OrderServiceImpl(
MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy
) {
//..
}
@Primary- 우선순위를 정하는 방법이다.
- 여러 빈이 매칭되면
@Primary가 우선권을 갖는다. - 테스트용 DB 환경을 주입한다거나 할때 사용한다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy { }
@Component
public class FixDiscountPolicy implements DiscountPolicy { }
# 빈 생명주기 콜백
- DB 커넥션 풀, 네트워크 소켓처럼 애플리케이션 시작 시점에 연결을 해두고 종료 시점에 해제하는 작업들이 필요하다.
- 스프링에서는 빈 객체를 생성하고 의존관계 주입까지 완료되어야 해당 객체의 데이터들을 활용할 준비가 된다.
- 의존관계 주입까지 완료된 시점을 스프링에서 콜백 메서드를 통해 제공해준다.
- 초기화 시점을 알려주기도, 스프링 컨테이너가 종료되기 직전 소멸 콜백을 주기도 한다.
객체 생성과 초기화의 분리
- 생성자는 필수 정보(파라미터)를 받고 메모리 할당 후 객체를 생성하는 책임을 갖는다.
- 초기화는 생성된 값들을 활용하여 외부 커넥션을 연결하는 등의 무거운 작업들을 수행한다.
- 초기화 작업이 무겁다면 둘의 동작을 분리하는 것이 유지보수 관점에서 더 좋다.
- 스프링에서는 3가지 방법으로 Bean 생명주기 콜백을 지원한다.
- 인터페이스(initializingBean, DisposableBean)
- 설정 정보에 초기화 메서드 / 종료 메서드 지정
@PostConstruct,@PreDestroy애노테이션
# 인터페이스(InitializingBean, DisposableBean)
- InitializingBean:
afterPropertiesSet(), 초기화 지원- 빈 생성(생성자 호출) 이후, 의존관계 주입이 완료된 후에 호출됨
- DisposableBean:
destroy()- 빈 소멸 직전 콜백으로 호출
- 스프링 전용 인터페이스이므로, 초기화 및 소멸 메서드 이름을 변경할 수 없다.
- 최근에는 거의 사용하지 않음.
# 빈 등록 초기화 / 소멸 메서드 지정
- 설정 정보에
@Bean(initMethod = "init", destroyMethod = "close")와 같이 초기화 및 소멸 메서드를 직접 지정할 수 있다.
public class NetworkClient {
private String url;
public NetworkClient() {
// ..
}
public void init() {
// 직접 정의한 초기화 함수
}
public void close() {
// 직접 정의한 소멸 함수
}
}
//..
@Configuration
static class LifeCycleConfig {
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
// ..
return networkClient;
}
}
- 메서드 이름을 자유롭게 지정할 수 있고, 스프링 빈이 스프링 코드에 의존하지 않는 방식이다.
- 설정 정보를 사용하기 때문에 외부 라이브러리에도 초기화 및 종료 메서드를 적용할 수 있다.
- InitializingBean, DisposableBean을 사용하는 경우 래퍼 클래스를 중간 계층에 하나 두어 연결해줘야 함.
- @Bean(initMethod, destroyMethod)을 사용하면 래퍼 클래스 없이 외부 라이브러리의 메서드를 직접 초기화/소멸 시점에 연결할 수 있다.
destroyMethod 추론
- 대부분의 라이브러리는 close, shutdown등의 이름으로 종료 메서드를 사용한다.
- @Bean의 destroyMethod는 기본값이 추론으로 등록되어, close, shutdown과 같은 이름의 메서드를 자동으로 호출해준다.
- 스프링 빈으로 등록된 객체는 destroyMethod 지정 없이도 잘 동작한다.
# 애노테이션 @PostConstruct, @PreDestroy
public class NetworkClient {
public NetworkClient() {
System.out.println("생성자");
}
@PostConstruct
public void init() {
// ..
}
@PreDestroy
public void close() {
// ..
}
}
// ..
@Configuration
static class LifeCycleConfig {
@Bean
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
// ..
return networkClient;
}
}
- 위와 같이 사용하는 방식으로, 가장 간단하고 최신 스프링에서 권장되는 방식이다.
- 스프링 종속적 기술이 아닌 자바 표준으로 동작한다.
@Component를 붙여 직접 빈으로 자동등록이 가능한 커스텀 클래스의 경우 위의 두 애노테이션을 사용하면 된다.
# Bean 스코프
- 스프링은 다양한 스코프를 지원한다.
- 싱글톤: 기본 스코프로, 스프링 컨테이너 시작과 종료까지 유지되는 스코프
- 프로토타입: 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 스코프
- 웹 관련 스코프
- request: 웹 요청이 들어온 뒤 나갈때까지 유지되는 스코프
- session: 웹 세션이 생성되고 종료시까지 유지되는 스코프
- application: 웹 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
- 스코프는
@Scope를 통해 지정한다.
// 컴포넌트 스코프
@Scope("prototype")
@Component
public class HelloBean { }
// 빈 직접 지정 스코프
@Scope("prototype")
@Bean
PrototypeBean HelloBean() { }
# 프로토타입 스코프
- 싱글톤 스코프는 빈 조회시 컨테이너가 항상 같은 인스턴스의 빈을 반환해준다.
- 프로토타입 스코프는 컨테이너에 빈 조회 시 항상 새로운 인스턴스를 생성하여 반환해준다.
- 새로운 빈을 생성하고 DI까지 새로 처리한다.
- 프로토타입 빈은 컨테이너가 생성과 주입까지만 책임지고 이후 관리는 하지 않는다.
- 따라서 컨테이너 종료 시
@PreDestroy같은 소멸 메서드는 호출되지 않는다. - 초기화까지는 책임이 있기 때문에
@PostConstruct메서드는 호출된다.
- 따라서 컨테이너 종료 시
싱글톤 빈이 프로토타입 빈을 참조할때
- DI 컨테이너 내에서 싱글톤 빈이 프로토타입 빈을 참조하는 구조를 생각해보자.
- 클라이언트가 싱글톤 빈을 통해서만 프로토타입 빈의 로직을 수행하는 상황이다.
- 이때 싱글톤 빈을 여러번 참조한다고 프로토타입 빈이 새로 인스턴스를 매번 생성하는 것이 아니다.
- 컨테이너는 프로토타입 빈의 생성, 초기화, 의존관계 주입까지만 책임지고 이후 라이프사이클은 관리하지 않는다.
- 따라서 클라이언트가 싱글톤 빈을 여러번 조회한다고 하여 프로토타입 빈이 매번 생성되는 것이 아니라 첫 초기화 시 생성된 참조를 계속해서 보유하고 있게 된다.
- 의존관계를 주입받는 것을 DI, 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL)이라고 한다.
- 지정한 프로토타입 빈을 컨테이너에서 찾아주는 DL 기능을 ObjectProvider, ObjectFactory가 지원한다.
# ObjectFactory, ObjectProvider
@Autowired
private ObjectProvider<PrototypeBean> prototypeBean;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount(); // 자체 로직
// ..
}
- ObjectProvider의 getObject 메서드 호출시 항상 새로운 프로토타입 빈이 생성된다.
- 사실 getObject 메서드는 컨테이너에 빈을 새로 요청하는 함수로서, 싱글톤 스코프 빈을 요청하는 경우 기존 참조값을 그대로 반환해준다.
- 또한 ObjectProvider는 호출시점에 빈이 생성되므로 빈을 지연생성하고싶은 경우 사용할 수 있다.
// 1. 프로토타입 or 무거운 빈
@Component
public class HeavyBean {
public HeavyBean() {
System.out.println("HeavyBean 생성"); // 생성 시점 확인용
}
public void doSomething() { }
}
// 2. ObjectProvider로 지연생성
@Component
public class OrderService {
private final ObjectProvider<HeavyBean> heavyBeanProvider;
@Autowired
public OrderService(ObjectProvider<HeavyBean> heavyBeanProvider) {
this.heavyBeanProvider = heavyBeanProvider;
}
public void logic() {
HeavyBean bean = heavyBeanProvider.getObject(); // 여기서 HeavyBean 생성
bean.doSomething();
}
}
// 3. OrderService를 사용하는 클라이언트
@Component
public class ClientService {
private final OrderService orderService;
@Autowired
public ClientService(OrderService orderService) {
this.orderService = orderService;
}
public void doSomething() {
orderService.logic();
}
}
// 4. 컨테이너 실행
@SpringBootApplication
public class Main {
public static void main(String[] args) {
ApplicationContext ac = SpringApplication.run(Main.class, args);
// 이 시점엔 HeavyBean 아직 생성 안됨
ClientService clientService = ac.getBean(ClientService.class);
// 이 호출 시점에 HeavyBean 생성됨
clientService.doSomething();
}
}
- 스프링 애플리케이션 실행
- 컴포넌트 스캔
- 빈 생성 및 의존관계 주입
- OrderService 싱글톤 빈 생성
ObjectProvider<HeavyBean>생성 (빈 아님, 스프링 내장 객체)- OrderService에 주입
- ClientService 싱글톤 빈 생성
- OrderService 주입
- OrderService 싱글톤 빈 생성
- 컨테이너 완전히 뜸 (HeavyBean 아직 생성 안됨)
- getBean(ClientService.class) 호출
- doSomething() 호출
- OrderService.logic() 호출
- heavyBeanProvider.getObject() 호출
- 컨테이너에 HeavyBean 요청
- 프로토타입 HeavyBean 새로 생성 후 반환
- OrderService.logic() 호출
# 웹 스코프
- 웹 환경에서만 동작하는 스코프이다.
- 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다.
- 종료 메서드가 호출된다.
- 다음과 같은 종류가 있다.
- request: HTTP 요청이 들어오고 나갈때까지 유지되는 스코프. 각 요청마다 Bean 인스턴스가 생성되고 관리됨
- session: HTTP 세션과 동일한 생명주기를 갖는 스코프
- application: 서블릿 컨텍스트와 동일한 생명주기를 갖는 스코프
- websocket: 웹소켓과 동일한 생명주기를 갖는 스코프
@Component
@Scope(value = "request")
public class MyLogger {
// ..
}
@Scope에 value값을 지정해주면 된다.- 빈 소멸시점에 @PreDestroy 등의 방법을 통해 종료시 동작들을 정의할 수 있다.
- 각 스코프의 생명주기내에서 빈을 여러번 조회해도 같은 빈이 반환된다.
스코프 프록시
@Component
@RequestScope
public class MyLogger { } // HTTP 요청 있어야 생성 가능
@Service
public class OrderService {
private final MyLogger myLogger; // 컨테이너 시작 시점엔 MyLogger 없음 → 문제!
}
- 위 코드에서 myLogger는 request 스코프이므로 HTTP 요청이 실제로 와야지 빈 생성이 이루어진다.
- 반면 OrderService는 싱글톤 스코프이므로 스프링 애플리케이션 실행과 동시에 생성된다.
- OrderService 생성 시점에 MyLogger를 주입하려 하지만 아직 HTTP 요청이 없어 MyLogger 빈이 존재하지 않으므로 오류가 발생한다. (BeanCreationException)
- 이때 스코프 프록시를 활용하면 HTTP request와 상관없이 가짜 프록시 클레스를 다른 빈에 미리 주입할 수 있다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger { }
- 적용대상이 클래스이면 TARGET_CLASS, 인터페이스이면 INTERFACES를 선택하면 된다.
- CGLIB 라이브러리가 MyLogger를 상속한 가짜 프록시 객체를 생성하며, 클래스명에
$$EnhancerBySpringCGLIB$$가 붙는다. - 실제 요청이 오면 프록시 객체가 내부적으로 진짜 빈을 찾아 실제 로직을 위임한다.
- 프록시 객체는 request scope와는 관계없이 위임로직만 가지고 싱글톤처럼 동작한다.
- request 스코프 빈은 HTTP request 생명주기에 따라 생성되고 소멸한다.