전통적인 CLI 프로그램의 흐름은 프로그래머에 의해 결정된다. UI 기반의 프로그램은 사용자 입력과 이벤트를 기반으로 프로그램의 흐름이 결정된다.
절차적 프로그래밍 방식에서 벗어나 프레임워크나 컴포넌트 같은 외부 소스가 프로그램의 흐름을 결정하는 방식으로 바뀌어야 했다.
객체지향 프로그래밍 방식이 등장하며 프레임워크들에 의존성 주입을 지원하는 IoC 컨테이너 패턴 구현이 보편화됐다.
DI란
의존성 주입(Dependency Injection)
IoC의 한 타입
생성자나 설정자, 팩토리 메소드 매개변수, @Autowired 등으로 연결 객체를 사용하고, 프레임워크가 실제로 런타임에 연결 객체를 주입하는 것
AOP란
관점지향 프로그래밍(Aspect-Oriented Programming)
객체지향 프로그래밍과 함께 작동하는 프로그래밍 패러다임
로깅, 보안, 트랜잭션 관리, 메트릭과 같은 기능은 추상화하고 캡슐화하는 방법
IoC 컨테이너
bean을 생성할 때 객체의 의존성을 주입하는 역할
스프링에서는 자바 객체가 IoC 컨테이너에 의해 인스턴스화 되고, 조합되고 관리되는 경우 bean으로 만들 수 있다.
BeanFactory 인터페이스는 설정 프레임워크와 기본 기능 제공, bean 인스턴스화와 연결을 처리
ApplicationContext 인터페이스는BeanFactory의 하위 인터페이스, bean 관리 이외에도 여러 기능이 있기 때문에 스프링은 ApplicationContext 사용 권장
ApplicationContext 기능
통합 라이프 사이클 관리
BeanPostProcessor와 BeanFactoryPostProcessor 자동 등록
MessageSource에 쉽게 액세스할 수 있는 국제화(메시지 리소스 처리)
내장된 ApplicationEvent를 사용한 이벤트 발행
웹 애플리케이션을 위한 애플리케이션 레이어 특화 컨텍스트인 WebApplicationContext 제공
Configuration metadata
스프링이 인스턴스화, 어셈블 및 설정할 beqan을 인식하기 위해 사용하는 데이터
설정 메타데이터를 사용하면 응용프로그램 객체와 타 객체 간의 상호의존성을 표현할 수 있다.
XML 설정, 자바 애노테이션, 자바 코드 방법이 있다.
Bean과 그 범위 정의하기
Bean
IoC 컨테이너가 관리하는 자바 객체
개발자가 IoC 컨테이너에게 제공한 설정 메타데이터를 이용하여 bean을 생성, 어셈블, 관리한다.
컨테이너 내부에서 고유 식별자를 가져야 하고, 별칭을 사용하여 두개 이상의 식별자를 가질 수 있다
bean 선언
@Configuration : 클래스에 설정코드가 포함돼 있음을 나타내는 클래스 어노테이션
@Bean : bean을 정의하는데 사용하는 메서드 어노테이션
@Bean 애노테이션은 @Component 애노테이션 내부에 있어야 함
@Configuration, @Controller, @Service, @Repository 등의 애노테이션은 @Component로 메타-애노테이션이 달려있음
일반적으로 bean의 이름은 첫글자가 소문자인 클래스명
name 속성을 사용하여 별칭 정의 가능
파괴를 위한 기본 메서드는 close/shutdown 메서드, 별도로 지정하는 경우 destroyMethod 속성 사용
public class SampleBean {
public void init(){
// bean 초기화로직
}
public void destroy() {
// bean 파괴 로직
}
}
public interface BeanInterface {
// 인터페이스 코드
}
public class BeanInterfaceImpl implements BeanInterface{
// bean 코드
}
@Configuration
public class AppConfig{
@Bean(initMethod = "init", destroyMethod = "destroy", name = {"sampleBean", "sb"})
@Description("bean 설명")
public SampleBean sampleBean(){
return new SampleBean();
}
@Bean
public BeanInterface beanInterface(){
return new BeanInterfaceImpl();
}
}
@ComponentScan 애노테이션
bean의 자동 스캔 허용
@Configuration
@ComponentScans({
@ComponentScan(basePackages = "베이스 패키지 경로"),
@ComponentScan(basePackageClasses = AppConfig.class)
})
public class AppConfig{
// 코드
}
Bean의 범위
스프링 컨테이너가 인스턴스를 생성하는 방법은 범위(scope)에 의해 정의
기본 범위는 싱글톤
싱글톤
IoC 컨테이너당 하나의 새로운 인스턴스만 생성
프로토타입
주입시마다 새로운 인스턴스 생성
요청
유효한 HTTP 요청 라이프 사이클 동안에 HTTP 요청마다 하나의 bean 인스턴스생성
세션
유효한 HTTP 세션 라이프 사이클 동안에 하나의 인스턴스 생성
응용 프로그램
유효한 서블릿 컨텍스트 라이프 사이클 동안에 하나의 인스턴스 생성
웹소켓
각 WebSocket 세션에 대해 단일 인스턴스 생성
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public PrototypeBean prototypeBean(){
return new PrototypeBean();
}
@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public ReqScopedBean reqScopedBean(){
return new ReqScopedBean();
}
@RequestScope
public ReqScopedBean reqScopedBean(){
return new ReqScopedBean();
}
자바를 사용하여 bean 설정
@Import 애노테이션
둘 이상의 설정 클래스가 있는 경우 설정을 모듈화하는데 사용
스프링부트는 자동설정이므로 @Import를 사용할 필요가 없으나 컨텍스트를 수동으로 인스턴스화하려면 @Import를 사용하여 설정을 모듈화하여야 함
@Configuration
public class FooConfig {
@Bean
public FooBean fooBean(){
return new FooBean();
}
}
@Configuration
@Import(FooConfig.class)
public class BarConfig{
@Bean
public BarBean barBean(){
return new BarBean();
}
}
public static void main(String[] args) {
ApplicationContext appContext = new AnnotationConfigApplicationContext(BarConfig.class);
FooBean fooBean = appContext.getBean(FooBean.class);
BarBean barBean = appContext.getBean(BarBean.class);
}
@DepondsOn 애노테이션
bean의 초기화 순서를 고정하는데 사용
Bean의 초기화 순서가 잘못되고 그로 인해 스프링 컨테이너가 의존성을 찾지 못하면 NoSuchBeanDefinitionException 예외가 발생
@Configuration
public class AppConfig{
@Bean
public FooBean fooBean(){
return new FooBean();
}
@Bean
public BarBean barBean(){
return new BarBean();
}
@Bean
@DependsOn({"fooBean", "barBean"})
public BazBean bazBean(){
return new BazBean();
}
}
DI 코딩 방법
생성자로 의존성 정의
@Configuration
public class AppConfig{
@Bean
public CartRepository cartRepository(){
return new CartRepositoryImpl();
}
@Bean
public CartService cartService(){
return new CartService(cartRepository());
}
}
설정 메서드로 의존성 정의
@Configuration
public class AppConfig{
@Bean
public CartRepository cartRepository(){
return new CartRepositoryImpl();
}
@Bean
public CartService cartService(){
CartService cartService = new CartService();
cartService.setCartRepository(cartRepository());
return cartService;
}
}
클래스 프로퍼티를 사용한 의존성 정의
public class CartService{
@Autowired
private CartRepository repository;
}
애노테이션을 사용하여 bean의 메타데이터 설정
@Autowired
bean의 클래스 자체에서 설정 부분을 정의할 수 있다
필드, 생성자, 설정자 또는 모든 메서드에 적용할 수 있다
해당 애노테이션이 달린 bean을 주입하기 위해 리플렉션을 사용한다. 리플렉션은 다른 주입 방법보다 비용이 많이 든다
클래스 멤버에 적용하는 것은 의존하는 bean을 주입할 생성자 또는 설정자 메서드가 없는 경우에만 작동한다
@Component
public class CartService {
private CartService repository;
private ARepository aRepository;
private BRepository bRepository;
private CRepository cRepository;
// 멤버 기반 오토 와이어링
@Autowired
private AnyBean anyBean;
// 생성자 기반 오토 와이어링
@Autowired
public CartService(CartRepository cartRepository){
this.repository = cartRepository;
}
// 설정자 기반 오토 와이어링
@Autowired
public void setARepository(ARepository aRepository){
this.aRepository = aRepository;
}
// 메서드 기반 오토 와이어링
@Autowired
public void xMethod(BRepository bRepository, CRepository cRepository){
this.bRepository = bRepository;
this.cRepository = cRepository;
}
}
타입별 일치(Match by type)
주입하려는 인스턴스의 타입과 동일한 타입을 가진 bean이 한개인 경우 주입하는 데 문제가 없다.
한정자별 일치(Match by qualifier)
주어진 타입의 bean이 두개 이상 있는 경우 @Qualifier 애노테이션을 사용해 원하는 bean을 주입할 수 있다
@Component
public class CartService {
@Autowired
@Qualifier("cartService1")
private CartService cartService1;
@Autowired
@Qualifier("cartService2")
private CartService cartService2;
}
이름으로 일치(Match by name)
필드명이 bean 생성시 부여된 value값과 동일한 경우
@Inject, @Resource는 @Autowired와 유사하며 의존성을 중비하는 데 사용한다.
@Autowired와 @Inject는 타입별, 한정자별, 이름별로 지정 @Resource는 이름(첫번째 선호), 타입, 한정자별로 지정
@Service(value="cartServc")
public class CartService{
// 코드
}
@Controller
public class CartController {
@Autowired
private CartService cartServc;
}
@Primary의 목적은 무엇일까?
여러 타입의 bean 중 하나를 기본 값으로 설정할 때 사용
@Configuration
public class AppConfig{
@Bean
@Primary
public CartRepository cartRepository(){
return new CartRepositoryImpl();
}
}
@Value는 언제 사용할까?
~.properties나 ~.yml과 같은 외부 프로퍼티 파일을 사용하는 경우 코드에서 프로퍼티 값을 사용할 때 @Value 애노테이션을 사용한다
스프링부트를 사용하는 경우에는 @PropertySource를 사용할 필요가 없다. src/main/resources 디렉터리 아래에 프로퍼티 파일을 두면 된다.
@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig{}
@Controller
public class CartController{
@Value("${default.currency}")
String defaultCurrency;
}
AOP용 코드 작성
포인트컷을 식별하기 위한 애노테이션 정의
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeMonitor {}
Aspect 정의
@Aspect
@Component
public class TimeMonitorAspect {
public TimeMonitorAspect(ExcelUtils excelUtils) {
this.excelUtils = excelUtils;
}
@Around("@annotation(경로)")
public Object logTime(ProceedingJoinPoint joinPoint) throws Throwable{
long start = System.currentTimeMillis();
Object proceed = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - start;
System.out.println(joinPoint.getSignature() + " takes: " + executionTime + "ms");
return proceed;
}
}
Advice의 종류
@Before
JoinPoint보다 먼저 실행
@After
메서드의 성공과 실패에 상관없이 JoinPoint 이후 실행
@AfterReturing
JoinPoint가 성공적으로 실행된 후 실행
@SfterThrowing
JoinPoint에서 예외가 발생한 후 실행
@Around
JoinPoint 전후에 실행
JoinPoint
Aspect가 작동하는 특정 지점
수정중인 필드, 호출되는 메서드, 발생되는 예외가 될 수 있다
JoinPoint 객체를 사용하면 메소드의 전체 시그니처, 클래스 이름, 메소드 이름, 인수 등을 캡쳐할 수 있다.
JoinPoint를 사용하면 대상 객체와 프록시를 캡쳐할 수 있다. Advice는 대상 객체에 적용된다. 스프링 AOP는 대상 객체의 서브클래스를 생성하고 메서드를 오버라이드하고 어드바이스를 삽입한다. 프록시는 GGLIB나 JDK Proxy lib를 이용하여 대상 객체에 어드바이스를 적용한 후 생성되는 객체이다.
스프링 부트를 사용하는 이유
컨테이너 없는 웹 애플리케이션 아키텍처에 대한 지원 개선이라는 제목의 SPR-9888 요청에 따라 개발되었다
스프링부트는 자체적으로 초경량 컨테이너를 만드는데 중점을 둔다.
스프링 부트에는 자체 기본 구성을 가지고 있으며 프로덕션 레디 웹 애플리케이션을 간단하게 만들기 위한 자동 구성을 지원한다.
스프링 이니셜라이저는 그룹, 아티팩트 및 의존성과 같은 프로젝트 메타데이터와 함께 Maven 또는 Gradle과 같은 빌드 도구를 선택할 수 있는 웹페이지다. 스프링 이니셜라이저는 개발자의 몫이던 설정 부분을 모두 수행한다. 덕분에 개발자는 비즈니스 로직과 API 작성에만 집중할 수 있다.
서블릿 디스패처의 중요성 이해
서블릿 기능
서블릿을 사용하면 REST 엔드포인트에서 작동하는 경로매핑을 가질 수 있으며 식별을 위한 HTTP 메서드도 제공한다.
또한 JSON 및 XML을 비롯한 다양한 타입의 응답 객체를 구성할 수 있다.
하지만 여전히 요청 URL을 처리하고 매개변수를 파싱하고 JSON/XML과 응답을 직접 변환해야하기 때문에 세련되지 않다.
스프링 MVC
스프링 MVC를 이용하면 위 작업을 간소화할 수 있다.
스프링 MVC는 MVC(모델-뷰-컨트롤러) 패턴 기반으로 한다.
스프링 MVC 구성
모델 : 애플리케이션 데이터를 포함하는 자바 객체(POJO), 응용 프로그램의 상태를 나타낸다
뷰 : HTML/JSP/템플릿 파일로 구성된 프레젠테이션 레이어. 모델에서 데이터를 렌더링하고 HTML 출력을 생성한다
컨트롤러 : 사용자 요청을 처리하고 모델을 빌드한다
DispatcherServlet
스프링 MVC의 일부. 프런트 컨트롤러 역할 수행
REST 컨트롤러를 위한 스프링 MVC의 사용자 요청 흐름
사용자는 HTTP 요청을 보내고, DispatcherServlet이 요청을 수신한다.
DispatcherServlet은 바톤을 HandlerMapping에 전달한다. HandlerMapping은 요청된 URL에 대한 올바른 컨트롤러를 찾는 작업을 수행하여 DispatcherServlet에 다시 전달한다.
DispatcherServlet은 HandlerAdaptor를 사용하여 Controller를 처리한다.
HandlerAdaptor는 Controller 내부에서 적절한 메서드를 호출한다.
그런 다음 Controller는 관련 비즈니스 로직을 실행하고 응답을 구성한다.
스프링은 자바에서 JSON/XML 변환을 위해 요청 및 응답 객체의 마샬링/언마샬링을 사용하며 그 반대의 경우도 마찬가지다