Skip to content

스프링 핵심 원리 ‐ 기본편

dvlp-sy edited this page May 17, 2024 · 12 revisions

섹션 0. 강의 소개

🌱 객체지향과 스프링을 함께 이해하자

  • 애플리케이션을 개발하고 설계하는 시야 넓히기
  • IoC, DI, 객체 지향 설계 원칙(SRP, OCP, DIP)

🌱 강의 목표

"스프링을 왜 만들었고 왜 이런 기능을 제공하는지에 대한 핵심 원리에 초점을 맞춘다"

  • 스프링 기본 기능 학습하기
  • 스프링 본질 깊이 이해하기
  • 객체 지향 설계를 고민하는 개발자로 성장하기


섹션 1. 객체 지향 설계와 스프링

1-1. 이야기 - 자바 진영의 추운 겨울과 스프링의 탄생

2000년대 초반

  • EJB(Enterprise Java Beans) : Java의 표준 기술 중 최고봉 -> Spring, JPA, 분산 처리 기능 등을 모두 지원, 게다가 표준 기술이다 보니 금융권에 도입되는 등 보급과 영업이 잘 됨
  • Entty Bean : ORM 기술(객체를 쿼리 없이 DB에 편하게 저장하고 접근하는 기술, 오늘날의 JPA) -> 비싼 가격

EJB 지옥

  • EJB는 복잡하고 어려운데 느리다
  • 지저분한 코드
  • 컨테이너 한 번 띄우는 데 백만 년
  • EJB 인터페이스를 다 의존적으로 설계해야 했다
  • POJO(Plan Old Java Object) : 오래된 방식의 간단한 자바 오브젝트를 쓰자는 말까지 나옴
  • 기술 수준이 현저히 낮은 Entity Bean

패러다임을 바꾼 두 명의 개발자

  • 로드 존슨 : EJB보다 단순하면서 좋은 방법을 제안하고 그게 미래의 스프링이 됨
  • 개빈 킹 : Entity Bean이 너무 구려서 대체 방안인 Hibernate를 개발함 -> 자바 표준 JPA의 구현체 80% 이상을 Hibernate가 차지하고 있음

스프링의 역사

  • 2002년 로드 존슨의 책 출간 : EJB 문제점 지적
  • 스프링의 이름 : EJB라는 겨울을 넘어 새로운 시작이라는 뜻
  • 2003년 스프링 프레임워크 1.0 출시 (XML 기반 설정)
  • 2009년 스프링 프레임워크 3.0 출시 (자바 코드로 설정)
  • 2014년 스프링 부트 1.0 출시

1-2. 스프링이란?

스프링 생태계

https://spring.io/projects

"스프링은 여러 가지 기술들의 모음"

  • Spring Framework : Spring의 가장 핵심이 되는 프레임워크
  • Spring Boot : Spring 기술을 편하게 사용할 수 있게 하는 장치
  • Spring Data : 등록, 수정, 삭제, 조회 등 CRUD 연산을 간편하게 하는 기능 제공 (가장 많이 사용하는 것은 Spring Data JPA)
  • Spring Session : 간편한 세션 관리 기능 제공
  • Spring Security : 보안 관련 기능 제공
  • Spring Rest Docs : API 문서와 테스트를 엮어 문서화를 편하게 하는 기능 제공
  • Spring Batch : 데이터를 한 번에 처리할 수 없을 때 배치처리 하는 기능 제공
  • Spring Cloud : 클라우드에 특화된 기능 제공

스프링 프레임워크

핵심 기술 스프링 DI 컨테이너, AOP, 이벤트 등
웹 기술 스프링 MVC, 스프링 WebFlux
데이터 접근 기술 트랜잭션, JDBC, ORM, XML
기술 통합 캐시, 이메일, 원격접근, 스케줄링
테스트 스프링 기반 테스트 지원
언어 Kotlin, Groovy

스프링 부트

"스프링을 굉장히 편리하게 사용할 수 있도록 지원하는 기술 (대부분 디폴트 설정 방식이 있고 잘 변경하지 않음)"

  • Tomcat 서버 내장 : 단독으로 실행할 수 있는 스프링 애플리케이션을 쉽게 생성
  • starter 종속성 제공 : 라이브러리를 하나만 가져오는 게 아니라 모버 스타터, AOP 스타터, JPA 스타터 등을 묶어서 가져와야 할 때 그런 것들을 한 번에 불러오게 함
  • 스프링과 3rd-party 라이브러리 자동 구성
  • production 준비 기능

⚠️ 스프링 부트는 스프링 프레임워크와 별도로 사용할 수 있는 게 아니다. 위의 여러 기술들을 편리하게 사용할 수 있게 기능을 제공하는 역할만 한다.

핵심 개념

  • 왜 만들었는가
    • 객체 지향 언어가 가진 강력한 특징을 살리는 프레임워크 개발
    • EJB를 상속받는 순간 EJB에 의존적으로 개발을 해야 한다
    • 객체 지향이 가진 장점을 모두 잃어버린다
  • 핵심 컨셉
    • 자바 언어(객체 지향 언어) 기반의 프레임워크
    • 좋은 객체 지향 애플리케이션을 개발할 수 있도록 돕는다

1-3. 좋은 객체 지향 프로그래밍이란?

객체 지향 프로그래밍의 정의

  • 컴퓨터 프로그램을 "객체"들의 모임으로 파악하고자 하는 것
  • 객체는 메시지를 주고받고 데이터를 처리할 수 있다 (협력)
  • 유연하고 변경이 용이하여 대규모 소프트웨어 개발에 많이 사용 -> 컴포넌트를 쉽고 유연하게 변경하면서 개발 가능 (Polymorphism)

다형성(Polymorphism)

"역할(인터페이스)과 구현(실체화한 클래스, 구현체)으로 객체 구분"

  • 구현체가 바뀌어도 인터페이스를 의존하고 있는 다른 객체에 영향을 주지 않는다
  • 클라이언트가 구현체의 내용을 알지 못해도 된다
  • 다른 대상으로 변환이 가능하고 새로운 구현체가 나와도 기존의 인터페이스를 그대로 사용할 수 있다
  • 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있다 = 클라이언트를 변경하지 않고 서버의 기능을 유연하게 변경할 수 있다

✓ 따라서 인터페이스를 안정적으로(변화가 없도록) 잘 설계하는 것이 중요하다

스프링과 객체 지향

  • 다형성 극대화 : 제어의 역전, 의존관계 주입은 모두 다형성을 활용한 기능
  • 객체 지향 설계의 5원칙 SOLID 준수

⚠️ 다형성만으로는 OCP, DIP를 준수할 수 없다

1-4. 좋은 객체 지향 설계의 5가지 원칙(SOLID)

로버트 마틴이 정리한 좋은 객체 지향 설계의 5가지 원칙

SRP 단일 책임 원칙(Single Responsibility Principle)
OCP 개방 폐쇄 원칙(Open/Close Principle)
LSP 리스코프 치환 원칙(Liskov Substitution Principle)
ISP 인터페이스 분리 원칙(Interface Segregation Principle)
DIP 의존관계 역전 원칙(Dependency Inversion Principle)

SRP 단일 책임 원칙

  • 한 클래스는 하나의 책임만 가진다
  • 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것이다
  • UI 변경, 객체의 생성과 사용 분리

OCP 개방-폐쇄 원칙

  • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다
  • 클라이언트가 구현체를 직접 선택하는 방식으로는 OCP 원칙을 지킬 수 없다
    MemberRepository m = new MemoryMemberRepository(); // 변경 전
    MemberRepository m = new JdbcMemberRepository(); // 변경 후
  • 객체를 생성하고 연관관계를 맺어주는 설정자가 필요하다

LSP 리스코프 치환 원칙

  • 프로그램의 객체는 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 변환 가능해야 한다
  • 하위 클래스는 인터페이스 규약을 모두 지켜야 한다

ISP 인터페이스 분리 원칙

  • 특정 클라이언트를 위해 범용 인터페이스보다 여러 개의 인터페이스를 만드는 것이 좋다
  • 인터페이스가 명확해지고 대체 가능성이 높아진다

DIP 의존관계 역전 원칙

  • 구현 클래스가 아닌 인터페이스에 의존해야 한다
  • 클라이언트가 인터페이스에 의존하면서 구현체에도 동시에 의존하면 DIP 위반이다
    MemberRepository m = new MemoryMemberRepository();

1-5. 객체 지향 설계와 스프링

스프링이 SOLID를 준수하는 방법

  • DI(Dependency Injection) : DI 컨테이너를 제공하여 의존성 주입
  • 클라이언트 코드를 변경하지 않고 기능 확장

객체 지향 설계의 이상과 현실

  • 이상 : 역할과 구현을 분리하여 모든 설계에 인터페이스 부여
  • 현실 : 추상화 비용 발생 -> 확장 가능성이 없다면 구체 클래스를 직접 사용하고 향후에 필요할 때 리팩토링하여 인터페이스 도입


섹션 2. 스프링 핵심 원리 이해1 - 예제 만들기

2-1. 프로젝트 생성

Setting

build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.5'
	id 'io.spring.dependency-management' version '1.1.4'
}

group = 'net'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}

tasks.named('test') {
	useJUnitPlatform()
}

2-2. 비즈니스 요구사항과 설계

회원 도메인 요구사항

  • 회원을 가입하고 조회할 수 있다 (회원 서비스)
  • 회원은 일반과 VIP 두 가지 등급이 있다 (도메인에 드러나야 함)
  • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다 (인터페이스 필요)

주문과 할인 정책 요구사항

  • 회원은 상품을 주문할 수 있다 (주문 서비스)
  • 회원 등급에 따라 할인 정책을 적용할 수 있다 (도메인에 드러나야 함)
  • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용하는데 추후 변경될 수 있다 (인터페이스 필요)
  • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고 최악의 경우 할인을 적용하지 않을 수도 있다

2-3. 회원 도메인 설계

  • 클라이언트는 회원 서비스를 활용한다
  • 회원 도메인에는 id, name, grade가 포함된다
  • 회원 서비스 인터페이스와 구현체를 별도로 생성한다
  • 회원 서비스에는 회원가입과 회원조회가 포함된다
  • 회원 리포지토리 인터페이스와 구현체를 별도로 생성한다
  • 회원 서비스는 회원 리포지토리 인터페이스를 의존한다

2-4. 회원 도메인 개발

  • github 코드 참조

2-5. 회원 도메인 실행과 테스트ㅤ

회원 도메인 설계의 문제점

  • github 코드 참조
  • 의존관계가 인터페이스뿐만 아니라 구현체까지 모두 의존하고 있다

2-6. 주문과 할인 도메인 설계

  • github 코드 참조

2-7. 주문과 할인 도메인 개발

  • github 코드 참조

2-8. 주문과 할인 도메인 실행과 테스트

주문과 할인 도메인 설계의 문제점

  • github 코드 참조
  • 의존관계가 인터페이스뿐만 아니라 구현체까지 모두 의존하고 있다


섹션 3. 스프링 핵심 원리 이해2 - 객체 지향 원리 적용

3-1. 새로운 할인 정책 개발

새로운 요구사항의 추가

  • 고정 금액 할인이 아닌 퍼센트 할인을 희망
  • DiscountPolicy 인터페이스에 새로운 구현체(RateDiscountPolicy)가 추가되어야 함
public class RateDiscountPolicy implements DiscountPolicy
{
    private int discountPercent = 10;

    @Override
    public int discount(Member member, int price)
    {
        if (member.getGrade() == Grade.VIP)
            return price * discountPercent / 100;
        else
            return 0;
    }
}

RateDiscountPolicy 기능 검증 (테스트)

class RateDiscountPolicyTest
{
    RateDiscountPolicy rateDiscountPolicy = new RateDiscountPolicy();

    @Test
    @DisplayName("VIP는 10% 할인이 적용되어야 한다")
    void vip()
    {
        // given
        Member member = new Member(1L, "Vip", Grade.VIP);
        // when
        int discount = rateDiscountPolicy.discount(member, 10000);
        // then
        assertThat(discount).isEqualTo(1000);
    }

    @Test
    @DisplayName("VIP가 아니면 10% 할인이 적용되지 않아야 한다")
    void basic()
    {
        // given
        Member member = new Member(2L, "Basic", Grade.BASIC);
        // when
        int discount = rateDiscountPolicy.discount(member, 10000);
        // then
        assertThat(discount).isEqualTo(0);
    }
}

3-2. 새로운 할인 정책 적용과 문제점

기존 방식의 문제점

  • 할인 정책을 변경하기 위해 OrderServiceImpl에서 코드 수정 발생
  • OCP, DIP를 준수하지 못함
    • 추상 인터페이스 : DiscountPolicy
    • 구현 클래스 : FixDiscountPolicy, RateDiscountPolicy
    • 기존 방식은 기능을 변경하면 서비스 클라이언트 코드가 변화함 -> OCP 위반
    • 기존 방식은 서비스 클라이언트가 인터페이스와 구현체를 모두 의존함 -> DIP 위반

해결 방안

  • OrderServiceImpl은 인터페이스만 의존하게 한다
  • OrderServiceImpl을 대신하여 구현 객체를 생성하는 별도의 클래스를 생성한다

3-3. 관심사의 분리

AppConfig

: 애플리케이션의 전체 동작 방식을 구성

public class AppConfig
{
    public MemberService memberService()
    {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService()
    {
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixedDiscountPolicy());
    }
}
  • 구현 객체 생성 + 연결하는 역할
  • MemberServiceImpl, OrderServiceImpl에서 의존성 주입 (생성자 주입 권장)

테스트 코드 수정

  • 객체를 직접 생성하던 방법을 AppConfig를 통해 생성하는 방법으로 변경
  • 아래 코드는 OrderServiceTest 기준, MemberServiceTest도 동일한 방법으로 변경
private MemberService memberService;
private OrderService orderService;

@BeforeEach
public void beforeEach()
{
    AppConfig appConfig = new AppConfig();
    memberService = appConfig.memberService();
    orderService = appConfig.orderService();
}

3-4. AppConfig 리팩토링

기존 AppConfig의 문제점

  • 중복이 존재함 : memberService와 orderService에서 MemoryMemberRepository를 각각 따로 생성함
  • "역할에 대한 구현이 어떻다"라는 게 한눈에 보이지 않음

수정한 AppConfig

public class AppConfig
{
    public MemberRepository memberRepository()
    {
        return new MemoryMemberRepository();
    }

    public DiscountPolicy discountPolicy()
    {
        return new FixedDiscountPolicy();
    }

    public MemberService memberService()
    {
        return new MemberServiceImpl(memberRepository());
    }

    public OrderService orderService()
    {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
}

3-5. 새로운 구조와 할인 정책 적용

AppConfig 변경

public class AppConfig
{
    public MemberRepository memberRepository()
    {
        return new MemoryMemberRepository();
    }

    public DiscountPolicy discountPolicy()
    {
        // return new FixedDiscountPolicy();
        return new RateDiscountPolicy();
    }

    public MemberService memberService()
    {
        return new MemberServiceImpl(memberRepository());
    }

    public OrderService orderService()
    {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
}

3-6. 전체 흐름 정리

관심사의 분리

  • 객체는 주어진 역할을 수행하는 데만 집중해야 한다
  • 객체가 다른 인터페이스를 의존할 때 해당 인터페이스가 어떤 구현체를 사용하고 있든 동일하게 작업을 수행해야 한다
  • 객체를 구현하는 역할을 담당하는 별도의 객체(AppConfig)가 필요하다
  • AppConfig는 애플리케이션의 전체 동작 방식을 구성하기 위해 구현 객체를 생성하고 연결하는 책임을 가진다

3-7. 좋은 객체 지향 설계의 5가지 원칙의 적용

SRP 단일 책임 원칙 관심사의 분리 > 구성(AppConfig), 사용(Service, Repository 등)
DIP 의존관계 역전 원칙 AppConfig를 통해 객체를 대신 생성하여 의존관계 주입
OCP 개방-폐쇄 원칙 소프트웨어를 변경해도 사용 영역이 변경되지 않음

3-8. IoC, DI, 그리고 컨테이너

제어의 역전 (IoC)

: Inversion of Control

  • 프로그램의 제어 흐름을 직접 관리하지 않고 외부에서 관리하는 것
  • 프로그램의 제어 흐름에 대한 권한을 모두 AppConfig가 보유

* 프레임워크 : 내가 작성한 코드를 제어하고 대신 실행 * 라이브러리 : 내가 작성한 코드가 직접 제어 흐름을 담당

의존 관계 주입 (DI)

: Dependency Injection

  • 정적인 클래스 의존관계 : import하는 클래스만 보고 바로 판단할 수 있는 의존관계
    • OrderServiceImpl은 MemberRepository와 DiscountPolicy를 의존한다는 것을 알 수 있다
    • 하지만 클래스 의존관계만으로는 OrderServiceImpl에 실제로 어떤 객체가 주입될지는 알 수 없다
  • 동적인 인스턴스 의존관계 : 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존관계
    • 의존관계 주입 : 런타임에 실제 구현 객체를 생성하고 클라이언트에 전달해서 의존관계가 연결되는 것

IoC 컨테이너, DI 컨테이너

  • AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것

3-9. 스프링으로 전환하기

구성을 위한 어노테이션

  • @Configuration : 설정 정보에 기재하는 어노테이션
  • @Bean: 설정 파일의 각 메소드에 기재하는 어노테이션 -> 스프링 컨테이너에 객체를 스프링 빈으로 등록

Spring Container 적용하기

  • Applcation 클래스에 ApplicationContext 추가
  • ApplicationContext가 모든 객체를 관리하는 Spring Container라고 보면 됨
  • 이전에는 개발자가 필요한 객체를 AppConfig를 통해 직접 조회 -> ApplicationContext를 사용하는 경우 applicationContext.getBean()을 통해 스프링 빈을 찾을 수 있음


섹션 4. 스프링 컨테이너와 스프링 빈

4-1. 스프링 컨테이너 생성

스프링 컨테이너 생성 코드

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
  • 스프링 컨테이너는 XML 기반으로 만들거나 어노테이션 기반의 자바 설정 클래스로 만들 수 있다
  • ApplicationContext : 스프링 컨테이너 인터페이스
  • AnnotationConfigApplicationContext : 어노테이션 기반의 자바 설정 클래스(구현체)

스프링 컨테이너 생성 과정

  • 스프링 컨테이너 생성(위의 코드) : 구성 정보(AppConfig.class)를 지정한 후 스프링 컨테이너 생성

  • 스프링 빈 생성 : 파라미터로 넘어온 구성 정보를 사용해 스프링 빈 등록

    ** 스프링 컨테이너 안에는 스프링 빈 저장소 존재 : key = 빈 이름(메서드 이름), value = 빈 객체(반환 객체) ** 빈의 이름은 일반적으로 메서드 이름을 사용하지만 직접 부여할 수도 있다. 단, 빈의 이름은 항상 다른 이름을 부여해야 한다

  • 스프링 빈 의존관계 설정 : 구성 정보를 참고하여 의존관계 주입

4-2. 컨테이너에 등록된 모든 빈 조회

모든 빈을 조회하는 테스트 코드 작성

  • ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회한다
  • ac.getBean() : 빈 이름으로 빈 객체를 조회한다
public class ApplicationContextInfoTest
{
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈 출력하기")
    void findAllBean()
    {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames)
        {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("name : "+beanDefinitionName+" object : "+bean);
        }
    }
}

애플리케이션 빈을 조회하는 테스트 코드 작성

  • BeanDefinition.ROLE_APPLICATION : 내부적인 동작을 위한 빈이 아니라 애플리케이션을 개발하기 위해 등록한 빈 (혹은 외부 라이브러리)
@Test
@DisplayName("모든 빈 출력하기")
void findApplicationBean()
{
    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);
        }
    }
}

실행 결과

name : appConfig object : net.core.AppConfig$$SpringCGLIB$$0@36b0fcd5
name : memberRepository object : net.core.repository.MemoryMemberRepository@4fad94a7
name : discountPolicy object : net.core.discount.FixedDiscountPolicy@475835b1
name : memberService object : net.core.service.MemberServiceImpl@6326d182
name : orderService object : net.core.service.OrderServiceImpl@5241cf67

4-3. 스프링 빈 조회 - 기본

getBean()을 통한 조회

  • github 코드 참조 (ApplicationContextBasicTest)
  • ac.getBean(빈이름, 타입)
    조회 방법 특징
    인터페이스 타입으로 조회 역할에 의존 X -> 유언성 증가
    구체 타입으로 조회 역할에 의존 -> 유연성의 감소, 그러나 가끔 필요할 때 있음
  • ac.getBean(타입)
  • 조회할 스프링 빈이 존재하지 않으면 NoSuchBeanDefinitionException 발생

4-4. 스프링 빈 조회 - 동일한 타입이 둘 이상

타입 중복 오류

  • github 코드 참조 (ApplicationContextSameBeanFindTest)
  • 같은 타입의 스프링 빈이 둘 이상이면 오류 발생
  • 빈 이름을 지정하여 문제 해결
  • ac.getBeanOfType()을 사용하면 모든 빈 조회 가능 (return : Map<String, 타입>)
@Test
@DisplayName("특정 타입 모두 조회")
void findAllBeanByType()
{
    Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
    for (String key : beansOfType.keySet())
        System.out.println("key = " + key + " value = " + beansOfType.get(key));

    System.out.println("beansOfType = " + beansOfType);
    assertThat(beansOfType.size()).isEqualTo(2);
}

4-5. 스프링 빈 조회 - 상속 관계

스프링 빈 조회의 상속 원칙

  • github 코드 참조 (ApplicationConfigExtendsFindTest)
  • 부모 타입으로 조회 시 자식이 둘 이상이면 오류 발생
  • 빈 이름을 지정하여 문제 해결
  • ac.getBeanOfType()을 사용하면 부모 타입으로 모두 조회 가능
    • 부모 타입으로 조회하면 자식 타입도 함께 조회
    • 예를 들어 자바 객체의 최고 부모인 Object로 조회하면 모든 스프링 빈 조회

4-6. BeanFactory와 ApplicationContext

상속 관계

BeanFactory 인터페이스 스프링 컨테이너의 최상위 인터페이스
ApplicationContext 인터페이스 BeanFactory에 부가기능을 더한 것
AnnotationConfigApplicationContext 구현클래스 ApplicationContext를 상속받은 어노테이션 기반의 자바 설정 클래스

BeanFactory

  • 스프링 빈을 관리하고 조회하는 역할 담당
  • getBean() 제공

ApplicationContext

: BeanFactory의 내용을 모두 상속받아 제공 (빈 관리 및 조회)

  • MessageSource 상속 : 국제화 기능
  • EnvironmentCapable 상속 : 환경변수(로컬, 개발, 운영 등을 구분하여 처리)
  • ApplicationEventPublisher 상속 : 이벤트를 발행하고 구독하는 모델 지원
  • ResourceLoader 상속 : 파일, 클래스패스, 외부 등에서 리소스 조회

4-7. 다양한 설정 형식 지원 - 자바 코드, XML

스프링 컨테이너의 다양한 Config 형식

  • 자바 코드, XML, Groovy 등
  • ApplicationContext의 구현 클래스에는 AnnotationConfigApplicationContext, GenericXmlApplicationContext 등이 있다
  • 최근에는 스프링부트를 많이 사용하면서 XML 기반의 Config는 잘 사용 X (대신 컴파일 없이 빈의 설정 정보를 변경할 수 있다는 장점이 있다)

appConfig.xml

  • AppConfig 파일에 작성한 내용과 정확하게 동일하다
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="memberService" class="net.core.service.MemberServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
    </bean>
    <bean id="memberRepository" class="net.core.repository.MemoryMemberRepository" />
    <bean id="orderService" class="net.core.service.OrderServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
        <constructor-arg name="discountPolicy" ref="discountPolicy" />
    </bean>
    <bean id="discountPolicy" class="net.core.discount.RateDiscountPolicy" />
</beans>

XmlAppContextTest 동작 확인

public class XmlAppContextTest
{
    @Test
    public void xmlAppContext()
    {
        ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

4-8. 스프링 빈 설정 메타 정보 - BeanDefinition

"스프링 설정 형식은 XML도 지원하고 Java Config도 지원하는데 애플리케이션에 영향이 없는 이유"

스프링 빈 설정 메타 정보

  • BeanDefinition(인터페이스) 추상화 : XML이나 Java 코드를 읽어서 BeanDefinition을 만들기만 하면 된다 (스프링 컨테이너는 BeanDefinition만 사용하면 그만이다)
  • AnnotationConfigApplicationContext가 AnnotatedBeanDefinitionReader를 통해 AppConfig.class를 읽고 BeanDefinition 생성
  • GenericXmlApplicationContext가 XmlBeanDefinitionReader를 통해 appConfig.xml을 읽고 BeanDefinition 생성

BeanDefinition

  • BeanClassName : 생성할 빈의 클래스명
  • factoryBeanName : 팩토리 역할의 빈을 사용하는 경우 그 이름 (appConfig)
  • facotyMethodName : 빈을 생성할 팩토리 메서드 (memberService 등)
  • scope : scope가 할당되어 있지 않으면 싱글톤 (default)
  • lazyInit : 스프링 컨테이너를 생성할 때 스프링 빈을 등록하지 않고 실제 사용하는 시점에 스프링 빈을 초기화할 것인지의 여부
  • initMethodName : 빈을 생성하고 의존관계 적용 후 호출되는 초기화 메서드 이름
  • destroyMethodName : 빈의 생명주기가 끝나서 제거하기 전 호출되는 메서드 이름
  • Constructor Agruments, Properties : 의존관계 주입에서 사용 (팩토리 역할의 빈을 사용하면 없음)

BeanDefinitionTest

public class BeanDefinitionTest
{
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 설정 메타정보 확인")
    public void findApplicationBean()
    {
        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);
        }
    }
}


섹션 5. 싱글톤 컨테이너

5-1. 웹 애플리케이션과 싱글톤

웹 애플리케이션

  • 웹 애플리케이션 : 프로세스와 프로세스 사이에 HTTP를 통해 정보를 주고받는 소프트웨어, 일반적으로 클라이언트 프로세스와 서버 프로세스가 있다
  • 스프링은 배치 프로세스나 데몬 프로세스를 관리하기도 하지만 "웹 애플리케이션"이 주목적
    배치(Batch) 일련의 작업을 지정된 시각에 실행하는 프로세스
    데몬(Daemon) 특정 서비스를 위해 백그라운드에서 항상 실행되는 프로세스

웹 애플리케이션에서 싱글톤의 필요성

  • 순수한 DI 컨테이너 : 다수의 클라이언트가 서비스를 요청하면 JVM에 객체 인스턴스가 계속 생성된다(만약 스프링이 없는 순수한 DI 컨테이너라면 AppConfig가 요청을 할 때마다 객체를 새로 생성) -> 극심한 메모리 낭비
  • 싱글톤 패턴 : 하나의 객체 인스턴스만이 JVM 안에 존재하고 그것을 공유한다

5-2. 싱글톤 패턴

정의

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴
  • 객체가 2개 이상 생성되지 않도록 방지

코드로 보는 싱글톤 패턴

  • private 생성자를 활용해 외부에서 임의로 객체를 새로 생성하지 못하게 막는다
  • static 영역에 이미 생성되어 있는 인스턴스만 활용 가능 (getInstance를 통해)
    public class SingletonService
    {
        private static final SingletonService instance = new SingletonService();
    
        public static SingletonService getInstance()
        {
            return instance;
        }
    
        // Private Constructor
        private SingletonService() {}
    
        public void logic()
        {
            System.out.println("싱글톤 객체 로직 호출");
        }
    }
  • 테스트 코드
    @Test
    @DisplayName("싱글톤 테스트")
    void singleToneService()
    {
        // given
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();
    
        // then
        Assertions.assertThat(singletonService1).isSameAs(singletonService2);
    }
  • isEqualTo vs isSameAs
    isEqualTo 값이 같은지 확인(비교대상이 객체일 때 사용)
    isSameAs 참조 값이 같은지 확인
    /**
     * 두 객체는 같은 값을 가지고 있다 -> member1 "is Equal to" member2
     * 두 객체는 서로 다른 인스턴스다 -> member1 "is Not Same as" member2
     */
    Member member1 = new Member();
    Member member2 = new Member();

싱글톤 패턴의 단점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다
  • 클라이언트가 구체 클래스에 의존한다 -> DIP 위반
  • OCP 원칙을 위반할 가능성이 높다
  • 유언성이 떨어진다 (DI를 적용하기 어렵다)

5-3. 싱글톤 컨테이너

스프링 컨테이너 SpringContainer

  • 싱글톤 패턴을 적용하지 않으면서 객체 인스턴스를 싱글톤으로 관리하는 컨테이너

테스트 코드

@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer()
{
    // Spring Container
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    MemberService memberService1 = ac.getBean("memberService", MemberService.class);
    MemberService memberService2 = ac.getBean("memberService", MemberService.class);

    System.out.println("memberService : " + memberService1);
    System.out.println("memberService : " + memberService2);

    // Spring Container = Singleton Container므로 테스트 성공
    assertThat(memberService1).isSameAs(memberService2);
}

5-4. 싱글톤 방식의 주의점

무상태(Stateless)

: 객체 인스턴스를 공유해 사용할 때 해당 객체는 상태를 유지하면 안 된다 - 특정 클라이언트에 의존적이지 않아야 한다 - 특정 클라이언트가 값을 수정할 수 있는 필드가 있으면 안 된다 - 가급적 읽기만 가능해야 한다

StatefulService Test

public class StatefulService
{
    private int price;

    public void order(String name, int price)
    {
        System.out.println("name = " + name + " price = " + price);
        this.price = price; // Shared Variable
    }

    public int getPrice()
    {
        return price;
    }
}
static class TestConfig
    {
        @Bean
        public StatefulService statefulService()
        {
            return new StatefulService();
        }
    }

    @Test
    void statefulServiceSingleton()
    {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);

        statefulService1.order("userA", 10000);
        statefulService2.order("userB", 20000);

        int price = statefulService1.getPrice();
        assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }

멀티 스레드 환경에서 싱글톤 방식의 문제점

  • 싱글톤 방식은 하나의 객체 인스턴스를 공유한다
  • 멀티 스레드 환경에서 여러 클라이언트가 동시에 이 객체 인스턴스를 사용한다고 가정
  • 한 스레드가 객체 인스턴스의 값을 변화시키면 다른 인스턴스도 영향을 받는다
  • 만약 스레드간 공유해야 하는 변수가 있다면 변수를 공유하지 않는 방향으로 코드를 수정하거나 synchronized 등의 동기화 기법을 사용해야 한다
  • final 변수에 한 번 지정된 값은 수정할 수 없다 -> 객체가 생성될 때 해당 변수를 초기화한 후 더 이상 변수가 수정되지 않도록 물리적으로 막을 수 있는 방법

5-5. @Configuration과 싱글톤

"AppConfig에서 분명 Repository를 두 번 new 하는데 이게 싱글톤이 맞나"

configurationTest 코드

  • Repository 빈은 중복으로 생성되지 않았다
  • 서로 다른 서비스의 repository가 서로 같은 인스턴스를 참조하고 있다
public class ConfigurationSingletonTest
{
    @Test
    @DisplayName("Repository 빈 중복 생성 여부 확인")
    void configurationTest()
    {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        MemberRepository memberRepository1 = memberService.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();

        assertThat(memberRepository).isSameAs(memberRepository1);
        assertThat(memberRepository).isSameAs(memberRepository2);
    }
}

5-6. @Configuration과 바이트코드 조작의 마법

바이트 코드 조작을 통한 싱글톤 보장

  • AnnotationConfigApplicationContext에 AppConfig가 파라미터로 전달되면 AppConfig 자체가 스프링 빈이 되지 않고 AppConfig를 상속받은 AppConfig@CGLIB가 스프링 빈으로 등록된다
  • CGLIB : 바이트코드를 조작하는 라이브러리
    • @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 새로 생성하는 코드를 동적으로 생성
    • 싱글톤 보장
  • 바이트 코드 조작은 @Configuration을 통해 이루어진다

만약 @Configuration을 사용하지 않는다면?

  • @Bean만 사용하는 경우 싱글톤이 보장되지 않는다


섹션 6. 컴포넌트 스캔

6-1. 컴포넌트 스캔과 의존관계 자동 주입 시작하기

지금까지의 스프링 빈 등록 방법

  • 한땀한땀 Config 파일에 나열하기
  • XML 파일 작성하기
  • 수백 개의 스프링 빈을 직접 등록하는 것은 매우 비효율적

스프링 빈을 효율적으로 등록하기 위해 스프링이 제공하는 기능

  • 컴포넌트 스캔 : 설정 정보가 없어도 자동으로 스프링 빈 등록
  • @Autowired : 스프링 컨테이너가 자동으로 스프링 빈을 찾아 의존관계 자동 주입

@ComponentScan, @Component를 사용한 자동 등록 방법

  • github에서 AutoAppConfig 코드 참조
  • AutoAppConfig는 컴포넌트 스캔을 통해 @Component가 붙은 객체를 스프링 빈에 자동으로 등록한다
@Configuration
@ComponentScan(
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig { }
  • 스프링 빈으로 등록할 객체에는 @Component를 붙여준다
  • 만약 이 객체가 의존관계를 주입을 받고 있다면 해당 필드, 생성자, 수정자에 @Autowired를 붙여준다(AutoAppConfig에는 아무런 빈도 명시되어 있지 않기 때문이다)

컴포넌트 스캔 테스트 코드

public class AutoAppConfigTest
{
    @Test
    void basicScan()
    {
        // given
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig .class);
        // when
        MemberService memberService = ac.getBean(MemberService.class);
        // then
        assertThat(memberService).isInstanceOf(MemberService.class);

    }
}

컴포넌트 스캔의 빈 이름 지정 방법

  • 기본 전략 : 클래스명을 사용하되 맨 앞글자는 소문자로 사용
  • 직접 지정 : @Component("name")

6-2. 탐색 위치와 기본 스캔 대상

컴포넌트 스캔의 시작 위치 지정

  • basePackages = { 경로 }를 통해 컴포넌트 스캔의 시작 디렉토리를 지정할 수 있다
  • 시작 위치를 지정하지 않으면 모든 자바 파일을 확인하기 때문에 비효율적
  • 이때 컴포넌트 스캔의 범위는 시작 디렉토리부터 하위 디렉토리 (설정 정보 클래스를 프로젝트의 최상위 디렉토리에 두면 편리하다 -> @SpringBootApplication은 @ComponentScan을 포함하고 있기 때문에 root에 두는 것이 관례)
@Component(
    basePackages = "net.core"
)

컴포넌트 스캔의 대상

  • @Component는 물론이고 @Component를 포함한 다른 어노테이션도 컴포넌트 스캔 대상
    • @Controller : 스프링 MVC 컨트롤러로 인식
    • @Service : 스프링 비즈니스 계층 인식
    • @Repository : 스프링 데이터 접근 계층으로 인식 -> 데이터 계층의 예외를 스프링 예외로 변환
    • @Configuration : 스프링 설정 정보로 인식 -> 싱글톤을 유지하도록 추가 처리

⚠️ 특정 어노테이션이 다른 어노테이션을 가지고 있는 것을 인식할 수 있는 것은 스프링이 지원하는 기능이다. 어노테이션 자체에는 상속관계가 존재하지 않는다.

6-3. 필터

필터의 종류

  • includeFilters : 컴포넌트 스캔 대상을 추가로 지정
  • excludeFilters : 컴포넌트 스캔에서 제외할 대상 지정

필터 타입 옵션

  • ANNOTATION : (기본값) 어노테이션 인식
  • ASSIGNABLE_TYPE : 지정한 타입과 자식 타입 인식
  • ASPECTJ : AspectJ 패턴 사용
  • REGEX : 정규표현식
  • CUSTOM : TypeFilter 인터페이스를 구현해서 사용

⚠️ 그러나 옵션을 변경해가며 컴포넌트 스캔을 사용하기보다 주어진대로 사용하는 것을 권장한다

MyIncludeComponent & MyExcludeComponent 어노테이션 생성

@Target(ElementType.TYPE) // 클래스 레벨에 붙는 어노테이션
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent { }
@Target(ElementType.TYPE) // 클래스 레벨에 붙는 어노테이션
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent { }

어노테이션의 적용

  • BeanA 클래스
@MyIncludeComponent
public class BeanA { }
  • BeanB 클래스
@MyIncludeComponent
public class BeanB { }

필터 동작 테스트

  • github 코드 참조

6-4. 중복 등록과 충돌

같은 이름으로 스프링 빈이 등록되는 경우

  • 자동 등록 빈 = 자동 등록 빈 : ConflictingBeanDefinitionException 발생
  • 자동 등록 빈 = 수동 등록 빈 : 수동 빈 등록이 우선권을 가짐, 그러나 충돌이 발생하면 스프링부트 오류 발생


7. 의존관계 자동 주입

7-1. 다양한 의존관계 주입 방법

생성자 주입

  • 생성자를 통해 의존관계 주입
    • 불변 의존관계 : 한 번 값을 정하면 변하지 않는다
    • 필수 의존관계 : 반드시 의존관계가 주입되어야 한다

수정자 주입 (Setter 주입)

  • 수정자 메서드를 통해 의존관계 주입
    • 변경 의존관계 : 값이 변경될 가능성이 있다
    • 선택 의존관계 : 의존관계가 반드시 주입될 필요가 없을 가능성이 있다

필드 주입

  • 필드에 바로 의존관계 주입
    • 외부에서 변경 불가능하여 테스트하기 어렵다
    • 웬만하면 사용 금지

일반 메서드 주입

  • 한 번에 여러 필드 주입 가능
  • 일반적으로 사용 X

7-2. 옵션 처리

선택 의존관계에서 사용

  • 의존관계가 주입되지 않은 상태에서 동작할 때 옵션 처리
  • @Autowired의 "required" 옵션은 true가 디폴트 -> 자동 주입 대상이 없으면 오류 발생

옵션 처리의 3가지 방법

@Autowired(required=false) 자동 주입할 대상이 없으면 수정자 메서드 호출 X
@Nullable 자동 주입할 대상이 없으면 null 입력
Optional<> 자동 주입할 대상이 없으면 Optional.empty
// 수정자 메서드 호출 X
@Autowired(required = false)
public void setNoBean1(Member member)
{
    System.out.println("setNoBean1 = " + member);
}
// null 호출
@Autowired
public void setNoBean2(@Nullable Member member)
{
    System.out.println("setNoBean2 = " + member);
}
// null 호출
// 강의자료에 required=false인 오류 있음
@Autowired
public void setNoBean3(Optional<Member> member)
{
    System.out.println("setNoBean3 = " + member);
}

Trouble Shooting

  • @Nullable 사용 시 나타나는 에러 문구가 있음
    warning: unknown enum constant When.MAYBE
        reason: class file for javax.annotation.meta.When not found
    
  • javax.annotation.meta.When 클래스 파일을 찾을 수 없어 발생하는 문제
  • 하단의 dependency를 build.gradle에 추가하여 해결
    dependencies : {
        implementation 'com.google.code.findbugs:jsr305:3.0.2'
    }

7-3. 생성자 주입을 선택하라!

생성자 주입의 장점

  • 대부분의 의존관계는 변경할 일이 없다 -> 불변성 보장
  • 주입해야 하는 데이터가 누락되었을 때 컴파일 오류가 발생한다
  • 필드에 final 키워드를 함께 사용할 수 있다 -> 생성자에서 값이 설정되지 않는 오류를 방지한다 (수정자 주입은 final을 사용할 수 없다)

7-4. 롬복과 최신 트렌드

Lombok 라이브러리 적용 방법

  • buid.gradle의 dependency에 하단의 내용 추가
configurations {
     compileOnly {
         extendsFrom annotationProcessor
    }
}

dependencies {
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
}

@Getter, @Setter

  • 특정 필드에 대한 Getter 함수와 Setter 함수 구현

@RequiredArgsConstructor

  • final이 붙은 필드를 모두 포함하는 생성자를 자동 생성한다
// 수정 전
@Component
public class OrderServiceImpl implements OrderService
{
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy)
    {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}
// 수정 후
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService
{
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}

참고사항

  • @NoArgsConstructor : 아무런 파라미터도 없는 기본 생성자 자동 생성
  • @AllArgsConstructor : 모든 필드의 값을 파라미터로 받아 설정하는 생성자 자동 생성

7-5. 조회 빈이 2개 이상 - 문제

ac.getBean(DiscountPolicy.class)

  • 만약 DiscountPolicy 타입을 가진 빈이 여러 개라면 타입 조회시 오류 발생
  • DiscountPolicy의 하위 타입인 FixedDiscountPolicy, RateDiscountPolicy를 모두 스프링 빈으로 선언하기 위해서는 별도의 조치 필요
  • @Component만 사용할 경우 NoUniqueBeanDefinitionException 발생

7-6. @Autowired 필드 명, @Qualifier, @Primary

@Autowired 필드명 매칭

@Autowired
private DiscountPolicy rateDiscountPolicy
  • 타입 매칭 시도 -> 여러 개의 빈이 있다면 필드명으로 매칭

@Qualifier 사용

  • Qualifier에 지정된 이름으로 자동 주입
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy { ... }
@Component
@Qualifier("fixedDiscountPolicy")
public class FixedDiscountPolicy implements DiscountPolicy { ... }
// 생성자 자동 주입
 public OrderServiceImpl(MemberRepository memberRepository,
                         @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy)
 {
     this.memberRepository = memberRepository;
     this.discountPolicy = discountPolicy;
 }

@Primary 사용

  • @Primary가 붙은 스프링 빈을 디폴트로 지정
  • @Qualifier가 우선순위가 더 높음
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy { ... }
// 생성자 자동 주입
 public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy)
 {
     this.memberRepository = memberRepository;
     this.discountPolicy = discountPolicy;
 }

7-7. 애노테이션 직접 만들기

  • github 코드 참조

7-8. 조회한 빈이 모두 필요할 때, List, Map

의도적으로 특정 타입의 스프링 빈이 모두 필요한 경우

  • 클라이언트가 스프링 빈을 선택할 수 있는 권한이 있는 경우
  • List, Map을 사용하여 전략 패턴을 간단히 구현
  • github 코드 참조

7-9. 자동, 수동의 올바른 실무 운영 기준

자동 빈 등록

  • 업무 로직 빈(컨트롤러, 서비스, 리포지토리 등)
  • 스프링과 스프링부트가 자동으로 등록하는 수많은 빈
  • 관리할 빈이 많아 설정 정보가 커지는 경우 사용

수동 빈 등록

  • 기술 지원 빈(기술적 문제나 AOP 처리) : 어디서 문제가 발생했는지 명확하게 드러나지 않아 수동 빈 등록으로 명확하게 하는 것이 좋다
  • 다형성을 적극적으로 활용하는 업무 로직 빈 : 다형성을 가진 각 빈의 이름을 코드만 보고 쉽게 파악할 수 없다 -> 한 번에 파악할 수 있도록 별도의 설정 파일을 작성하는 것이 좋다
    @Configuration
    public class DiscountPolicyConfig {
        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }
        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }
    }


8. 빈 생명주기 콜백

8-1. 빈 생명주기 콜백 시작

NetworkClient

public class NetworkClient
{
    private String url;

    // Constructor
    public NetworkClient()
    {
        System.out.println("생성자 호출, url = " + url);
        connect();
        call("초기화 연결 메시지");
    }

    // Setter
    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("disconnect: " + url);
    }
}

BeanLifeCycleTest

public class BeanLifeCycleTest
{
    @Configuration
    static class LifeCycleConfig
    {
        @Bean
        public NetworkClient networkClient()
        {
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        }
    }

    @Test
    public void lifeCycleTest()
    {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient networkClient = ac.getBean(NetworkClient.class);
        ac.close();
    }
}

테스트 실행 결과

생성자 호출, url = null
connect: null
call: null message = 초기화 연결 메시지
  • 생성자 : url을 Set 하기 전이다
  • connect : 객체를 생성한 후 url을 Set 하기 전에 호출
  • call : 객체를 생성한 후 url을 Set 하기 전에 호출

스프링 빈의 이벤트 라이프사이클

  • 스프링 컨테이너 생성
  • 스프링 빈 생성
  • 의존관계 주입
  • 초기화 콜백
  • 소멸 전 콜백
  • 스프링 종료

⚠️ 객체의 생성과 초기화는 분리하는 것이 좋다. 초기화 작업은 무겁기 때문이다.

8-2. 인터페이스 InitializingBean, DisposableBean

: 스프링 빈 생명주기 콜백 지원 방법 1

NetworkClient 수정

public class NetworkClient implements InitializingBean, DisposableBean
{
    ...

    @Override
    public void afterPropertiesSet() throws Exception
    {
        connect();
        call("초기화 연결 메시지");
    }

    @Override
    public void destroy() throws Exception
    {
        disconnect();
    }
}
  • NetworkClient는 InitializingBean 인터페이스를 구현한다 (@Override afterPropertiesSet, 초기화)
  • NetworkClient는 InitializingBean 인터페이스를 구현한다 (@Override destroy, 소멸)

테스트 실행 결과

생성자 호출, url = null
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
disconnect: http://hello-spring.dev

인터페이스 방식의 단점

  • 스프링 전용 인터페이스에 의존
  • 초기화, 소멸 메서드의 이름 변경 불가
  • 외부 라이브러리에 적용 불가능

8-3. 빈 등록 초기화, 소멸 메서드

: 스프링 빈 생명주기 콜백 지원 방법 2

NetworkClient 수정

public class NetworkClient
{
    ...

    public void init()
    {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메시지");
    }

    public void close()
    {
        System.out.println("NetworkClient.close");
        disconnect();
    }
}
public class BeanLifeCycleTest
{
    @Configuration
    static class LifeCycleConfig
    {
        @Bean(initMethod = "init", destroyMethod = "close")
        public NetworkClient networkClient()
        {
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        }
    }

    @Test
    ...
}

테스트 실행 결과

생성자 호출, url = null
NetworkClient.init
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
NetworkClient.close
disconnect: http://hello-spring.dev

설정 정보 사용 방식의 장점

  • 메서드 이름 자유롭게 지정 가능
  • 스프링 빈이 스프링 코드에 의존 X
  • 설정 정보를 사용하여 외부 라이브러리에도 적용 가능

종료 메서드 추론

  • @Bean의 destroyMethod는 디폴트값이 (inferred)로 등록되어 있다
  • close, shutdown이라는 이름의 종료 메서드를 자동 호출한다

8-4. 애노테이션 @PostConstruct, @PreDestroy

: 스프링 빈 생명주기 콜백 지원 방법 3

NetworkClient 수정

public class NetworkClient
{
    ...

    @PostConstruct
    public void init()
    {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메시지");
    }

    @PreDestroy
    public void close()
    {
        System.out.println("NetworkClient.close");
        disconnect();
    }
}
public class BeanLifeCycleTest
{
    @Configuration
    static class LifeCycleConfig
    {
        @Bean
        public NetworkClient networkClient()
        {
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        }
    }

    @Test
    ...
}

테스트 실행 결과

생성자 호출, url = null
NetworkClient.init
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
NetworkClient.close
disconnect: http://hello-spring.dev

어노테이션 방식의 장점

  • 최신 스프링에서 가장 권장
  • 어노테이션 하나만 붙이면 되므로 매우 편리
  • 자바 표준 -> 스프링이 아닌 다른 컨테이너에서도 동작
  • 컴포넌트 스캔과 잘 어울린다

⚠️ 단 외부 라이브러리에는 어노테이션 방식을 적용할 수 없다. 외부 라이브러리를 초기화, 종료해야 하는 경우 빈 등록 초기화 방식을 사용해야 한다.

9. 빈 스코프

9-1. 빈 스코프란?

: 스프링 빈은 싱글톤 스코프로 생성되기 때문에 스프링 컨테이너가 만들어질 때 함께 생성되고 종료된다.

스코프(Scope)

  • 빈이 생성되는 범위
  • 빈의 인스턴스가 생성되고 유지되는 시간

스프링이 지원하는 스코프의 종류

  • 싱글톤 : 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
  • 프로토타입 : 요청할 때 생성 -> 스프링 컨테이너가 프로토타입 빈의 생성과 의존관계 주입까지만 관여(종료 메소드 호출 X) -> 더 이상 관리 X
  • 웹 관련 스코프(Spring Web 관련) : request, session, application

스코프 지정 방법

@Scope("prototype")
@Component
public class HelloBean()
@Scope("prototype")
@Bean
PrototypeBean HellloBean()
{
    return new HelloBean();
}

9-2. 프로토타입 스코프

싱글톤 스코프 vs 프로토타입 스코프

  • 싱글톤 스코프의 빈 : 스프링 컨테이너가 항상 같은 인스턴스의 스프링 빈 반환
      1. 싱글톤 스코프의 빈을 스프링 컨테이너에 요청
      1. 스프링 컨테이너가 스프링 빈 반환
      1. 이후에 같은 요청이 들어오면 같은 객체 인스턴스 반환
  • 프로토타입 스코프의 빈 : 스프링 컨테이너가 항상 새로운 인스턴스를 생성하여 스프링 빈 반환
      1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청
      1. 스프링 컨테이너는 이 시점에 새로운 프로토타입 빈을 생성, 의존관계 주입, 초기화
      1. 스프링 컨테이너가 스프링 빈 반환 (더 이상 스프링 컨테이너가 관리 X)
      1. 이후에는 스프링 컨테이너에 같은 요청이 들어오면 항상 새로운 프로토타입 빈을 생성하여 반환
    • 만약 종료메서드를 호출해야 한다면 클라이언트가 책임지고 호출해주어야 함

싱글톤 스코프 테스트 코드

public class SingletonTest
{
    @Scope("singleton")
    static class SingletonBean
    {
        @PostConstruct
        public void init()
        {
            System.out.println("SingletonBean.init");
        }

        @PreDestroy
        public void destroy()
        {
            System.out.println("SingletonBean.destroy");
        }
    }

    @Test
    void singletonBeanFind()
    {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
        SingletonBean bean1 = ac.getBean(SingletonBean.class);
        SingletonBean bean2 = ac.getBean(SingletonBean.class);

        System.out.println("singletonBean1 : " + bean1);
        System.out.println("singletonBean2 : " + bean2);
        assertThat(bean1).isSameAs(bean2);

        ac.close();
    }
}
SingletonBean.init
singletonBean1 : net.core.scope.SingletonTest$SingletonBean@4b3a45f1
singletonBean2 : net.core.scope.SingletonTest$SingletonBean@4b3a45f1
SingletonBean.destroy
  • 스프링 빈 조회 시 항상 같은 인스턴스를 반환한다
  • 스프링 컨테이너 생성 시점에 init()이 실행된다
  • 싱글톤 빈은 스프링 컨테이너가 관리하므로 destroy()가 실행된다

프로토타입 스코프 테스트 코드

public class PrototypeTest
{
    @Scope("prototype")
    static class PrototypeBean
    {
        @PostConstruct
        public void init()
        {
            System.out.println("PrototypeBean.init");
        }

        @PreDestroy
        public void destroy()
        {
            System.out.println("PrototypeBean.destroy");
        }
    }
    @Test
    void prototypeBeanFind()
    {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        System.out.println("find prototypeBean1");
        PrototypeBean bean1 = ac.getBean(PrototypeBean.class);
        System.out.println("find prototypeBean2");
        PrototypeBean bean2 = ac.getBean(PrototypeBean.class);

        System.out.println("prototypeBean1 : " + bean1);
        System.out.println("prototypeBean2 : " + bean2);

        assertThat(bean1).isNotSameAs(bean2);

        ac.close();
    }
}
find prototypeBean1
PrototypeBean.init
find prototypeBean2
PrototypeBean.init
prototypeBean1 : net.core.scope.PrototypeTest$PrototypeBean@4b3a45f1
prototypeBean2 : net.core.scope.PrototypeTest$PrototypeBean@17a87e37
  • 스프링 빈 조회 시 항상 새로운 인스턴스를 반환한다
  • 스프링 컨테이너에서 스프링 빈을 조회할 때 인스턴스가 새로 생성되고 init()이 실행된다
  • 프로토타입 빈은 스프링 컨테이너가 관리하지 않으므로 destroy()가 호출되지 않는다 (필요시 클라이언트가 직접 호출)

9-3. 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점

싱글톤 빈에서 프로토타입 빈을 사용할 때

: github 코드 참조

  • 스프링 컨테이너 생성 시점에 싱글톤 빈 생성, 의존관계 주입, 초기화가 이루어진다
  • 이때 스프링 컨테이너는 새로운 프로토타입 빈을 생성하여 싱글톤 빈에 반환한다
  • 클라이언트 A가 싱글톤 빈을 요청한 후 프로토타입 빈 내부의 값을 변경하는 메소드를 호출한다
  • 클라이언트 B가 싱글톤 빈을 요청한 후 프로토타입 빈 내부의 값을 변경하는 메소드를 호출한다
  • 프로토타입 빈은 이미 주입되어 있고 싱글톤 빈은 동일한 인스턴스이므로, 동일한 프로토타입 빈 인스턴스에 대해 값의 변경이 이루어진다

9-4. 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결

"싱글톤 빈과 프로토타입 빈을 함께 사용할 때 항상 새로운 프로토타입 빈을 생성하는 방법"

스프링 컨테이너에 요청

  • 싱글톤 빈이 프로토타입 빈을 사용할 때마다 스프링 컨테이너에 새로 요청 (ac.getBean())
  • DL(Dependency Lookup) : 의존관계를 외부에서 주입받는 게 아니라 직접 찾는 방식
  • 단점 : 스프링 컨테이너에 종속적이다, 단위 테스트가 어렵다
public int logic()
{
    PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class); // 추가된 부분
    protypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}

ObjectFactory & ObjectProvider

  • DL을 대신 수행해주는(프로토타입 빈을 직접 요청해주는) 역할
  • ObjectFactory : getObject 하나만 제공하는 인터페이스
  • ObjectProvider : ObjectFactory를 상속받아 몇 가지 기능을 추가한 인터페이스
  • 장점 : DL을 대신 수행해준다, 단위테스트를 만들기 쉽다, 별도의 라이브러리가 필요없다
  • 단점 : 스프링에 의존적이다
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider; // 수정된 부분

public int logic()
{
    PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); // 수정된 부분
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}

JSR-330 Provider

  • javax.inject.Provider 활용 (JSR-XXX : 자바 표준)
    • 장점 : DL을 대신 수행해준다, 단위테스트를 만들기 쉽다, 스프링에 의존하지 않는다
  • 단점 : javax.inject:javax.inject:1 라이브러리를 gradle에 추가해야 한다
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider; // 수정된 부분

public int logic()
{
    PrototypeBean prototypeBean = prototypeBeanProvider.get(); // 수정된 부분
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}

프로토타입 빈을 사용하는 상황

  • 사용할 때마다 의존관계 주입이 완료된 새로운 객체가 필요할 때
  • 실무에서는 싱글톤 빈으로 대부분의 문제를 해결할 수 있어 프로토타입 빈을 직접 사용하는 일은 매우 드물다

9-5. 웹 스코프

개념

  • 웹 환경에서만 동작하는 스코프

  • 프로토타입과 달리 해당 스코프의 종료 시점까지 관리 (즉 종료 메서드 호출) request(HTTP 요청이 들어오고 나갈 때까지의 생존 범위를 가지는 스코프), session(세션이 시작될 때부터 종료될 때까지 유지되는 스코프), application(Servlet Context 범위)

  • 웹 스코프의 종류

    request HTTP 요청이 들어오고 나갈 때까지 유지되는 스코프
    session HTTP Session과 동일한 생명주기를 가지는 스코프
    application 서블릿 컨텍스트과 동일한 생명주기를 가지는 스코프
    websocket 웹 소켓과 동일한 생명주기를 가지는 스코프

9-6. request 스코프 예제 만들기

웹 환경 추가

  • build.gradle에 org.springframework.boot:spring-boot-starter-web 추가
dependencies : {
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

⚠️ 웹 라이브러리가 추가되면 웹과 관련된 추가 설정과 환경이 필요하므로 AnnotationConfigApplicationContext 대신 AnnotationConfigServletWebServiceApplicationContext 사용

MyLogger

: github 코드 참조

  • 동시에 여러 HTTP 요청이 오면 어떤 요청이 남긴 로그인지 구분하기 어렵다
  • 이럴 때 request 스코프를 사용해서 로그를 남길 수 있다
    • format : [UUID][requestURL]{message}
    • requestURL 정보를 추가로 넣어 어떤 URL을 요청해서 남은 로그인지 확인
  • 스프링 빈 인스턴스가 생성되는 시점(요청이 생성될 때)에 자동으로 @PostConstruct를 통해 uuid를 생성해서 저장한다.
  • 스프링 빈 인스턴스가 소멸하는 시점에 @PreDestroy를 통해 종료 메시지를 남긴다.
  • requestUrl은 생성 시점에 알 수 없으므로 외부에서 Setter로 입력받는다.

LogDemoController & LogDemoService

  • HttpServletRequest를 통해 요청 URL을 받고 해당 정보를 MyLogger의 setter 함수에 전달한다
  • 만약 request scope를 사용하지 않고 파라미터로 모든 정보를 서비스 계층에 넘기면 파라미터가 너무 많아서 지저분 + 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어간다(유지보수 어려움)
@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();
        myLogger.setRequestUrl(requestUrl);

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}
@Service
@RequiredArgsConstructor
public class LogDemoService
{
    private final MyLogger myLogger;

    public void logic(String id)
    {
        myLogger.log("service id = " + id);
    }
}

실행 결과

  • 오류 발생
  • 스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request scope 빈 인스턴스는 생성되지 않았기 때문
  • 스프링 컨테이너에게 MyLogger를 요청하는 단계를 실제 고객의 요청이 왔을 때로 지연시켜야 한다

9-7. 스코프와 Provider

9-6에서의 오류를 Provider를 통해 해결

  • MyLogger를 주입받는 Controller, Service에 ObjectProvider 추가
  • ObjectProvider를 통해 request scope 빈 인스턴스의 생성 지연
public String logDemo(HttpServletRequest request)
    {
        String requestUrl = request.getRequestURL().toString();
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestUrl(requestUrl);
    
        ...
    }
public void logic(String id)
    {
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("service id = " + id);
    }

실행 결과

  • 하나의 클라이언트 요청에 대해 다음과 같은 로그 메시지를 확인할 수 있다
    [10a02251-7575-426a-95d0-89f627ade987] request scope bean create:net.core.common.MyLogger@13b0ed8e
    [10a02251-7575-426a-95d0-89f627ade987][http://localhost:8080/log-demo] controller test
    [10a02251-7575-426a-95d0-89f627ade987][http://localhost:8080/log-demo] service id = testId
    [10a02251-7575-426a-95d0-89f627ade987] request scope bean close:net.core.common.MyLogger@13b0ed8e
    
  • 다른 클라이언트가 요청을 보내면 uuid와 MyLogger 인스턴스가 다른 로그가 남는다
    [3f78749d-3c4b-4def-874d-38f0f2145d98] request scope bean create:net.core.common.MyLogger@33dc954c
    [3f78749d-3c4b-4def-874d-38f0f2145d98][http://localhost:8080/log-demo] controller test
    [3f78749d-3c4b-4def-874d-38f0f2145d98][http://localhost:8080/log-demo] service id = testId
    [3f78749d-3c4b-4def-874d-38f0f2145d98] request scope bean close:net.core.common.MyLogger@33dc954c
    

9-8. 스코프와 프록시

9-6에서의 오류를 프록시를 통해 해결

  • @Scope에 proxyMode 파라미터 추가 : 적용 대상이 클래스면 TARGET_CLASS, 인터페이스면 INTERFACES 선택
  • MyLogger의 가짜 프록시 클래스를 통해 request와 상관없이 프록시 클래스를 다른 빈에 미리 주입해둘 수 있다
    • CGLIB를 통해 가짜 프록시 객체를 생성해서 주입한다
    • 가짜 프록시는 실제 요청이 오면 내부에서 실제 빈을 요청하는 위임 로직을 포함한다
    • 가짜 프록시는 request scope와 관계가 없으며 싱글톤처럼 동작한다
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger
{
    ...
}

스코프 사용시 주의사항

  • 필요한 곳에만 최소화해서 사용해야 한다
  • 무분별한 사용은 유지보수를 어렵게 한다
Clone this wiki locally