-
Notifications
You must be signed in to change notification settings - Fork 1
스프링 핵심 원리 ‐ 기본편
🌱 객체지향과 스프링을 함께 이해하자
- 애플리케이션을 개발하고 설계하는 시야 넓히기
- IoC, DI, 객체 지향 설계 원칙(SRP, OCP, DIP)
ㅤ
🌱 강의 목표
"스프링을 왜 만들었고 왜 이런 기능을 제공하는지에 대한 핵심 원리에 초점을 맞춘다"
- 스프링 기본 기능 학습하기
- 스프링 본질 깊이 이해하기
- 객체 지향 설계를 고민하는 개발자로 성장하기
ㅤ
ㅤ
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 출시
ㅤ
스프링 생태계
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에 의존적으로 개발을 해야 한다
- 객체 지향이 가진 장점을 모두 잃어버린다
-
핵심 컨셉
- 자바 언어(객체 지향 언어) 기반의 프레임워크
- 좋은 객체 지향 애플리케이션을 개발할 수 있도록 돕는다
ㅤ
객체 지향 프로그래밍의 정의
- 컴퓨터 프로그램을 "객체"들의 모임으로 파악하고자 하는 것
- 객체는 메시지를 주고받고 데이터를 처리할 수 있다 (협력)
- 유연하고 변경이 용이하여 대규모 소프트웨어 개발에 많이 사용 -> 컴포넌트를 쉽고 유연하게 변경하면서 개발 가능 (Polymorphism)
ㅤ
다형성(Polymorphism)
"역할(인터페이스)과 구현(실체화한 클래스, 구현체)으로 객체 구분"
- 구현체가 바뀌어도 인터페이스를 의존하고 있는 다른 객체에 영향을 주지 않는다
- 클라이언트가 구현체의 내용을 알지 못해도 된다
- 다른 대상으로 변환이 가능하고 새로운 구현체가 나와도 기존의 인터페이스를 그대로 사용할 수 있다
- 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있다 = 클라이언트를 변경하지 않고 서버의 기능을 유연하게 변경할 수 있다
✓ 따라서 인터페이스를 안정적으로(변화가 없도록) 잘 설계하는 것이 중요하다
ㅤ
스프링과 객체 지향
- 다형성 극대화 : 제어의 역전, 의존관계 주입은 모두 다형성을 활용한 기능
- 객체 지향 설계의 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();
ㅤ
스프링이 SOLID를 준수하는 방법
- DI(Dependency Injection) : DI 컨테이너를 제공하여 의존성 주입
- 클라이언트 코드를 변경하지 않고 기능 확장
ㅤ
객체 지향 설계의 이상과 현실
- 이상 : 역할과 구현을 분리하여 모든 설계에 인터페이스 부여
- 현실 : 추상화 비용 발생 -> 확장 가능성이 없다면 구체 클래스를 직접 사용하고 향후에 필요할 때 리팩토링하여 인터페이스 도입
ㅤ
ㅤ
Setting
- https://start.spring.io
- Gradle -Groovy 프로젝트 생성
- Dependencies는 선택하지 않는다
ㅤ
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()
}
ㅤ
회원 도메인 요구사항
- 회원을 가입하고 조회할 수 있다 (회원 서비스)
- 회원은 일반과 VIP 두 가지 등급이 있다 (도메인에 드러나야 함)
- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다 (인터페이스 필요)
ㅤ
주문과 할인 정책 요구사항
- 회원은 상품을 주문할 수 있다 (주문 서비스)
- 회원 등급에 따라 할인 정책을 적용할 수 있다 (도메인에 드러나야 함)
- 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용하는데 추후 변경될 수 있다 (인터페이스 필요)
- 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고 최악의 경우 할인을 적용하지 않을 수도 있다
ㅤ
- 클라이언트는 회원 서비스를 활용한다
- 회원 도메인에는 id, name, grade가 포함된다
- 회원 서비스 인터페이스와 구현체를 별도로 생성한다
- 회원 서비스에는 회원가입과 회원조회가 포함된다
- 회원 리포지토리 인터페이스와 구현체를 별도로 생성한다
- 회원 서비스는 회원 리포지토리 인터페이스를 의존한다
ㅤ
- github 코드 참조
ㅤ
회원 도메인 설계의 문제점
- github 코드 참조
- 의존관계가 인터페이스뿐만 아니라 구현체까지 모두 의존하고 있다
ㅤ
- github 코드 참조
ㅤ
- github 코드 참조
ㅤ
주문과 할인 도메인 설계의 문제점
- github 코드 참조
- 의존관계가 인터페이스뿐만 아니라 구현체까지 모두 의존하고 있다
ㅤ
ㅤ
새로운 요구사항의 추가
- 고정 금액 할인이 아닌 퍼센트 할인을 희망
- 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);
}
}
ㅤ
기존 방식의 문제점
- 할인 정책을 변경하기 위해 OrderServiceImpl에서 코드 수정 발생
- OCP, DIP를 준수하지 못함
- 추상 인터페이스 : DiscountPolicy
- 구현 클래스 : FixDiscountPolicy, RateDiscountPolicy
- 기존 방식은 기능을 변경하면 서비스 클라이언트 코드가 변화함 -> OCP 위반
- 기존 방식은 서비스 클라이언트가 인터페이스와 구현체를 모두 의존함 -> DIP 위반
ㅤ
해결 방안
- OrderServiceImpl은 인터페이스만 의존하게 한다
- OrderServiceImpl을 대신하여 구현 객체를 생성하는 별도의 클래스를 생성한다
ㅤ
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();
}
ㅤ
기존 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());
}
}
ㅤ
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());
}
}
ㅤ
관심사의 분리
- 객체는 주어진 역할을 수행하는 데만 집중해야 한다
- 객체가 다른 인터페이스를 의존할 때 해당 인터페이스가 어떤 구현체를 사용하고 있든 동일하게 작업을 수행해야 한다
- 객체를 구현하는 역할을 담당하는 별도의 객체(AppConfig)가 필요하다
- AppConfig는 애플리케이션의 전체 동작 방식을 구성하기 위해 구현 객체를 생성하고 연결하는 책임을 가진다
ㅤ
SRP 단일 책임 원칙 | 관심사의 분리 > 구성(AppConfig), 사용(Service, Repository 등) |
DIP 의존관계 역전 원칙 | AppConfig를 통해 객체를 대신 생성하여 의존관계 주입 |
OCP 개방-폐쇄 원칙 | 소프트웨어를 변경해도 사용 영역이 변경되지 않음 |
ㅤ
제어의 역전 (IoC)
: Inversion of Control
- 프로그램의 제어 흐름을 직접 관리하지 않고 외부에서 관리하는 것
- 프로그램의 제어 흐름에 대한 권한을 모두 AppConfig가 보유
* 프레임워크 : 내가 작성한 코드를 제어하고 대신 실행 * 라이브러리 : 내가 작성한 코드가 직접 제어 흐름을 담당
ㅤ
의존 관계 주입 (DI)
: Dependency Injection
- 정적인 클래스 의존관계 : import하는 클래스만 보고 바로 판단할 수 있는 의존관계
- OrderServiceImpl은 MemberRepository와 DiscountPolicy를 의존한다는 것을 알 수 있다
- 하지만 클래스 의존관계만으로는 OrderServiceImpl에 실제로 어떤 객체가 주입될지는 알 수 없다
- 동적인 인스턴스 의존관계 : 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존관계
- 의존관계 주입 : 런타임에 실제 구현 객체를 생성하고 클라이언트에 전달해서 의존관계가 연결되는 것
ㅤ
IoC 컨테이너, DI 컨테이너
- AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것
ㅤ
구성을 위한 어노테이션
- @Configuration : 설정 정보에 기재하는 어노테이션
- @Bean: 설정 파일의 각 메소드에 기재하는 어노테이션 -> 스프링 컨테이너에 객체를 스프링 빈으로 등록
ㅤ
Spring Container 적용하기
- Applcation 클래스에 ApplicationContext 추가
- ApplicationContext가 모든 객체를 관리하는 Spring Container라고 보면 됨
- 이전에는 개발자가 필요한 객체를 AppConfig를 통해 직접 조회 -> ApplicationContext를 사용하는 경우 applicationContext.getBean()을 통해 스프링 빈을 찾을 수 있음
ㅤ
스프링 컨테이너 생성 코드
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
- 스프링 컨테이너는 XML 기반으로 만들거나 어노테이션 기반의 자바 설정 클래스로 만들 수 있다
- ApplicationContext : 스프링 컨테이너 인터페이스
- AnnotationConfigApplicationContext : 어노테이션 기반의 자바 설정 클래스(구현체)
ㅤ
스프링 컨테이너 생성 과정
-
스프링 컨테이너 생성(위의 코드) : 구성 정보(AppConfig.class)를 지정한 후 스프링 컨테이너 생성
-
스프링 빈 생성 : 파라미터로 넘어온 구성 정보를 사용해 스프링 빈 등록
** 스프링 컨테이너 안에는 스프링 빈 저장소 존재 : key = 빈 이름(메서드 이름), value = 빈 객체(반환 객체) ** 빈의 이름은 일반적으로 메서드 이름을 사용하지만 직접 부여할 수도 있다. 단, 빈의 이름은 항상 다른 이름을 부여해야 한다
-
스프링 빈 의존관계 설정 : 구성 정보를 참고하여 의존관계 주입
ㅤ
모든 빈을 조회하는 테스트 코드 작성
- 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
ㅤ
getBean()을 통한 조회
- github 코드 참조 (ApplicationContextBasicTest)
- ac.getBean(빈이름, 타입)
조회 방법 특징 인터페이스 타입으로 조회 역할에 의존 X -> 유언성 증가 구체 타입으로 조회 역할에 의존 -> 유연성의 감소, 그러나 가끔 필요할 때 있음 - ac.getBean(타입)
- 조회할 스프링 빈이 존재하지 않으면
NoSuchBeanDefinitionException
발생
ㅤ
타입 중복 오류
- 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);
}
ㅤ
스프링 빈 조회의 상속 원칙
- github 코드 참조 (ApplicationConfigExtendsFindTest)
- 부모 타입으로 조회 시 자식이 둘 이상이면 오류 발생
- 빈 이름을 지정하여 문제 해결
- ac.getBeanOfType()을 사용하면 부모 타입으로 모두 조회 가능
- 부모 타입으로 조회하면 자식 타입도 함께 조회
- 예를 들어 자바 객체의 최고 부모인 Object로 조회하면 모든 스프링 빈 조회
ㅤ
상속 관계
BeanFactory | 인터페이스 | 스프링 컨테이너의 최상위 인터페이스 |
ApplicationContext | 인터페이스 | BeanFactory에 부가기능을 더한 것 |
AnnotationConfigApplicationContext | 구현클래스 | ApplicationContext를 상속받은 어노테이션 기반의 자바 설정 클래스 |
ㅤ
BeanFactory
- 스프링 빈을 관리하고 조회하는 역할 담당
- getBean() 제공
ㅤ
ApplicationContext
: BeanFactory의 내용을 모두 상속받아 제공 (빈 관리 및 조회)
- MessageSource 상속 : 국제화 기능
- EnvironmentCapable 상속 : 환경변수(로컬, 개발, 운영 등을 구분하여 처리)
- ApplicationEventPublisher 상속 : 이벤트를 발행하고 구독하는 모델 지원
- ResourceLoader 상속 : 파일, 클래스패스, 외부 등에서 리소스 조회
ㅤ
스프링 컨테이너의 다양한 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);
}
}
ㅤ
"스프링 설정 형식은 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);
}
}
}
ㅤ
ㅤ
웹 애플리케이션
- 웹 애플리케이션 : 프로세스와 프로세스 사이에 HTTP를 통해 정보를 주고받는 소프트웨어, 일반적으로 클라이언트 프로세스와 서버 프로세스가 있다
- 스프링은 배치 프로세스나 데몬 프로세스를 관리하기도 하지만 "웹 애플리케이션"이 주목적
배치(Batch) 일련의 작업을 지정된 시각에 실행하는 프로세스 데몬(Daemon) 특정 서비스를 위해 백그라운드에서 항상 실행되는 프로세스
ㅤ
웹 애플리케이션에서 싱글톤의 필요성
- 순수한 DI 컨테이너 : 다수의 클라이언트가 서비스를 요청하면 JVM에 객체 인스턴스가 계속 생성된다(만약 스프링이 없는 순수한 DI 컨테이너라면 AppConfig가 요청을 할 때마다 객체를 새로 생성) -> 극심한 메모리 낭비
- 싱글톤 패턴 : 하나의 객체 인스턴스만이 JVM 안에 존재하고 그것을 공유한다
ㅤ
정의
- 클래스의 인스턴스가 딱 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를 적용하기 어렵다)
ㅤ
스프링 컨테이너
- 싱글톤 패턴을 적용하지 않으면서 객체 인스턴스를 싱글톤으로 관리하는 컨테이너
ㅤ
테스트 코드
@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);
}
ㅤ
무상태(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 변수에 한 번 지정된 값은 수정할 수 없다 -> 객체가 생성될 때 해당 변수를 초기화한 후 더 이상 변수가 수정되지 않도록 물리적으로 막을 수 있는 방법
ㅤ
"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);
}
}
ㅤ
바이트 코드 조작을 통한 싱글톤 보장
- AnnotationConfigApplicationContext에 AppConfig가 파라미터로 전달되면 AppConfig 자체가 스프링 빈이 되지 않고 AppConfig를 상속받은 AppConfig@CGLIB가 스프링 빈으로 등록된다
- CGLIB : 바이트코드를 조작하는 라이브러리
- @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 새로 생성하는 코드를 동적으로 생성
- 싱글톤 보장
- 바이트 코드 조작은 @Configuration을 통해 이루어진다
ㅤ
만약 @Configuration을 사용하지 않는다면?
- @Bean만 사용하는 경우 싱글톤이 보장되지 않는다
ㅤ
ㅤ
지금까지의 스프링 빈 등록 방법
- 한땀한땀 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")
ㅤ
컴포넌트 스캔의 시작 위치 지정
- basePackages = { 경로 }를 통해 컴포넌트 스캔의 시작 디렉토리를 지정할 수 있다
- 시작 위치를 지정하지 않으면 모든 자바 파일을 확인하기 때문에 비효율적
- 이때 컴포넌트 스캔의 범위는 시작 디렉토리부터 하위 디렉토리 (설정 정보 클래스를 프로젝트의 최상위 디렉토리에 두면 편리하다 -> @SpringBootApplication은 @ComponentScan을 포함하고 있기 때문에 root에 두는 것이 관례)
@Component(
basePackages = "net.core"
)
ㅤ
컴포넌트 스캔의 대상
- @Component는 물론이고 @Component를 포함한 다른 어노테이션도 컴포넌트 스캔 대상
- @Controller : 스프링 MVC 컨트롤러로 인식
- @Service : 스프링 비즈니스 계층 인식
- @Repository : 스프링 데이터 접근 계층으로 인식 -> 데이터 계층의 예외를 스프링 예외로 변환
- @Configuration : 스프링 설정 정보로 인식 -> 싱글톤을 유지하도록 추가 처리
ㅤ
필터의 종류
- 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 코드 참조
ㅤ
같은 이름으로 스프링 빈이 등록되는 경우
- 자동 등록 빈 = 자동 등록 빈 : ConflictingBeanDefinitionException 발생
- 자동 등록 빈 = 수동 등록 빈 : 수동 빈 등록이 우선권을 가짐, 그러나 충돌이 발생하면 스프링부트 오류 발생
ㅤ
생성자 주입
- 생성자를 통해 의존관계 주입
- 불변 의존관계 : 한 번 값을 정하면 변하지 않는다
- 필수 의존관계 : 반드시 의존관계가 주입되어야 한다
ㅤ
수정자 주입 (Setter 주입)
- 수정자 메서드를 통해 의존관계 주입
- 변경 의존관계 : 값이 변경될 가능성이 있다
- 선택 의존관계 : 의존관계가 반드시 주입될 필요가 없을 가능성이 있다
ㅤ
필드 주입
- 필드에 바로 의존관계 주입
- 외부에서 변경 불가능하여 테스트하기 어렵다
- 웬만하면 사용 금지
ㅤ
일반 메서드 주입
- 한 번에 여러 필드 주입 가능
- 일반적으로 사용 X
ㅤ
선택 의존관계에서 사용
- 의존관계가 주입되지 않은 상태에서 동작할 때 옵션 처리
- @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' }
ㅤ
생성자 주입의 장점
- 대부분의 의존관계는 변경할 일이 없다 -> 불변성 보장
- 주입해야 하는 데이터가 누락되었을 때 컴파일 오류가 발생한다
- 필드에 final 키워드를 함께 사용할 수 있다 -> 생성자에서 값이 설정되지 않는 오류를 방지한다 (수정자 주입은 final을 사용할 수 없다)
ㅤ
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 : 모든 필드의 값을 파라미터로 받아 설정하는 생성자 자동 생성
ㅤ
ac.getBean(DiscountPolicy.class)
- 만약 DiscountPolicy 타입을 가진 빈이 여러 개라면 타입 조회시 오류 발생
- DiscountPolicy의 하위 타입인 FixedDiscountPolicy, RateDiscountPolicy를 모두 스프링 빈으로 선언하기 위해서는 별도의 조치 필요
- @Component만 사용할 경우 NoUniqueBeanDefinitionException 발생
ㅤ
@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;
}
ㅤ
- github 코드 참조
ㅤ
의도적으로 특정 타입의 스프링 빈이 모두 필요한 경우
- 클라이언트가 스프링 빈을 선택할 수 있는 권한이 있는 경우
- List, Map을 사용하여 전략 패턴을 간단히 구현
- github 코드 참조
ㅤ
자동 빈 등록
- 업무 로직 빈(컨트롤러, 서비스, 리포지토리 등)
- 스프링과 스프링부트가 자동으로 등록하는 수많은 빈
- 관리할 빈이 많아 설정 정보가 커지는 경우 사용
ㅤ
수동 빈 등록
- 기술 지원 빈(기술적 문제나 AOP 처리) : 어디서 문제가 발생했는지 명확하게 드러나지 않아 수동 빈 등록으로 명확하게 하는 것이 좋다
- 다형성을 적극적으로 활용하는 업무 로직 빈 : 다형성을 가진 각 빈의 이름을 코드만 보고 쉽게 파악할 수 없다 -> 한 번에 파악할 수 있도록 별도의 설정 파일을 작성하는 것이 좋다
@Configuration public class DiscountPolicyConfig { @Bean public DiscountPolicy rateDiscountPolicy() { return new RateDiscountPolicy(); } @Bean public DiscountPolicy fixDiscountPolicy() { return new FixDiscountPolicy(); } }
ㅤ
ㅤ
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 하기 전에 호출
ㅤ
스프링 빈의 이벤트 라이프사이클
- 스프링 컨테이너 생성
- 스프링 빈 생성
- 의존관계 주입
- 초기화 콜백
- 소멸 전 콜백
- 스프링 종료
ㅤ
: 스프링 빈 생명주기 콜백 지원 방법 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
ㅤ
인터페이스 방식의 단점
- 스프링 전용 인터페이스에 의존
- 초기화, 소멸 메서드의 이름 변경 불가
- 외부 라이브러리에 적용 불가능
ㅤ
: 스프링 빈 생명주기 콜백 지원 방법 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이라는 이름의 종료 메서드를 자동 호출한다
ㅤ
: 스프링 빈 생명주기 콜백 지원 방법 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
ㅤ
어노테이션 방식의 장점
- 최신 스프링에서 가장 권장
- 어노테이션 하나만 붙이면 되므로 매우 편리
- 자바 표준 -> 스프링이 아닌 다른 컨테이너에서도 동작
- 컴포넌트 스캔과 잘 어울린다
ㅤ
: 스프링 빈은 싱글톤 스코프로 생성되기 때문에 스프링 컨테이너가 만들어질 때 함께 생성되고 종료된다.
스코프(Scope)
- 빈이 생성되는 범위
- 빈의 인스턴스가 생성되고 유지되는 시간
ㅤ
스프링이 지원하는 스코프의 종류
- 싱글톤 : 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
- 프로토타입 : 요청할 때 생성 -> 스프링 컨테이너가 프로토타입 빈의 생성과 의존관계 주입까지만 관여(종료 메소드 호출 X) -> 더 이상 관리 X
- 웹 관련 스코프(Spring Web 관련) : request, session, application
ㅤ
스코프 지정 방법
@Scope("prototype")
@Component
public class HelloBean()
@Scope("prototype")
@Bean
PrototypeBean HellloBean()
{
return new HelloBean();
}
ㅤ
싱글톤 스코프 vs 프로토타입 스코프
- 싱글톤 스코프의 빈 : 스프링 컨테이너가 항상 같은 인스턴스의 스프링 빈 반환
-
- 싱글톤 스코프의 빈을 스프링 컨테이너에 요청
-
- 스프링 컨테이너가 스프링 빈 반환
-
- 이후에 같은 요청이 들어오면 같은 객체 인스턴스 반환
-
- 프로토타입 스코프의 빈 : 스프링 컨테이너가 항상 새로운 인스턴스를 생성하여 스프링 빈 반환
-
- 프로토타입 스코프의 빈을 스프링 컨테이너에 요청
-
- 스프링 컨테이너는 이 시점에 새로운 프로토타입 빈을 생성, 의존관계 주입, 초기화
-
- 스프링 컨테이너가 스프링 빈 반환 (더 이상 스프링 컨테이너가 관리 X)
-
- 이후에는 스프링 컨테이너에 같은 요청이 들어오면 항상 새로운 프로토타입 빈을 생성하여 반환
- 만약 종료메서드를 호출해야 한다면 클라이언트가 책임지고 호출해주어야 함
-
ㅤ
싱글톤 스코프 테스트 코드
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()가 호출되지 않는다 (필요시 클라이언트가 직접 호출)
ㅤ
싱글톤 빈에서 프로토타입 빈을 사용할 때
: github 코드 참조
- 스프링 컨테이너 생성 시점에 싱글톤 빈 생성, 의존관계 주입, 초기화가 이루어진다
- 이때 스프링 컨테이너는 새로운 프로토타입 빈을 생성하여 싱글톤 빈에 반환한다
- 클라이언트 A가 싱글톤 빈을 요청한 후 프로토타입 빈 내부의 값을 변경하는 메소드를 호출한다
- 클라이언트 B가 싱글톤 빈을 요청한 후 프로토타입 빈 내부의 값을 변경하는 메소드를 호출한다
- 프로토타입 빈은 이미 주입되어 있고 싱글톤 빈은 동일한 인스턴스이므로, 동일한 프로토타입 빈 인스턴스에 대해 값의 변경이 이루어진다
ㅤ
"싱글톤 빈과 프로토타입 빈을 함께 사용할 때 항상 새로운 프로토타입 빈을 생성하는 방법"
스프링 컨테이너에 요청
- 싱글톤 빈이 프로토타입 빈을 사용할 때마다 스프링 컨테이너에 새로 요청 (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;
}
ㅤ
프로토타입 빈을 사용하는 상황
- 사용할 때마다 의존관계 주입이 완료된 새로운 객체가 필요할 때
- 실무에서는 싱글톤 빈으로 대부분의 문제를 해결할 수 있어 프로토타입 빈을 직접 사용하는 일은 매우 드물다
ㅤ
개념
-
웹 환경에서만 동작하는 스코프
-
프로토타입과 달리 해당 스코프의 종료 시점까지 관리 (즉 종료 메서드 호출) request(HTTP 요청이 들어오고 나갈 때까지의 생존 범위를 가지는 스코프), session(세션이 시작될 때부터 종료될 때까지 유지되는 스코프), application(Servlet Context 범위)
-
웹 스코프의 종류
request HTTP 요청이 들어오고 나갈 때까지 유지되는 스코프 session HTTP Session과 동일한 생명주기를 가지는 스코프 application 서블릿 컨텍스트과 동일한 생명주기를 가지는 스코프 websocket 웹 소켓과 동일한 생명주기를 가지는 스코프
ㅤ
웹 환경 추가
- build.gradle에 org.springframework.boot:spring-boot-starter-web 추가
dependencies : {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
ㅤ
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-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-6에서의 오류를 프록시를 통해 해결
- @Scope에 proxyMode 파라미터 추가 : 적용 대상이 클래스면 TARGET_CLASS, 인터페이스면 INTERFACES 선택
- MyLogger의 가짜 프록시 클래스를 통해 request와 상관없이 프록시 클래스를 다른 빈에 미리 주입해둘 수 있다
- CGLIB를 통해 가짜 프록시 객체를 생성해서 주입한다
- 가짜 프록시는 실제 요청이 오면 내부에서 실제 빈을 요청하는 위임 로직을 포함한다
- 가짜 프록시는 request scope와 관계가 없으며 싱글톤처럼 동작한다
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger
{
...
}
ㅤ
스코프 사용시 주의사항
- 필요한 곳에만 최소화해서 사용해야 한다
- 무분별한 사용은 유지보수를 어렵게 한다