[김영한 스프링 review] 스프링 핵심 원리 - 기본편 (1)에서 이어지는 내용입니다.
이전에 순수한 자바 언어로 작성한 코드를 스프링 프레임워크의 도움을 받아 수정해본다.
@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 RateDiscountPolicy();
}
}
Bean을 등록하기 위해서
클래스에 @Configuration
어노테이션을 붙이고
메소드에 @Bean
어노테이션을 붙인다.
@Configuration
을 붙이지 않으면 싱글톤 빈으로 관리되지 않는다.
스프링이 위 어노테이션을 탐색하여 CGLIB을 통한 프록시 작업을 하고, Config Class를 싱글톤으로 관리하게 된다.
(후에 나오겠지만 AOP 또한 CGLIB을 이용한 프록시 기술을 제공한다.)
이전 설정(AppConfig 사용하기 참조) 에 스프링을 적용해보자.
public class OrderApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
// OrderService orderService = appConfig.orderService();
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
Order order = orderService.createOrder(findMember.memberId, "itemA", 10000);
}
}
여기서 bean의 이름은 @Bean
어노테이션을 붙인 메소드의 이름이 된다.
따라서 getBean(메소드명, 변환할 클래스) 에서 메소드명은 memberService
, orderService
가 된다.
또한, 이전에는 개발자가 작성한 AppConfig 클래스를 호출했는데,
이제는 스프링에 등록된 빈을 조회하기 위해 context.getBean 메소드를 사용한다.
스프링을 적용했는데, 코드가 이전보다 더 복잡해진것 같다.
추후에 자동 의존관계 주입을 통해 코드를 더 간결하게 해보겠다.
//스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
AnnotationConfig
ÀpplicationContext는 구현체이다.AnnotationConfig
ApplicationContext : 어노테이션 기반 자바 설정 코드 (AppConfig.class)GenericXml
ApplicationContext : XML 기반 설정 파일 (appConfig.xml)xxx
ApplicationContext : 기타 등등 환경 기반 설정 파일 (appConfig.xxx)name 속성
을 할당하여 빈 이름을 변경할 수도 있다.등록한 스프링 빈을 조회해보자.
// 스프링 컨테이너 생성
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 스프링 컨테이너에 등록된 빈들의 이름을 조회
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
// 빈 이름으로 빈에 설정된 정보들 조회
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
//Role ROLE_APPLICATION: 직접 등록한 애플리케이션 빈
//Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
// 빈 이름으로 빈 객체 조회
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name=" + beanDefinitionName + " object=" + bean);
}
}
ac.getBean(빈이름, 타입)
ac.getBean(타입)
조회 대상 스프링 빈이 없으면 예외 발생
NoSuchBeanDefinitionException: No bean named 'xxxxx' available
타입으로만 조회하면 유연성이 떨어진다.
만약 타입이 같은데 이름이 다른 빈이 2개 이상 있으면 어떻게 될까?
타입이 동일한데 이름이 다른 빈이 2개 이상 있으면, NoUniqueBeanDefinitionException
오류가 발생한다.
이런 경우에는 빈 이름을 지정해서 검색해야한다.
ac.getBeansOfType(클래스) 사용하여 클래스의 빈들을 모두 조회할 수 있다.
Map<빈 이름, 빈> 의 형태로 반환된다.
Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + " value = " + beansOfType.get(key));
}
부모 타입으로 조회하면, 자식 타입도 함께 조회한다.
그래서 최상위 객체인 Object 타입으로 조회하면, 모든 스프링 빈을 조회한다.
Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
for (String key : beansOfType.keySet()) {
// 모든 스프링 빈을 출력
System.out.println("key = " + key + " value=" + beansOfType.get(key));
}
getBean()
)하는 역할을 담당한다.public interface ApplicationContext extends
EnvironmentCapable,
ListableBeanFactory,
HierarchicalBeanFactory,
MessageSource,
ApplicationEventPublisher,
ResourcePatternResolver { ... }
스프링은 다양한 방법으로 빈을 생성하고 관리할 수 있다.
예를 들면 우리가 설정한 AppConfig.class 처럼 어노테이션 기반으로 관리할 수 있고, xml 형태로도 관리할 수 있다.
이렇게 다양한 방법을 지원하는 핵심은 BeanDefinition을 추상화하는 것이다.
이러한 추상화를 통해, 스프링 컨테이너는 구성 정보가 자바 코드인지, xml인지 알 필요 없이, BeanDefinition만 보면 된다.
(새로운 형태로 구성 정보를 설정하더라도, BeanDefinition만 구현하면 된다.)
BeanDefinition을 확인하기 위해서 getBeanDefinition()
을 사용하면 된다.
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
if (beanDefinition.getRole() == beanDefinition.ROLE_APPLICATION) {
System.out.println("beanDefinitionName" + beanDefinitionName + " beanDefinition = " + beanDefinition);
}
}
웹 어플리케이션은 많은 사용자들이 API를 호출한다.
만약 싱글톤이 아니라면, 매번 새로운 객체를 만들어서 반환한다.
-> 메모리 낭비가 심하다
싱글톤 패턴은 객체의 인스턴스가 1개만 생성되는걸 보장하는 패턴이다.
자바에서는 static 키워드를 통해 생성할 수 있다.
public class Singleton {
// 1. static 영역에 객체를 딱 1개만 생성해둔다.
private static final Singleton instance = new Singleton();
// 2. public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용한다.
public static Singleton getInstance() {
return instance;
}
// 3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
private SingletonService() {}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
외부에서는 아래와 같이 호출한다.
Singleton singleton = Singleton.getInstance();
singleton.logic();
싱글톤을 생성하는 방식에는 여러가지가 있다.
여담이지만, 신입 면접 단골질문이니, 취준생이라면 관련 지식을 익혀두자.
싱글톤을 직접 구현해서 사용하면 생기는 문제점들도 있다.
스프링이 제공해주는 싱글톤 컨테이너를 사용해보자.
스프링 컨테이너는 개발자가 별도로 싱글톤패턴을 구현하지 않아도,
빈으로 관리되는 객체를 내부적으로 싱글톤으로 관리한다.
-> 위에 있는 모든 문제점을 해결한다.
기본적으로는 빈들을 싱글톤으로 관리하지만, 설정에 따라 다르게 관리시킬 수도 있다.
객체를 재사용하기 때문에 메모리 낭비가 없다.
싱글톤은 객체 하나만을 사용하기 때문에 무상태성(stateless)
으로 설계해야한다.
특정 클라이언트를 위한 필드를 사용하면 안된다는 의미.
가급적 읽기전용으로 사용되어야 하며,
공유되는 필드 대신 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야한다.
-> 그러지 않으면 정말 큰 장애를 유발할 수 있음.
위에서도 작성했지만,
@Bean
을 싱글톤으로 관리하기 위해서는 클래스에 @Configuration
을 작성해야만 한다.
@Configuration
을 작성하지 않으면, Bean이 싱글톤으로 관리되지 않는다.
스프링은 이렇게 @Bean이 싱글톤으로 관리하는 것을 CGLIB 이라는 바이트코드 조작 라이브러리를 사용한다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
//AppConfig도 스프링 빈으로 등록된다.
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
//출력: bean = class AppConfig$$EnhancerBySpringCGLIB$$bd479d70
순수한 자바 클래스라면 bean = class AppConfig
가 출력되어야 하지만
이상한 문자열이 함께 출력된다. 이것이 CGLIB을 이용해 프록시를 적용한 것이다.
AppConfig$$EnhancerBySpringCGLIB$$bd479d70
는 AppConfig 를 상속받기 때문에, AppConfig처럼 사용할 수 있다.
CGLIB 라이브러리의 코드는 매우 복잡하지만, 결과물을 보면 AppConfig@CGLIB 클래스의 코드는 아래같은 형태라고 예상할 수 있다.
AppConfig@CGLIB 예상 코드
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
} else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환
}
}
Bean을 등록할 때마다, 매번 @Configuration
어노테이션과 @Bean
을 통해 등록하는 것은 번거로운 일이다. 또한, 그 개수가 늘어나면 설정 정보도 커지고 누락하는 상황도 발생할 수 있다.
스프링에서는 컴포넌트 스캔을 통해 설정 정보를 별도로 작성하지 않더라도 등록된 컴포넌트를 Bean으로 등록해주는 기능을 제공한다.
위 예제에서 AppConfig를 Bean으로 별도 등록하지 않고, @Configuration
만 붙여도 Bean으로 관리되는 것도 동일한 맥락이다. (후술하겠지만, @Configuration
어노테이션 안에는 @Component
라는 어노테이션이 있어서 컴포넌트로서 관리된다.)
컴포넌트를 스캔하여 빈으로 등록하기 위해서는, 구성정보로 사용할 클래스에 @ComponentScan
어노테이션을 붙여주면 된다.
@Configuration
@ComponentScan
public class AutoAppConfig { }
AutoAppConfig 클래스는 Bean을 등록해야 했던 AppConfig와 다르게, 내용이 비어있는게 맞다. AppConfig는 Bean을 직접 등록했던 반면, AutoAppConfig는 @ComponentScan
을 통해 @Component
어노테이션이 붙은 클래스를 자동으로 Bean을 등록해주기 때문이다.
이제 Bean으로 등록할 클래스에 @Component
어노테이션을 붙여 컴포넌트 스캔 대상으로 만들어준다.
@Component
public class MemoryMemberRepository implements MemberRepository { ... }
@Component
public class RateDiscountPolicy implements DiscountPolicy { ... }
@Component
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
@Autowired // Autowired 어노테이션을 통해 Bean을 자동으로 주입받을 수 있다.
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired // 2개 이상의 Bean도 주입받을 수 있다.
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
Autowired에 대한 소개는 이곳에서 확인할 수 있다.
@ComponentScan
은 @Component
가 붙은 모든 클래스를 스프링 빈으로 지정한다.
이 때, bean의 기본 이름은 클래스 명의 앞글자만 소문자로 변환한다.
즉, 위 예시에서 M
emoryMemberRepository 의 bean 기본 이름은 m
emoryMemberRepository 가 된다.
만약 기본 bean 이름이 아니라, 별도로 지정하고 싶다면 @Component(name = 새로운 bean 이름)
으로 지정할 수 있다.
의존관계 주입에서 @Autowired
는, 기본적으로 동일한 클래스를 탐색하여 주입한다. (getBean(XXX.class)와 동일) 다른 주입 방식은 뒤에서 다시 소개한다.
@ComponentScan
어노테이션은 기본적으로 해당 클래스의 패키지 및 하위 패키지에 있는 @Component
어노테이션을 탐색하여 Bean으로 등록한다.
만약 직접 스캔 시작 위치를 정하고싶다면, 아래 옵션을 사용할 수도 있다.
basePackages
: 지정한 패키지와 그 하위 패키지들을 탐색
basePackageClasses
: 지정한 클래스의 위치와 그 하위를 탐색
@ComponentScan(basePackages = "hello.core")
@ComponentScan(basePackages = {"hello.core", "hello.service"})
@ComponentScan(basePackageClasses = MemoryMemberRepository.class)
@ComponentScan(basePackageClasses = {MemoryMemberRepository.class, OrderServiceImpl.class})
하지만, 직접적으로 위치를 지정하는 것은 일부 패키지의 누락이 발생하거나 불필요한 패키지의 컴포넌트도 스캔할 수 있다. 따라서, Springboot 가 시작되는 위치에 구성정보와 함께 @ComponentScan
을 사용하는 것이 일반적이다.
Springboot 프로젝트를 생성하면 시작지점에 @SpringBootApplication
어노테이션이 붙어있는 것을 확인할 수 있는데, 이 어노테이션을 들어가보면 @ComponentScan
이 붙어있는 것을 확인할 수 있다.
(그래서 우리가 별도로 @ComponentScan을 작성하지 않더라도, 프로젝트 내에서 빈 등록을 자유롭게 할 수 있는 것이다.)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication { ... }
컴포넌트 스캔은 @Component
어노테이션을 직접적으로 작성하지 않더라도
@Component 를 포함하는 어노테이션도 탐색 대상에 포함한다.
(*어노테이션에는 상속관계가 없기 때문에, 이 기능은 자바 언어가 제공하는 기능이 아니라 스프링이 지원하는 기능이다.)
대표적인 예시는 아래와 같다.
@Controller
: 스프링 MVC 컨트롤러에서 사용
@Service
: 스프링 비즈니스 로직에서 사용
@Repository
: 스프링 데이터 접근 계층에서 사용
@Configuration
: 스프링 설정 정보에서 사용
위 어노테이션의 내부를 살펴보면, 전부 @Component
가 붙어있는 것을 알 수 있다.
추가로, 위 어노테이션은 단순히 @Component를 붙여주는것 뿐만 아니라, 추가적인 작업도 수행한다.
@Controller
: 스프링 MVC 컨트롤러로 인식
@Repository
: 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
@Configuration
: 앞서 보았듯이 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.
@Service
: 사실 @Service 는 특별한 처리를 하지 않는다. 대신 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나 라고 비즈니스 계층을 인식하는데 도움이 된다
@ComponentScan
어노테이션에 필터 옵션을 통해, 컴포넌트 스캔 대상을 예외적으로 추가하거나, 제외할 수 있다.
@ComponentScan(
includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = MemberRepository.class),
excludeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = OrderService.class)
)
FilterType 의 종류는 아래와 같다.
ANNOTATION
: 기본값, 애노테이션을 인식해서 동작한다.ASSIGNABLE_TYPE
: 지정한 타입과 자식 타입을 인식해서 동작한다.ASPECTJ
: AspectJ 패턴 사용REGEX
: 정규 표현식CUSTOM
: TypeFilter 이라는 인터페이스를 구현해서 처리지금까지 정리했던 내용을 보면, 빈 등록은
수동 (@Configuration - @Bean)과
자동 (@ComponentScan - @Component) 로 정리할 수 있다.
만약 빈 등록 과정에서 충돌이 발생하면 어떻게 될까?
스프링 시작 시 ConflictingBeanDefinitionException
예외 발생
스프링에서는 항상 구체적인게 우선권을 가진다.
따라서 이 경우에서 수동 빈 등록이 우선권을 가지며, 자동 등록된 빈을 오버라이딩 시킨다.
이러한 경우는 보통 개발자가 의도한 경우는 없기 때문에, 최근 스프링 부트에서는 수동 vs 자동 충돌에서도 에러를 발생시킨다.
Consider renaming one of the beans or enabling overriding by setting
spring.main.allow-bean-definition-overriding=true