스프링 핵심 원리 - 기본편(정리) 1편

해당 글은 김영한님의 스프링 핵심 원리를 완강하고 난 후, 정리를 하기 위한 글로써 전체적인 내용을 포함하고 있는 것이 아닌 개인적으로 다시 한번 더 정리한 내용들만 포함하고 있습니다. 강의를 듣고자 하시는 분들은 아래 링크를 타시고 수강 하시면 되겠습니다. 

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com


# MemoryDB

설계 초기에 프로토타입으로 데이터를 입력하고자 할 때 Map 자료구조를 활용하여 데이터를 처리하는 것이 가능합니다. 

 

HashMap은 동시성 이슈가 발생 할 수 있으므로 ConcurrentHashMap 으로 통한 Map 인터페이스 구현을 추천합니다. 

public class MemoryMemberRepository implements MemberRepository {
     private static Map<Long, Member> store = new HashMap<>();
     
     @Override
     public void save(Member member) {
     	store.put(member.getId(), member);
     }
     
     @Override
     public Member findById(Long memberId) {
     	return store.get(memberId);
     }
}

# OCP와 DIP

OCP 개방 폐쇄 원칙과 DIP 의존성 주입 원칙을 준수 하기 위해서는 최대한 인터페이스를 활용하여 역할과 구현 구분하여야 합니다. 

 

어떠한 정책이나 방식 등이 시시때때로 변경이 될 수 있는 상황이 자주 있으므로 이러한 객체지향프로그래밍 설계를 지켜주면서 개발을 하면 최대한 이러한 문제를 회피할 수 있습니다. 

설계 초기 정책이 지정되는 과정


# 관심사의 분리

모든 역할을 좀 더 구분하였다고 하였을 때, 해당 강의에서는 배역과 공연 기획자를 기준으로 설명하였습니다. 전체적인 연기는 배역들이 수행을 하지만 해당 배역에 있는 연기자는 언제든지 변경이 가능하다는 것 입니다. 이것이 DIP 원리에 적합한 과정이라고 설명합니다. 

 

그렇다면 공연 기획자는 어떻게 spring에서 구현을 하고 있는지 설명을 하자면 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();
     }
}

객체 생성 및 의존성 주입 과정

AppConfig가 등장하게 됨으로서 어플리케이션을 크게 사용하는 영역 그리고 객체를 생성하고 구성 하는 영역으로 분리되었음을 알 수 있습니다.


# IoC, DI, Container

IOC(Inversion Of Control)

IoC(제어의 역전, Inversion of Control) 는 객체지향 프로그래밍에서 일반적으로 사용되는 개념으로, 객체 생성과 의존성 관리를 개발자가 아닌 컨테이너가 담당하는 패턴을 말합니다.

기존에는 개발자가 직접 객체를 생성하고 의존성을 관리해야 했으나, IoC 컨테이너가 등장하면서 개발자는 객체 생성 및 의존성 관리를 컨테이너에 위임할 수 있게 되었습니다. 이를 통해 코드의 재사용성과 유지보수성이 향상되고, 개발자는 비즈니스 로직에 집중할 수 있게 됩니다.

 

DI(Dependency Injection)

DI(Dependency Injection) 는 의존성 주입이라는 뜻으로, 객체지향 프로그래밍에서 객체간의 의존 관계를 느슨하게 하기 위한 방법 중 하나입니다.

객체 지향 프로그래밍에서 클래스들은 상호작용을 하면서 서로 의존성이 생길 수 있습니다. 이 의존성이 생기면 클래스들 간의 결합도가 높아지게 됩니다. 따라서 하나의 클래스를 수정하면, 의존하는 클래스들에서도 수정이 필요하게 됩니다. 이러한 상황을 수정하기 위해, DI는 클래스들 간의 의존성을 최소화하는 방법입니다.

 

의존관계의 종류 

  • 정적인 클래스 의존관계 : 클래스가 사용하는 import문만 보고도 의존관계를 파악할 수 있는 관계를 말합니다. 
  • 동적인 객체 의존관계 : 외부로부터 주입받은 객체를 활용하는 관계, 즉 내부에서는 런타임 전까지 확인이 불가 합니다. 

 

IoC 컨테이너, DI 컨테이너

IoC 컨테이너와 DI 컨테이너는 같은 의미로 사용되는 단어 입니다. 컨테이너라는 단어에 의미 자체가 spring에서 생성 되는 모든 Bean 객체를 관리그리고 객체 간 연결해주는 작업을 수행하는 곳을 말합니다.

 

@Configuration과 @Bean을 사용하게 되면은 자동으로 Spring이 해당 객체를 DI컨테이너에 등록을 하게 됩니다. 이렇게 함으로써 스프링 프레임워크는 개발자가 오로지 개발에만 집중할 수 있는 환경을 제공해준다라고 생각하면 되겠습니다. 


# ApplicationContext

ApplicationContext는 스프링의 핵심 컨테이너를 의미합니다. 스프링 애플리케이션을 구성하는 객체들을 생성하고, 연결하고, 설정하며, 관리합니다. 


ApplicationContext는 다양한 구현체를 제공하며, 대표적으로 ClassPathXmlApplicationContext, FileSystemXmlApplicationContext, AnnotationConfigApplicationContext 등이 있습니다.

 

  • ClassPathXmlApplicationContext는 classpath에서 XML 설정 파일을 읽어들여서 ApplicationContext를 생성하는 방식이며,
  • FileSystemXmlApplicationContext는 파일 시스템에서 XML 설정 파일을 읽어들여서 ApplicationContext를 생성하는 방식입니다.
  • AnnotationConfigApplicationContext는 JavaConfig로 설정한 클래스를 읽어들여서 ApplicationContext를 생성하는 방식입니다.

 

Spring Container를 생성할 때는 아래와 같이 구성 정보를 지정해주어야 합니다. ex) AppConfig.class

new AnnotationConfigApplicationContext(AppConfig.class)

# @Bean

Bean은 컨테이너에 등록이 되는 하나의 객체를 의미하며, 빈 이름은 메서드 이름을 사용합니다. 그리고 개발자에 선택에 따라서 빈 이름을 직접 부여하기도 합니다. 하지만 직접 설정 같은 경우에는 이후에 다른 문제가 발생 할 수 있는 원인이 되므로 자동으로 등록하는 이름을 지정하는 것을 추천합니다. 

 

참고 사항

  • 같은 이름의 빈을 등록하게 되면은 오류가 발생하오니 이점 참고하시기 바랍니다.
  • 스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나뉘어져 있습니다. 

 

Context Method

  • ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈이름을 조회 합니다. 
  • ac.getBean() : 빈 이름으로 빈 객체를 조회합니다. 
  • ac.getBeansOfType() : 해당 타입의 모든 빈을 조회 할 수 있습니다. 
  • getRole() : 해당 메소드로 직적 생성한 객체인지, 스프링이 생성한 객체인지 구분합니다. 
    • ROLE_APPLICATION : 사용자가 정의한 빈
    • ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈
public class ApplicationContextInfoTest {

    AnnotationConfigApplicationContext ac = 
    	new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈 출력하기")
    void findApplicationBean() {
        //given
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);


            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + "object = " + bean);
            }
        }

    }

}

# BeanFactory

BeanFactory는 Spring 프레임워크에서 제공하는 인터페이스 중 하나입니다. 스프링에서 빈의 생성, 관리 및 조회를 담당하는 컨테이너 역할을 합니다.

 

그렇다면 우리가 사용하는 ApplicationContext가 BeanFactory의 차이점이 무엇인가에 대한 궁금점이 생기는데요. ApplicationContext는 기존의 BeanFactory를 상속을 받고, 추가적인 기능을 위해 여러 인터페이스를 다중 상속을 받고 있는 인터페이스라고 생각하시면 됩니다. 

 

스프링 컨테이너는 다양한 형식의 설정 정보를 받아드일 수 있게 설계되어져 있습니다. 그렇기 때문에 Java 설정 뿐만 아니라 xml 설정, Groovy 를 통한 설정 사용이 가능합니다. 


# BeanDefinition

BeanDefinition은 스프링 프레임워크에서 컨테이너에 의해 생성되는 Bean(객체)을 정의하는 메타데이터의 형태입니다. BeanDefinition은 어떤 클래스의 인스턴스를 생성할 것인지, 프로퍼티는 무엇인지, 어떤 생성자를 호출할 것인지, 빈의 범위는 무엇인지 등 빈에 대한 정보를 담고 있습니다.

 

  • AnnotationConfigApplicationContext 는 AnnotatedBeanDefinitionReader 를 사용해서 AppConfig.class 를 읽고 BeanDefinition 을 생성합니다.
  • GenericXmlApplicationContext 는 XmlBeanDefinitionReader 를 사용해서 appConfig.xml 설정 정보를 읽고 BeanDefinition 을 생성합니다.

 

BeanDefinition 정보

  • BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
  • factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름
  • factoryMethodName: 빈을 생성할 팩토리 메서드 지정
  • Scope: 싱글톤(기본값)
  • lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연처리 하는지 여부
  • InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
  • DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
  • Constructor arguments, Properties: 의존관계 주입에서 사용한다. (자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)

# 싱글톤 패턴

싱글톤 패턴은 디자인 패턴 중 하나로, 어떤 클래스가 최대 한 번의 인스턴스 생성만을 보장하고, 그 인스턴스에 대한 전역적인 접근점을 제공하는 패턴입니다.

싱글톤 패턴의 특징은 다음과 같습니다.

  • 인스턴스가 오직 하나만 생성됩니다.
  • 생성된 인스턴스는 어디에서든지 접근이 가능합니다.
  • 생성자가 private으로 선언되어, 외부에서 직접 인스턴스를 생성할 수 없습니다.
  • 인스턴스 생성을 담당하는 메소드는 반드시 static으로 선언되어야 합니다.

 

싱글톤 패턴의 문제점은 다음과 같습니다.

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. --> DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다.
  • 안티패턴으로 불리기도 한다.

# 싱글톤 컨테이너

싱글톤 패턴에서 보여주는 단점들을 해결하기 위해 spring에서 제공하는 기능이 바로 싱글톤 컨테이너 입니다. 

 

스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 자동으로 객체 인스턴스를 싱글톤으로 관리합니다. 즉, 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다는 것 입니다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 하며, 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있습니다. 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 되고, DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있습니다.

 

싱글톤 방식의 주의점

싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 문제가 발생 할 수 있습니다.

 

즉, 무상태(stateless)로 설계해야 한는 것입니다.다르게 말하자면, 특정 클라이언트에 의존적인 필드가 있으면 안되며, 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안됩니다. 가급적 읽기만 가능하도록 설계해야 합니다. 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용하는 것을 권장합니다. 


# @Configuration 과 바이트코드 조작 라이브러리

싱글톤 컨테이너를 싱글톤 레지스트리를 통해 여러 번의 호출에도 하나의 객체만을 반환 하게 되는데, 이러한 동작을 할 수 있는 원인은 CGLIB에 있습니다. 우리가 @Bean을 등록하기 위해 @Configuration을 사용하게 되면 Spring은 알아서 BGLIB를 사용을 하게 되고 가상의 객체를 만들어서 관리하게 됩니다. 

 

그렇기 때문에 생성된 객체를 출력을 하게 되면 아래와 같은 정보가 출력이 됩니다. 

bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70

 

그렇다면 CGLIB를 사용하지 않는 객체를 만드려고 하면 어떻게 해야 하는가에 대한 궁금증이 생기는데, 방법은 간단합니다. 바로 @Configuration을 사용하지 않고 @Bean 어노테이션을 사용하면 됩니다. 그러면 아래와 같이 출력 됩니다. 그리고 기존에 싱글톤으로 관리되던 객체가 아니라 여러번 생성이 되는 객체가 됩니다.

class hello.core.AppConfig

 

CGLIB

영한님이 말씀하시는 CGLIB 예상 코드입니다.

@Bean
public MemberRepository memberRepository() {
 
     if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
     	return 스프링 컨테이너에서 찾아서 반환;
     } else { //스프링 컨테이너에 없으면
     	기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
     	return 반환
     }
}

# @ComponentScan

@ComponentScan 어노테이션을 사용하게 되면은 패키지 내부에 @Component가 지정된 클래스들을 빈으로 등록하는 작업을 수행합니다. 

 

@Configuration 내부에는 @ComponentScan 어노테이션이 존재하므로 @Configuration만 사용하여도 자동으로 @Component가 인식이 되고 있습니다. 

 

@Component를 사용하게 되면은 자동으로  클래스 명을 빈으로 등록하는데 이때, 제일 처음 대문자를 소문자로 변환하여 등록 하게 됩니다. ex) Test --> test 

 

다른 이름으로 등록 하고자 하면은 @Component("test1")과 같이 설정 하면 됩니다. 

 

탐색 시작위치 지정방법

  • basePackages : 탐색할 패키지의 시작 위치를 지정합니다. 이 패키지를 포함해서 하위 패키지를 모두 탐색합니다.
    • basePackages = {"hello.core", "hello.service"} 이렇게 여러 시작 위치를 지정할 수도 있습니다.
  • basePackageClasses : 지정한 클래스의 패키지를 탐색 시작 위치로 지정합니다.
  • 만약 지정하지 않으면 @ComponentScan 이 붙은 설정 정보 클래스의 패키지가 시작 위치가 됩니다.

 

참고로 @SpringBootApplication 에 @ComponentScan이 들어 있으므리 Applicaion class 하단 패키지에 자바 파일을 만들어 두어야 인식을 할 수 있습니다. 

 

@Component 종류와 기능

  • @Controller : 스프링 MVC 컨트롤러로 인식 합니다.
  • @Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환합니다.
  • @Configuration : 앞서 보았듯이 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 합니다.
  • @Service : 사실 @Service 는 특별한 처리를 하지 않습니다. 대신 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나 라고 비즈니스 계층을 인식하는데 도움이 됩니다.

includeFilters 와 excludeFilters룰 통해 scan대상을 추가로 지정이 가능합니다. 

 

수동으로 빈을 등록하였을 경우, 자동으로 빈으로 등록되는 것보다 객체 생성에서 우선권을 가지게 되는데 이러한 경우 나중에 오류가 발생하면 찾기가 어려운 오류가 되는 경우가 대부분이라고 합니다. 최대한 자동빈 등록을 사용하는 것을 권유드립니다. 

 

내용이 긴 관계로 1편 2편으로 나누어서 글을 올리도록 하겠습니다.