'스프링 핵심 원리 이해-예제 만들기', '객체 지향 원리 적용','스프링 컨테이너와 스프링 빈','싱글톤 컨테이너', '컴포넌트 스캔' 중 마지막 두 개를 맡아서 자료 정리를 준비했습니다
싱글톤 패턴이 적용되지 않아, 클라이언트가 서비스를 요청할 때마다 Service 객체가 매번 새로 생성이 된다면 고객 트래픽이 초당 100이 나온다면 초당 100개의 객체가 생성되고 소멸되어야할 것입니다. 이는 큰 메모리 낭비로 이어질 수 있습니다. 따라서 이를 해결하기 위해 '클래스의 인스턴스가 딱 한 개만 생성되는 것을 보장하는' 싱글톤 패턴이 사용됩니다.
java 코드의 속성을 이용하여 생성자를 private으로 선언하여 싱글톤 패턴을 만들 수도 있습니다.
public class ExService{
//1. 객체 한개 생성
private static final ExService instance = new ExService();
// 객체 조회 메서드 선언
public static ExService getInstance(){
return instance;}
// 생성자 선언
private ExService(){}
public void logic(){
System.out.prinln("싱글톤 객체 로직 호출");}}
하지만 이러한 싱글톤 패턴에도 문제점은 존재합니다.
참조
앱 안에 한 개의 인스턴스가 존재하는데, 이를 전역에서 접근할 수 있다면 이 인스턴스의 상태가 마구잡이로 변경될 수도 있습니다. 또한 싱글톤 패턴은 의존관계상 클라이언트가 구체 클래스에 의존하므로 DIP(Dependency Inversion Principle)를 위반합니다. 또한 클라이언트가 구현체에 의존해서 OCP 원칙을 위반할 가능성이 높아집니다. (싱클톤을 사용하면 구현체를 미리 생성해놓고 정적 메소드를 이용하여 불러오기 때문에) 또한 private 생성자를 가지고 있기 때문에 상속도 불가능합니다. 따라서 이는 다형성과 같은 객체 지향 특징을 적용할 수 없게 됩니다.
이러한 상황에서 스프링 컨테이너 는 이러한 싱글톤 패턴의 문제점을 해결하면서 객체 인스턴스를 한개만 생성해서 관리합니다. 스프링 컨테이너는 싱글톤 컨테이너 역할을 하고, 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리 라 합니다.
@Bean과 @Configuration을 이용하여 테스트를 해보면
위와 같이 같은 객체가 불러옴을 확인할 수 있습니다.
따라서 스프링 컨테이너를 통해 이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있는 것입니다. (스프링의 설정을 변경하여 요청할 때마다 새로운 객체를 생성해서 반환하도록 만들 수도 있습니다.)
위에서 언급했듯, 싱글톤 객체 안의 인스턴스가 전역에서 접근 가능하다면 상태가 마구잡이로 변경될 가능성이 존재합니다. 따라서 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안되고, 무상태(stateless)로 설계해야합니다. 특정 클라이언트에 의존적인 필드가 있으면 안되고, 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안되며 가급적 읽기만 가능해야합니다. 따럿 필드 대신에 지역변수, 파라미터, ThreadLocal등을 사용해야합니다.
참조
객체는 힙 또는 스택 메모리 영역에 배치시킬 수 있는데, 힙은 일반적으로 모든 쓰레드에서 접근 가능하고 스택은 thread 하나 당 만들어지는 메모리 영역으로 thread간 접근이 불가능한 것으로 알려져 있습니다.아래처럼 한 메서드 안에서 변수를 사용한다면, stack 메모리에 올라갈 것이며 이 변수의 값은 Thread에 안전한 변수로 사용될 수 있을 것입니다. signInRequest 파라미터를 받아서, email, password에 할당한 후, SignInResponse 객체를 생성해서 리턴하면 email, password 객체에 대해서는 외부에서 접근할 수 있는 방법이 없으므로 외부 thread에 안전한 변수로서 사용할 수 있습니다. 따라서 변수 공유를 위해서는 파라미터로 받아서 사용해야하며 자신의 변수를 다른 곳에서 사용하게 하기 위해서는 리턴값으로 제공해야만합니다.
public SignInResponse signIn(SignInRequest request){ String email = request.getEmail(); String password = request.getPassword(); return new SignInResponse(email, password); }
ThreadLocal이란 한 스레드 안에서 파라미터 또는 리턴 값으로 정보를 제공하는 것이 아닌 다른 방법으로 Thread 안에서의 값을 공유하는 방법을 제공해줍니다.
내부는 thread 정보를 키로 하여 값을 저장해두는 Map 구조를 가지고 있습니다. 기본 사용에는 get, set 메서드를 사용합니다.public class UserTermSerice{ public static ThreadLocal<Integer> threadLocalValue = new Trhead Local<>(); public UserTermService(Integer value){ threadLocalValue.set(value);} public volid threadLocal_1(){ System.out.println("threadLocalValue: " + threadLocalValue.get());}}
스프링 컨테이너는 싱글톤 레지스트리입니다. 이를 위해 스프링은 클래스의 바이트 코드를 조작하는 라이브러리를 사용합니다.
@Test
void confDeep(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println(bean.getClass());
로 출력해보면 클래스 정보 뒤에 ~CGLIB이 붙음을 확인할 수 있습니다. 즉, 스프링이 CGLIB이라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 스프링 빈으로 등록하는 것이라고 볼 수 있습니다. @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어지며 싱글톤이 보장되는 것입니다.
: 스프링 빈을 @Bean이나 등을 통해서 직접 나열하는 식으로 등록하게 된다면, 서비스가 커지면 커질 수록 등록해야할 빈의 개수도 늘어날 것이고 이에 따른 불편함도 증가될 것 입니다. 따라서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔 이라는 기능을 제공합니다. 또한 의존관계도 자동으로 주입하는 @Autowired
기능도 제공합니다.
컴포넌트 스캔을 사용하려면 @ComponentScan을 설정 정보에 붙여주면 됩니다. 이는 @Component 어노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록합니다. 이때 컴포넌트 스캔을 사용하면 @Configuration이 붙은 설정 정보도 자동으로 등록됩니다.
@Configuration
@ComponentScan
public class AutoAppConfig{}
@Autowired를 사용하면 생성자에서 여러 의존관계도 한 번에 주입받을 수 있습니다.
탐색할 패키지의 시작 위치를 선정해줄 수도 있습니다.
@ComponentScan(
basePackages = "hello.core",)
basePackages
를 통해 여러 시작 위치를 지정할 수도 있습니다.
basePackagesClasses
를 통해 지정한 클래스의 패키지를 탐색 시작위치로 지정할 수도 있고, 그렇지 않다면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 됩니다. 따라서, 프로젝트 메인 설정 정보는 주로 프로젝트 시작 루트 위치에 두게 됩니다.
컴포넌트 스캔 기본 대상
@Component: 컴포넌트 스캔에서 사용
@Controller : 스프링 MVC 컨트롤러에서 사용
@Service : 스프링 비즈니스 로직에서 사용
@Repository :스프링 데이터 접근 계층에서 사용
@Configuration : 스프링 설정 정보에서 사용
스프링은 애노테이션을 통해 부가 기능을 수행하기도 합니다.
@Controller : 스프링 MVC 컨트롤러 인식
@Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해줍니다.
@Configuration : 스프링 설정 정보로 인식하고 추가 처리를 해줍니다.
@Service : 특별한 처리를 해주지는 않고, 개발자들이 핵심 비즈니스 계층을 인식하는데 도움을 줍니다.
includeFilters
와 excludeFilters
로 필터를 적용할 수도 있습니다. 해당 기능을 이용하여 컴포넌트 스캔 대상을 추가로 지정하거나 제외할 대상을 지정할 수 있습니다.
예제는 다음과 같습니다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent{}
@MyIncludeComponent
public class BeanA{}
@ComponentScan(
includeFilters = @Filter(type = FilterType.ANNOTATION, classes = ~.class),
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class))
FilterType은 다섯가지 옵션을 제공합니다.
자동 빈 등록시 중복 이름이 발생하면 스프링은 예외를 발생시키고, 자동 빈 등록과 수동 빈 등록에서 중복 이름이 발생하면 수동 빈이 자동 빈을 오버라이딩 해버려, 우선권을 가지게 됩니다. 하지만 이는 많은 애매한 오류를 유발시키기 때문에 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 변경하였습니다.