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 사용
: 다른 컨테이너에서 사용할 일이 있다면(아마 없겠지만) 이것을 사용