Search

[강의 요약] 김영한 - 스프링 핵심 원리 - 기본편

Last update: @2/14/2023
주의
본 포스팅은 인프런 강의를 통해 학습한 내용을 임의로 요약한 것으로 일부 내용의 오류 및 누락, 링크 숨김 등이 존재합니다.

객체 지향 설계와 스프링

인터페이스를 통한 역할과 구현의 분리가 핵심
SOLID 원칙
SRP: 단일 책임 원칙 (Single Responsibility Priciple)
한 클래스는 하나의 책임만. 변경 시 파급 효과가 적을수록 책임의 크기를 잘 조절한 것
OCP: 개방-폐쇄 원칙(Open/Closed Principel)
소프트웨어 요소는 확장에는 열려 있고 변경에는 닫혀 있어야 함. 다형성을 활용
LSP: 리스코프 치환 법칙 (Liskov Substitution Priciple)
구현체는 인터페이스에서 기대하는 동작(규약)을 구현해야 함
ISP: 인터페이스 분리 원칙 (Interface Segregation Priciple)
특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 나음
DIP: 의존관계 역전 원칙 (Dependency Inversion Principle)
추상화에 의존해야지 구체화에 의존하면 안 됨. 구현 클래스에 의존하지 말고 인터페이스에 의존
다형성만으로는 OCP와 DIP를 지킬 수 없음
직접 코드 내에서 구현체를 지정해줘야 하기 때문
스프링은 DI 및 DI 컨테이너 제공을 통해 OCP, DIP를 가능하게 지원함
클라이언트 코드의 변경 없이 기능 확장 가능

의존성 주입 (Depenadency Injection, DI)

AppConfig
구현 객체를 생성하고 연결해주는 별도의 설정 클래스
클래스에서는 이 AppConfig 객체를 생성해서 의존하는 객체를 받아 씀
정적인 클래스 의존관계(클래스 다이어그램)와 동적인 클래스 의존관계(객체 다이어그램) 구분
애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라고 함
AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC(Inversion of Control) 컨테이너 또는 DI 컨테이너라고 함
요즘은 DI 컨테이너라고 주로 부르고 어샘블러, 오브젝트 팩토리 등으로 부르기도 함

스프링 컨테이너와 스프링 빈

AppConfig 클래스에 @Configuration, 내부 메서드에 @Bean 추가
@Configuration public class AppConfig { @Bean public static MemberRepository getMemberRepository() { return new MemoryMemberRepository(); } ... }
Java
복사
이제 AppConfig 대신 ApplicationContext라는 스프링 컨테이너를 통해 의존관계를 주입받음
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); MemberService memberService = ac.getBean("memberService", MemberService.class);
Java
복사
스프링 컨테이너에 등록된 객체를 스프링 빈이라고 함
@Bean이 붙은 메서드 명을 빈 이름으로 사용
config 클래스 외에 xml, groovy 등 다양한 설정 형식을 지원함
이는 BeanDefinition을 통한 추상화 덕분임
수동 등록 vs 자동 등록
자동 등록
비즈니스 로직 빈
수동 등록
비즈니스 로직 중 다형성 활용 시(정액/정량 할인 선택 등)
기술 지원 빈 - 공통 관심사(AOP) 처리(DB 연결, 로깅 등)

빈 조회

모든 빈 조회
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); void findAllBean() { String[] beanDefinitionNames = ac.getBeanDefinitionNames(); for (String beanDefinitionName : beanDefinitionNames) { Object bean = ac.getBean(beanDefinitionName); System.out.println("name = " + beanDefinitionName + ", object = " + bean); } }
Java
복사
이름으로 조회
void findBeanByName() { MemberService memberService = ac.getBean("memberService", MemberService.class); System.out.println("memberService = " + memberService); System.out.println("memberService.getClass() = " + memberService.getClass()); }
Java
복사
타입으로 조회
void findBeanByType() { MemberService memberService = ac.getBean(MemberService.class); Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class); }
Java
복사
타입으로 조회 시 같은 타입이 둘 이상 있으면, 빈 이름을 지정
void findBeanByName() { MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class); assertThat(memberRepository).isInstanceOf(MemoryMemberRepository.class); }
Java
복사
특정 타입 모두 조회
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); }
Java
복사
부모 타입으로 조회 시 자식이 둘 이상 있으면 이름 지정
void findBeanByParentTypeBeanName() { DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class); assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class); }
Java
복사

BeanFactory, Application Context

BeanFactory는 빈을 관리하고 조회하는 스프링 컨테이너 최상위 인터페이스(getBean() 제공)
Application Context가 제공하는 부가기능
메시지소스를 활용한 국제화
환경변수로 로컬, 개발, 운영 등을 구분해서 처리
애플리케이션 이벤트로 이벤트를 발행하고 구독하는 모델을 편리하게 지원
파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회

싱글톤

여러 객체가 필요 없는 클래스는 싱글톤으로 관리
수동으로 싱글톤을 구현하려면
public class SingletonService { //1. static 영역에 객체를 딱 1개만 생성해둔다. private static final SingletonService instance = new SingletonService(); //2. public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용한다. public static SingletonService getInstance() { return instance; } //3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다. private SingletonService() { } public void logic() { System.out.println("싱글톤 객체 로직 호출"); } }
Java
복사
이런 방식으로 AppConfig에서도 모두 구현해야하고, 싱글톤은 아래와 같은 문제가 있음
코드 양이 많아짐
클라이언트가 구체 클래스에 의존함(DIP 위반, OCP 위반)
테스트하기 어려움
내부 속성을 변경하거나 초기화하기 어려움
private 생성자를 쓰기 때문에 자식 클래스를 만들기 어려움
결론적으로 유연성이 떨어짐
하지만 스프링에서는 등록된 빈을 모두 싱글톤으로 관리해주고 위 문제들을 모두 해결해줌(싱글톤 레지스트리)
스프링의 기본 빈 등록 방식은 싱글톤이고, 이 외의 Http request, session 등의 라이프사이클에 맞추는 경우에는 싱글톤 이외의 방식을 사용함
스프링 빈은 절대 공유필드를 만들지 말고 항상 무상태(stateless)로 설계해야 함
인스턴스 필드로 만들더라도 싱글톤으로 유지되기 때문에 정적 필드와 같아져버림
스프링은 @Configuration 어노테이션이 붙은 빈 설정파일을 CGLIB이라는 바이트코드 조작 라이브러리를 사용해서 코드를 수정함. 이를 통해 각 빈들에 주입되는 빈이 모두 싱글톤이 되도록 함
단, @Configuration 없이 @Bean 어노테이션만으로는 싱글톤이 보장되지 않음

컴포넌트 스캔

설정 파일에 @Configuration@ComponentScan을 같이 붙이고 빈으로 등록할 클래스에 @Component를 붙여주면 @Component가 붙은 클래스가 빈으로 자동 등록됨
빈 이름은 클래스 이름 맨 앞자리를 소문자로 바꿔서 등록
컴포넌트 스캔 옵션
필터(스캔 제외 대상, 포함 대상)
@Configuration @ComponentScan( includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class), excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class) ) static class ComponentFilterAppConfig { }
Java
복사
탐색 위치 지정 - 해당 패키지 및 하위 모든 패키지, 여러개 지정 가능
@ComponentScan( basePackages = "hello.core" )
Java
복사
없으면 @ComponentScan이 붙은 설정 정보의 클래스 패키지가 시작 위치
@Component가 붙어 있는 어노테이션 종류
@Configuration
@Service
@Controller
@Repository
@SpringBootApplication 어노테이션은 스프링 부트의 대표 시작 정보
해당 어노테이션안에 @ComponentScan이 포함되어있음
시작 루트 위치에 두는 것이 관례
빈 이름 충돌 시 수동 등록이 우선함. 최근 버전에는 충돌나면 오류 발생

@Autowired

@Autowired를 붙이면 스프링이 의존관계를 자동으로 주입해줌
스프링에게 가지고 있는 빈이 있다면 매개변수나 필드변수에 연결(wire)해달라고 부탁하는 것
다음에 붙여서 의존성 주입 가능
생성자 및 수정자(Setter) 주입
파라미터 타입에 맞춰 주입해줌
 생성자가 하나일 경우 @Autowired 생략 가능
 생성자 주입 추천
수정자(setter) 주입
파라미터 타입에 맞춰 주입해줌
옵션이 필요할 경우 사용
필드 주입
코드가 간결하지만 DI 프레임워크 없이는 아무것도 할 수 없음
@Configuration같은 곳 외에는 사용하지 말자
옵션
자동 주입할 대상이 없을 경우
@Autowired(required=false): 수정자 메서드 자체가 호출이 안 됨
@Nullable: null이 입력됨
주입 대상이 Optional타입: Optional.empty가 주입됨
타입으로 조회하기 때문에 같은 타입에 여러 구현체가 있을 경우 충돌함
필드명 매칭
주입받을 파라미터 이름을 구체 빈 이름으로 등록 (ex> DiscountPolicy rateDiscountPolicy)
@Qualifier
추가 구분자 사용
@Component @Qualifier("mainDiscountPolicy") public class RateDiscountPolicy implements DiscountPolicy{...}
Java
복사
위는 빈, 아래는 주입받는 곳
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy ) {...}
Java
복사
@Primary
빈 클래스에 위 어노테이션을 붙이면 충돌 시 우선 주입
@Qaulifier@Primary보다 우선순위가 높음
자주 사용하는 메인에 @Primary를 붙이고 가끔 사용할 때만 @Qualifier를 붙이는 전략이 좋음
같은 타입의 여러 빈을 Map 또는 List에 주입받아 사용할 수 있음
타입 체크를 위한 어노테이션 직접 제작 및 활용

롬복(Lombok)

@Data
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
@Getter, @Setter
@AllArgsConstructor
@Slf4J
설정에서 Annotation Processors 활성화 필요
최신 IntelliJ에는 플러그인이 번들로 따라옴

빈 생명주기 콜백

DB 커넥션 풀, 네크워크 소켓 등에서 사용
빈 이벤트 라이프 사이클
스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 > 사용 -> 소멸전 콜백 -> 스프링 종료
Java
복사
초기화 : 필요한 객체와 연결되고 값이 세팅되어 사용할 수 있는 상태
객체 생성과 초기화를 분리
생성자에서 가급적 초기화 x (단일책임에서 벗어나 유지보수가 어려워짐)
빈 생명주기 콜백 지원 방식
인터페이스(InitializingBean, DisposableBean) → 거의 사용 안함
@PostConstruct, @PreDestroy
메서드에 @PostConstruct, @PreDestroy 기재
최신 스프린에서 권장, JSR-250 자바 표준을 따르기 때문에 다른 컨테이너에서도 동작
외부 라이브러리에서는 사용 불가
설정 정보에 초기화 메서드, 종료 메서드 지정
@Bean(initMethod = "init", destroyMethod = "close")
Java
복사
→ 외부 라이브러리 사용 시 쓰는 방법

빈 스코프

빈 클래스에 @Scope("스코프이름") 어노테이션으로 지정
빈이 존재하는 범위
싱글톤(기본값)
컨테이너 시작부터 종료까지 유지되는 스코프
프로토타입(prototype)
컨테이너가 빈의 생성 및 의존관계 주입까지만 관여하고 이후 더 관리하지 않는 스코프
컨테이너에서 받아 쓸 때마다 새로운 객체 생성
@PreDestroy 메서드가 실행되지 않기 때문에 수동으로 실행해줘야 함
웹 관련 스코프
request : 웹 요청이 들어오고 나갈 때까지 유지되는 스코프
web request가 들어와야만 생성됨
따라서 request 요청 이전에 의존성 주입을 하려면 아래 두 방법을 사용
ObjectProvider를 통해 request 요청 시점에 컨테이너에 요청(DL)
request 스코프 빈에 프록시를 설정해서 가짜 프록시 클래스를 다른 빈에 미리 주입
@Component @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
Java
복사
이 역시 CGLIB을 통해 바이트조작된 가짜 프록시 객체를 스프링 컨테이너에 등록
가짜 프록시 객체 내에는 진짜 빈을 요청하는 위임 로직이 있고, 싱글톤처럼 동작
진짜 빈들은 싱글톤이 아니니 주의해야 함
session: 웹 세션이 생성되고 종료될 때까지 유지되는 스코프
application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프
스프링은 웹 라이브러리가 추가되면 AnnotationConfigServletWebServerApplicationContext를 기반으로 애플리케이션으로 구동함
포트 변경 설정은 application.properties에 server.port=9090과 같이 설정
싱글톤 빈에서 프로토타입 빈을 주입받는 경우 프로토타입 빈도 싱글톤처럼 유지되는 문제 해결
스프링 컨테이너에게 매번 받아 쓰기 - 의존관계 탐색(Dependency Lookup, DL)
ObjectFactory, ObjectProvider 사용
@Autowired private ObjectProvider<PrototypeBean> prototypeBeanProvider;
Java
복사
ObjectFactory : 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
ObjectProvider : ObjectFactory 상속. 옵션, 스트림 처리 등 편의기능이 많고 별도의 라이브러리 필요 없음. 스프링에 의존
JSR-330 Provider 사용 : 다른 컨테이너에서 사용할 일이 있다면(아마 없겠지만) 이것을 사용