Spring 프레임워크 정말 기초 정리

IOC 컨테이너와 빈

DI

  • Dependency Injection
  • 디자인 패턴의 한 종류
  • A객체에서 B객체를 생성한다면 A는 B객체에 의존성을 가지게 됨
  • B객체 생성자가 수정되면 A객체의 소스코드도 바뀌어야함
  • B객체를 A객체가 아닌 외부에서 생성후 주입하자 -> DI
  • 모듈 의존성 없애고 유닛테스트가 쉬우며 재사용및 코드 관리 좋아짐

Java의 DI 문제점

  • 주입된 객체를 다른 클래스에서 사용되는 경우 분개문이 FactoryPattern처럼 패턴이
    적용되어 있지 않아 재사용 불가해서 분개문이 여기저기 흩어지게 됨.

Bean

  • 스프링 Ioc 컨테이너에서 관리하는 객체
  • Bean으로 지정된 객체를 외부(Ioc 컨테이너)가 생성하고 주입도 프레임워크에서 한다.
  • 빈 스코프
    • 빈 생명 주기
    • 기본적으로 싱글톤 : 따로 설정하지 않아도 객체를 싱글톤으로 관리할 수 있는 장점
    • 프로토타입: 매번 다른 객체를 쓰고 싶다면 프로토타입으로 하면 된다.
    • 라이프사이클 인터페이스 : 빈생성이나 생성후에 무언가 부가적인 일을 할 수 있다.

IoC

  • Inversion of Control 제어의 역전
  • 객체의 생명주기를 개발자가 짠 코드가 아닌 외부의 프레임워크에서 제어
  • 의존 객체를 직접 만들어 사용하는 것이 아니라 주입받아 사용
  • 일반적으로 IoC 하위로 DI를 보는게 일반적

스프링 IoC 컨테이너

  • 스프링 프레임워크에서 생성된 빈을 관리하는 공간
  • 스프링 컨테이너, 빈 컨테이너등의 이름으로도 사용
  • 빈 객체 보관, 생명주기 관리, DI, 그리고 그밖을 관리.
  • 빈 설정 소스로부터 빈 정의를 읽고 빈을 구성하고 제공한다.

BeanFactory

  • Ioc 컨테이너의 가장 최상위 인터페이스이자 핵심 인터페이스
  • 다양한 구현체가 존재
  • 다양한 빈 팩토리 라이프사이클 인터페이스들이 순서대로 존재
  • 가장 중요한 getBean()메소드 가 선언 되어 있음

ApplicationContext

  • 개발자가 가장 많이 보게 되는 인터페이스
  • 정의를 보면 위에서 본 BeanFactory말고도 수많은 인터페이스를 상속하고 있다.
  • Ioc 기본 기능외에 국제화나 리소스,Environment등의 기능을 쓸수 있게 인퍼페이스 정의
  • BeanFactory에 더 다양한 기능이 추가된 인터페이스라고 생각하면 무방

고전적인 아주 옛스러운 스프링 프로젝트 만들기

스프링부트에 익숙해져서 이제 기억도 안나겠지만 ㅋㅋ 되새김질 해보자
스프링 프레임워크는 2004년 릴리즈 되었고 2.0부터 XML namespace를 제공했다.
BookService가 있고 여기서 BookRepository를 사용하기 위해 주입이 필요하다고 하자.

1
2
3
4
5
6
7
public class BookService {
BookRepository bookRepository;

public void setBookRepository(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
}

이클립스의 bean Configuration file이나 인텔리J의 springconfig xml을 만든다.
그리고 여기에 빈을 정의한다.

template이 만드는 기본 스프링 설정 xml
1
2
3
4
5
6
<?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">

</beans>

이제 저 안에 빈을 설정해준다.

정말 고전적인 빈설정
1
2
3
4
5
<bean id="bookService" class="me.rkaehdaos.applicationcontest.BookService">
</bean>

<bean id="bookRepository" class="me.rkaehdaos.applicationcontest.BookRepository">
</bean>

이리하면 빈은 등록되지만 실제로 DI가 되지 않는다. DI를 하려면 사용할 빈 을 설정을 해주어야한다. 빈을 사용하는 빈 정의에 사용할 빈을 프로퍼티 태그로 정의한다.

프로퍼티로 주입받을 빈을 설정
1
2
3
4
5
6
<bean id="bookService" class="me.rkaehdaos.applicationcontest.BookService">
<property name="bookRepository" ref="bookRepository"/>
</bean>

<!-- 내용 넣을게 없을땐 바로 />로 끊을 수 있다.-->
<bean id="bookRepository" class="me.rkaehdaos.applicationcontest.BookRepository"/>
  • id는 소문자로 시작하는 camelCase로 하는게 컨벤션이다.
  • 프로퍼티의 name은 setter메소드의 뒷부분에서 가져온 것이다.
  • ref에는 참조할 빈의 id가 온다.

이제 이 빈 등록파일을 등록해서 사용할 ApplicationContext를 만들어 사용한다.
(인텔리J같은 경우엔 이 빈 설정 파일을 만들었을떄 등록하자는 도움말이 뜬다.)

메인클래스에서 컨텍스트를 만들고 getbean으로 하는..정말 고전 샘플
1
2
3
4
5
6
7
public class ApplicationcontestApplication {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
BookService bookService =(BookService) context.getBean("bookService");
System.out.println(bookService.bookRepository!=null);
}
}
  • 진짜 이거 정리하면서 참 시간이 많이 지났다는 생각이 든다.
  • 예전에 저거 타입캐스팅 하는 것도 빼먹어서 나는 에러도 못찾았더랬다.
  • 무사히 빈이 등록되었고 DI도 되었기에 true가 뜬다.

이 방법은 예전에 SI프로젝틋히 관리하는 사람이 따로 있는 경우에는 “내가” 편하다.
그사람은 죽어나겠지.. 빈이 한두개도 아닌데 이를 등록하는 것도 장난이 아니다.
그래서 등록한 것이 어노테이션을 사용하는 “컴포넌트 스캔”이다.

xml에 있던 내용을 전부 지운후 다음과 같이 등록한다.
1
<context:component-scan base-package="me.rkaehdaos"/>

이렇게 하면 정해놓은 패키지로부터 ‘@Component’어노테이션을 찾아서 빈으로 등록하게 된다. 이 ‘@Component’를 확장한 어노테이션이 몇개 있는데 이것들이 스프링MVC에서 그토록 쓰고 있는 ‘@Controller’,‘@Service’, ‘@Repository’등이다.

자 이제 어노테이션을 이용한 등록을 해본다. 스프링 2.5부터 가능했던 내용이다.
일단 BookService에는 ‘@Service’, BookRepository에는 ‘@BookRepository’를 붙인다.
이러면 저 xml을 읽고 콤포넌트 스캔 태그에서 ‘me.rkaehdaos’하위의 두 빈을 등록한다.
하지만 아직 DI가 되지 않았다. DI를 시키려면 ‘@Autowired’ 어노테이션(혹은 @Inject)등을 사용해서 등록한다.
다시 실행해보면 정상적으로 작동하는 것을 볼 수 있다.

그다음에 나오는 것이 자바 설정 파일이다. 스프링 3.0부터 지원하기 시작했다.
빈 설정을 xml이 아닌 자바 코드에서 할수 없을까 해서 나온 기능이다.
굉장히 유연한 빈 설정 기능을 제공하며 아마 이 기능이 나오지 않았더라면 스프링부트는 절대 지금처럼 나오지 못했을 것이다.

이것도 테스트 해보자. 일단 앞에서 붙였던 어노테이션은 전부 제거한다.
특정 클래스를 만들고 (보통은 이 클래스 이름도 ~~~Config등으로 끝나게 만든다)
거기에 ‘@Configuration’을 붙여주면 그 클래스는 설정 클래스로 인식된다.
그리고 이 설정 클래스 내에서 빈을 등록하게 된다.

@Configuration 을 붙인 자바 설정 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class ApplicationConfig {
@Bean
public BookRepository bookRepository() {
return new BookRepository();
}
@Bean
public BookService bookService() {
BookService bookService = new BookService();
bookService.setBookRepository(bookRepository()); // 직접 의존성 주입
return bookService;
}
}

  • 메소드 이름이 빈의 id가 된다.
  • 메소드 반환 타입이 빈의 타입이 된다.
  • 메소드 안의 리턴 되는 객체가 실제 빈이 될 객체
  • 위에서 직접 의존성 주입하는 방법은 setter를 이용한 방법이며
    메소드 파라미터로 주입 받는 것도 가능하다.
    (ex. public BookService bookService(BookRepository bookRepository) {})
  • 아니면 설정파일에서 주입 정의를 하지 말고 bookService의 멤버 bookRepository에게
    ‘@Autowired’를 사용해서도 가능하다.[ 생성자 주입으로는 불가능]

이렇게 만든 자바 설정파일은 어플리케이션 컨텍스트에서 어떻게 사용할까?

ApplicationContext 구현체가 바뀌었다.
1
2
3
4
5
6
7
8
public class ApplicationcontestApplication {
public static void main(String[] args) {
//ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
BookService bookService =(BookService) context.getBean("bookService");
System.out.println(bookService.bookRepository!=null);
}
}

이제 실행해보면 정상 작동하는 것을 볼 수 있다.

자 예전 책에서나 볼 수 있던 소스에서 점점 요새 코드와 비슷해지고 있다.
이 부분도 문제라고 하면 xml 처럼 일일히 빈을 등록해야한다는 부분이다.
이 자바설정에서도 아까 xml때의 ‘컴포넌트 스캔’을 할수 없을까?

콤포넌트 스캔을 하는 자바 설정 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
@ComponentScan(basePackages = "me.rkaehdaos.applicationcontest") //문자열 입력, type safety 문제 여지 있음
@ComponentScan(basePackageClasses =ApplicationcontestApplication.class)//지정 클래스 위치에서부터 하위스캔
public class ApplicationConfig {

// 콤포넌트스캔을 할것이니 이전 빈 설정은 필요 없음
/*
@Bean
public BookRepository bookRepository() { return new BookRepository();}
@Bean
public BookService bookService() {
BookService bookService = new BookService();
bookService.setBookRepository(bookRepository());
return bookService;
}
*/
}
  • ‘@ComponentScan’에는 basePackages와 basePackageClasses가 올수 있는데 전자는 문자열로 (패키지가 유니크 한경우 자동완성도 쉽게 지원하나) 타입 세이프트 문제가 있다.
  • 후자는 지정한 클래스 위치로부터 하위 스캔한다.

이 방법이 지금 쓰고 있는 스프링부트와 가장 가까운 방법이라고 봐도 좋을 것이다.
사실 yml을 사용하거나 4.0부터 나온 groovy DSL을 사용해서 더 느슨하고 유연한 설정을 추구하는 방법도 있는데 스프링 부트가 나온이상 사실 이방법에서 정착해도 된다.

스프링 부트를 사용하면 사실 어플리케이션 컨텍스트 자체를 직접 만들어 쓰지 않는다.
스프링 부트 에서 사용하는 @SpringBootApplication은 이미 컴포넌트 스캔도 붙어 있고 ‘@Configration’도 포함하고 있기때문에 사실상 Config 클래스를 새로 만들 필요없이 main메소드를 포함하는 클래스가 곧 자바 설정 클래스가 된다.

Autowired

알고 있지만 정리니까..

  • required : default = true (따라서 못찾는 경우 어플리케이션 구동 실패)
    • 위 예제에서 @Autwired에 required=false를 하면 autowired가 실패해서
      BookRepository를 DI받지 못해서 BookService 빈이 생성된다.
      사용할 수 있는 위치
  • Setter
  • filed
  • Constuctor
    • 생성자 autowired는 required=false 처리가 불가능하다. 왜?
    • B빈의 DI뿐이 아닌 A빈의 인스턴스 자체 생성이 불가능

같은 타입의 빈이 여러개일때 어떻게 Autowired 되는가?

  • 해당 타입의 빈이 없는 경우 : 에러
  • 해당 타입의 빈이 한개 : DI 성공
  • 해당 타입의 빈이 여러개인 경우
    • 기본동작 : 빈 이름으로 시도 가급적 권장하지 않음
      • 같은 이름의 빈을 찾으면 해당 빈 사용
      • 같은 이름의 빈을 못 찾으면 실패
    • 사용자 처리 방법
      • ‘@Primary’ 빈 클래스에 어노테이션 사용 가장 추천하는 방법
        • 동일 빈 판단시 우선권을 줌
      • ‘@Qualifier’ : @Autowired 옆에 주입할 빈 이름으로 주입
      • 기본적으로 컴포넌트 스캔되는 빈의 id는 소문자로 시작하는 camelcase 클래스명
      • 위의 BookRepository의 경우 빈 이름은 ‘bookRepository’가 된다.
      • 이 방법보다는 @Primary 방법이 더 type safe 하다.

여러개의 빈을 동시에 받을 수 있을까?

여러 BookRepository 타입의 빈이 있을때 다음과 같이 받을 수 있을까?

같은 타입의 여러 빈을 한꺼번에 autowired 하기
1
2
3
4
5
6
@Autowired
List<BookRepository> bookRepositories;

public void printBookRepository() {
this.bookRepository.forEach(System.out::println);
}

‘@Autowired’ 동작원리

  • BeanPostProcessor: 빈 초기화(initialize)라이프사이클 이전 혹은 이후에 부가적인 작업을 할 수 있는 또 다른 라이프사이클 콜백

    • ‘@PostConstruct’ 사용방법
      • 빈 클래스 메소드에 이 어노테이션을 붙인다.
      • 해당 어노테이션은 빈이 만들어진후 값을 세팅하거나 부가작업 하는 용도로 쓰인다.
    • 고전적인 방법 : InitializingBean 인터페이스를 구현
  • BeanPostProcessor인터페이스 구현체인 AutowiredAnnotationBeanPostProcessor에 의해서 동작한다.

    • AutowiredAnnotationBeanPostProcessor가 postProcessorBeforeInitialization
      단계에서 @Autowired어노테이션을 읽어와서 처리한다.
    • 스프링의 ‘@Autowired’, ‘@Value’, JSR-330의 ‘@Inject’의 어노테이션 처리기
  • BeanFActory가(현재 구현체 ApplicationContext) BeanPostProcessor 빈을 찾는다.

  • 그중에 등록이 되어 있는 AutowiredAnnotationBeanPostProcessor 빈이 검색된다.

  • 다른 일반적인 빈에게 BeanPostProcessor를 적용한다.

@Component와 컴포넌트 스캔 원리

@Component 어노테이션 종류

  • @Repository
  • @Service
  • @Controller
  • @Configuration

동작원리

  • @ComponentScan은 스캔할 패키지와 어노테이션에 대한 정보
  • 실재 스캐닝은 ‘ConfigurationClassPostProcessor’라는 ‘BeanFactoryPostProcessor’(BeanPostProcessor가 아님)에 의해 처리된다.
  • BeanFactoryPostProcessor는 다른 모든 빈 만들기 전에 적용됨
    –>다른 빈을 등록하기 전에 컴포넌트 스캔의 빈이 등록 된다는 뜻

펑션을 이용한 빈 등록

  • 구동시간에 민감하다면 고려
  • 기존 리플렉션 프록시기반 cglib등의 빈 은 구동시간에 영향을 미친다.
  • 람다 공부후 다시 공부할 예쩡
  • 결론만 말하면 기존 컴포넌트 스캔을 대체는 불가능 –> 예전의 불편함을 다시 야기
  • 직접 ‘@Bean’으로 등록하는 방법의 대체법으로는 나쁘지 않는 방법

빈의 스코프

  • 싱글톤
  • 프로토타입
    • Request
    • Session
    • WebSocket

서로 다른 스코프의 빈 참조

  • 프로토 타입 빈이 싱글 톤 빈을 참조 하는 경우에는 아무 이상없음
  • 싱글톤이 프로토 타입 빈을 참조하는 경우?
    • 참조하는 프로토 타입빈이 새로운 빈으로 업데이트 되지 않는다.
    • 업데이트 하려면?
    1. scoped-proxy
    • 가장 쉽지만 이해하긴 어려운 방법
      프록시모드를 설정한 프로토타입 스코프 빈
      1
      2
      3
      @Component @Scope(value="prototype", proxyMode=ScopedProxyMode.TARGET_CLASS)
      class prototypeBean{
      }
    • 기본값은 ScopedProxyMode.DEFAULT이며 프록시를 사용하지 않는다.
    • 현재 타겟이 인터페이스가 아닌 클래스이므로 TARGET_CLASS로 설정하였다.
    • 프록시 모드
      • 스프링은 JDK1.3부터 있는 Java의 다이나믹 프록시와 Cglib의 프록시 둘다 지원
      • 타겟이 최소 하나 이상의 인터페이스를 구현하였다면 Java의 다이나믹 프록시,
        그렇지 않다면 Cglib의 프록시를 사용한다.
      • Cglib는 스프링 3.2이전에는 포함x, 이후에는 core패키지에 포함
      • Java 다이나믹 프록시는 Reflection을 이용하여 만들며,
        Cglib는 바이트코드를 조작하여 프록시 객체를 만든다.
        (스프링 부트에서는 현재 기본으로 무조건 Cglib를 사용하는 것 같다.)
    • 프록시모드를 쓰면 Service는 프록시를 거쳐서 Repository에 도달하게 된다.
      그러지 않으면 그떄마다 바꿀 여지가 없다.
    1. Object-Provider
      Object-Provider를 사용하는 방법, 코드 자체가 POJO가 아닌 스프링 코드.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      import  org.springframework.beans.factory.ObjectProvider;
      @Component
      public class Single {
      @Autowired
      private ObjectProvider<Proto> proto;

      public Proto getProto() {
      return proto.getIfAvailable();
      }
      }
      • 코드 자체에 POJO 자바가 아닌 스프링 코드가 들어가므로 추천하지 않는다.
    2. 가능한 긴 생명주기의 빈이 짧은 생명주기의 빈을 사용하지 않도록 추천
  • 싱글톤 사용시 주의
    • 기본적으로 쓰레드 세이프 하지 않으므로 쓰레드 세이프하게 코딩하자.
    • ApplicationContext시에 객체 생성하므로 구동시 시간이 오래걸릴 수 있다.

ApplicationContext의 구성

  • ApplicationContext은 위에서 말했듯이 BeanFactory말고도 여러 인터페이스를 같이 상속받고 있다.
    ApplicationContext은 여러 인테페이스를 상속받는다.
    1
    2
    3
    public interface ApplicationContext extends
    EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
    MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
    이 인터페이스를 하나씩 살펴보면 스프링 IoC에 대해 좀더 알 수 있다.

Environment가 제공하는 기능 1 - 프로파일

ApplicationContext이 구현하는 EnvironmentCapable인터페이스가 있는데
여기에서 제공하는 프로파일이 있다.

  • 프로파일 = 빈들의 그룹

  • 각각의 환경에 따라 특정 빈 사용, 특정 상황일때 특정 빈 사용 할때

  • 스프링의 Environment 인터페이스를 통해서 사용할 수 있다.

  • 위 코드의 ApplicationContext에서 EnvironmentCapable 인터페이스 정의를 보면

    EnvironmentCapable 정의에 getEnvironment가 있다.
    1
    2
    3
    public interface EnvironmentCapable {
    Environment getEnvironment();
    }

    getEnvironment를 통해 Environment를 꺼내올 수 있다는 것을 알 수 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package me.rkaehdaos.testdemo;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.ApplicationArguments;
    import org.springframework.boot.ApplicationRunner;
    import org.springframework.context.ApplicationContext;
    import org.springframework.core.env.Environment;
    import org.springframework.stereotype.Component;
    import java.util.Arrays;
    @Component
    public class AppRunner implements ApplicationRunner {
    @Autowired
    ApplicationContext context;
    @Override
    public void run(ApplicationArguments args) throws Exception {
    Environment environment = context.getEnvironment();
    System.out.println(Arrays.toString(environment.getActiveProfiles()));
    System.out.println(Arrays.toString(environment.getDefaultProfiles()));
    }
    }

  • getActiveProfiles

    • 현재 active된 프로파일의 목록을 가져올 수 있다.
    • 없으면 비어있다. 위의 같은 경우 []가 찍힌다.
  • getDefaultProfiles

    • 어떤 프로파일이 정의가 되지 않더라도 기본적으로 정의되는 프로파일
    • 위의 경우 [default]가 찍힌다.
    • 빈이나 컴포넌트등의 정의에 따로 프로파일을 정하지 않으면 기본 프로파일에 속한다.
  • @Profile("test")

    • 빈이나 컴포넌트 정의에 위처럼 하면 해당 정의는 프로파일에 속하게 된다.
    • @Bean, @Component, @Configuration과 같이 쓰일 수 있으며,
      클래스정의, 메소드 정의 모두 가능하다.
  • 프로파일 지정

    • 기본적으로 실행하면 default 프로파일이므로 위에서 살펴본 어노테이션으로
      정의한 빈이나 컴포넌트는 찾을 수 없다.(왜? test에 속하니까)
    • 방법1 - 인텔리J Ultimate
      • 인텔리J 실행을 Edit Configuration해서 살펴보면 Active profile항목이 있다.
      • active하고 싶은 profile 이름을 적으면 된다.
    • 방법2 - 인텔리 J VM
      • VM Option에 -Dspring.profiles.active="test,A,B,C,..."를 준다.
    • 방법3 - @ActiveProfiles
      • 테스트시 많이 이용
  • 프로파일 표현식

    • ex) @Profile(“!test”) : test 프로파일 이 active 안되었을떄 작동
    • ! : not
    • & : and
    • | : or

Environment가 제공하는 기능 2 - 프로퍼티

  • 다양한 방법으로 정의할 수 있는 설정값

  • key=value의 쌍 형태

  • ex) VM options에 -Dapp.name=appname식으로 줄 수 있음

  • Environment의 역할은 프로퍼티 설정 및 프로퍼티 값 가져오기

  • 위의 경우 environment.getProperty(“app.name”)식으로 가져올 수 있다.

  • 위의 경우는 context.getBean같은 극단적인 샘플

  • 보통은 .properties파일에 정리

  • 프로퍼티는 우선 순위가 있다.( 스프링 부트떄도 봤었는데..)

    • StandardServletEnvoronment의 우선순위

      • ServletConfig 매개변수
      • ServletContext 매개변수
      • JNDI (java.comp/env/)
      • JVM 시스템 프로퍼티 (-Dkey=”value”)
      • JVM 시스템 환경 변수 (운영체제 환경변수)
    • @PropertySource(“classpath:/application.properties”)

      • Environment를 통해 프로퍼티를 추가 하는 방법
  • 스프링부트의 외부설정 참고

    • 기본 프로퍼티 소스 지원 (application.properties)
    • 프로파일까지 고려한 계층형 프로퍼티 우선1 순위 제공

MessageSource

  • ApplicationContext가 상속받고 있는 인터페이스들 중 하나

    MessageSource인터페이스, getMessage가 눈에 들어온다.
    1
    2
    3
    4
    5
    6
    public interface MessageSource {
    @Nullable
    String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
    String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
    String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
    }
  • 국제화(i18n)기능을 제공, 메세지를 다국화하는 방법으로 여겨도 된다.

  • 예전 사용방법

    1. MessageSource 구현체(3종류)를 bean으로 등록한다.
    2. 구버전의 경우 id를 messageSource로 해야만 작동하는 듯 하다.
      그러면 등록된 빈중 해당 id의 messageSource 타입 빈 객체를 이용한다.
    3. 빈 설정의 basename에 프로퍼티폴더등을 지정한다.
    4. 해당 폴더안에 message_en_US.properties, message_ko_KR.properties등이 있고
      접속하는 세션의 로케일에 따라 혹은 getMessage에서 지정하는 로케일에 따라
      알맞는 프로퍼티를 자동으로 로딩한다.
    5. ApplicationContext.getMessage()로 메세지를 획득한다.
  • Component에서 필요시 ApplicationContext를 @Autowired 받았듯이
    MessageSource도 당연히 @Autowired 가능하다.

  • ApplicationContext로 해도 사용 가능하지만 좋은 코딩 방법은 아니다.
    MessageSource로 받는 것이 사용목적등이 드러나기 떄문에 좋은 방법이다.

  • ResourceBundle은 프로퍼티 파일의 이름을 이용하여 언어 및 지역에 따른 메세지를 로딩

  • resources 폴더에 로케일별 프로퍼티를 만들면 인텔리J는 자동으로 리소스 번들처리

messageSource 예제, ApplicationRunner를 쓰는 시점에 이미 스프링부트가ㅠ
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class AppRunner1 implements ApplicationRunner {
@Autowired
MessageSource messageSource;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("messageSource: "+messageSource.getClass());
System.out.println(messageSource.getMessage("greeting", new String[]{"GeunChang"}, Locale.getDefault()));
System.out.println(messageSource.getMessage("greeting", new String[]{"GeunChang"}, Locale.KOREA));
System.out.println(messageSource.getMessage("greeting", new String[]{"GeunChang"}, Locale.US));

}
}
messages_en_US.properties
1
greeting=hello~! {0}
messages_ko_KR.properties
1
greeting=안녕~! {0}
messages.properties 출력할 로케일이 없을 경우 여길 참조된다.
1
greeting=Locale not imple~! {0}
  • 일반적인 윈도우 환경인 경우 getDefault여도 ko_KR이 찍힌다.

  • JVM 구동시 OS환경변수의 정보를 읽는데 해당 OS에 정의된 ko_KR을 읽는거라 추측된다.

  • 구현체 3종류

    1. org.springframework.context.support.StaticMessageSource
    • 가장 간단한 구현체, 테스트시 많이 사용, 기본 다국어 지원
    1. org.springframework.context.support.ResourceBundleMessageSource
    • esourceBundle Class, MessageFormat Class 기반으로 만들어져서 Bundle에 특정 명칭으로 접근 가능
    1. org.springframework.context.support.ReloadableResourceBundelMessageSource
    • Property 설정을 통해 Reloading 정보를 입력해 주기적인 Message Reloading을 수행한다. Application 종료없이 실행 도중에 변경 가능한 장점.

구현체 3번을 테스트 해보자
위의 예에서 messageSource 구현체를 바꿔보도록한다.

MessageSource구현을 직접 작성
1
2
3
4
5
6
7
@Bean
public MessageSource messageSource() {
var messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:/messages");
messageSource.setDefaultEncoding("UTF-8"); //안깨지면 안해줘도 되지만, 명시적 선언은 해주는게 좋은것 같다.
return messageSource;
}

여기까지는 결과가 같다.

AppRunner의 run부분을 1초 간격으로 찍게 한다.
1
2
3
4
5
while(true) {
System.out.println(messageSource.getMessage("greeting", new String[]{"GeunChang"}, Locale.KOREA));
System.out.println(messageSource.getMessage("greeting", new String[]{"GeunChang"}, Locale.US));
System.out.println("");
Thread.sleep(1000); //1초 간격
MessageSource.java MessageSource구현 작성에 캐쉬를 3초로 잡았다.
1
2
3
4
5
6
7
8
9
10

@Bean
public MessageSource messageSource() {
var messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:/messages");
//안깨지면 안해줘도 되지만, 명시적 선언은 해주는게 좋은것 같다.
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheSeconds(3);
return messageSource;
}

내용

  • 실행하면 계속적으로 1초간격으로 메세지가 출력된다.
  • 이 상태에서 프로퍼티의 문구를 수정하면 반영된다.
  • 당연히 프로퍼티 수정 저장만 하면 바뀌지 않는다.
  • 빌드된 .class를 읽는 것이기에 빌드를 해야만 반영이 된다.

ApplicationEventPublisher

역시 ApplicationContext가 구현하는 인터페이스중 하나로 이벤트 프로그래밍에 필요한 옵저버 패턴 구현체이다.

스프링 4.2 이전에는 ApplicationEvent 클래스를 상속하여 사용하였다.

4.2이전까지 사용하던 클래스 상속 이벤트 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyEvent extends ApplicationEvent {

//커스텀 이벤트를 위한 변수
private int data;

//원함수 생성자: 이벤트 받은 소스만 전달
public MyEvent(Object source) {
super(source);
}

// 실제로는 이런식으로 따로 추가되는 아규먼트가 많을 것이다.
public MyEvent(Object source, int data) {
super(source);
this.data=data;
}
}
  • 이 이벤트 자체는 는 빈으로 등록 되는 것이 아니다.
  • 기본 메서드는 이벤트를 받은 소스만 전달하고 있다.
  • 원하는 데이터가 있다면 그 데이터를 받는 생성자 메서드를 새로 만들 수 있다.
  • ApplicationContext 주입후 사용해도 등록되지만 위에서 봤던 messageSource처럼
    ApplicationEventPublisher로 받는 것이 좋다.
AppRunner에서 실행한 예시, run 안쪽만 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class AppRunner implements ApplicationRunner {

@Autowired
//ApplicationContext도 가능하지만 목적에 맞게 받는게 좋다.
ApplicationEventPublisher publisherEvent;

@Override
public void run(ApplicationArguments args) throws Exception {
//event 발생
publisherEvent.publishEvent(new MyEvent(this,100));
}
}

그럼 이벤트를 받아서 처리하는 이벤트 핸들러는 어떻게 만들어야 할까?
이벤트 핸들러는 빈으로 등록이 되어야한다.
역시 4.2 이전에는 이벤트 핸들러도 ApplicationListener라는 인터페이스를 구현했어야 한다.

4.2이전의 이벤트 핸들러, 특정 인터페이스를 구현.
1
2
3
4
5
6
7
8
9
@Component
public class MyEventHandler implements ApplicationListener<MyEvent> {

@Override
public void onApplicationEvent(MyEvent event) {
//TODO
System.out.println("receive event. data : " + event.getData());
}
}
  • 실행해보면 100이 잘 찍히는 것을 볼 수 있다.
  • AppRunner의 run부분의 event발생부분을 잘 보도록 하자.
  • 이벤트 발생하면 빈들중 해당 이벤트를 받는 이벤트핸들러 리스너 메소드가 실행된다.

4.2 이후에는 이제 특정 클래스를 상속받을 일이 없다.

4.2이후의 이벤트. 스프링코드가 없는 POJO코드가 되었다.
1
2
3
4
5
6
7
8
9
10
11
public class MyEvent {
private int data;
private Object source;

public MyEvent(Object source, int data) {
this.source=source;
this.data=data;
}

public int getData() {return data;}
}
  • 프레임워크 코드가 들어있지 않는 POJO코드
  • 스프링 프레임워크가 지향하는 비침투성
  • POJO 스러울수록 테스트도 원할하고 유지보수가 쉽다.
  • 과거나 지금이나 이벤트는 빈이 아님을 명심하자.

이벤트핸들러 역시 리스너 인터페이스를 구현할 필요가 없어졌다.

4.2이후 바뀐 이벤트 리스너, 역시 상속,구현이 없다.
1
2
3
4
5
6
7
8
@Component
public class MyEventHandler {

@EventListener
public void handle(MyEvent event) {
System.out.println("receive event. data : " + event.getData());
}
}
  • 핸들러는 과거나 지금이나 빈이어야 한다.
    (그래야 스프링 프레임워크가 특정 이벤트를 누구한테 전달할지 알 수 있다)

  • 특정 인터페이스 구현이 아니기에 메소드 이름도 마음대로 바꿀 수 있다.

  • 결과는 동일하다.

  • 좀더 자세히 알고 싶으면 API Document를 살펴본다.

  • 특정 이벤트에 대하여 핸들러가 2개 이상이 있다면?

    • 모두 실행된다.
    • 순차적으로 실행된다.
      • 동시에 다른 쓰레드 실행이 아니다.
      • 각 이벤트에서 Thread.currentThread().toString()을 찍어보면 동일하다.
      • 즉 같은 쓰레드에서 순차적으로 실행됨을 알 수 있다.
    • 기본적으로 어떤 핸들러가 먼저 실행될 지는 알수 없다. 다만 순서를 정할 수 있다.
      • @Order어노테이션을 사용한다.(스프링의 여러 곳에서 많이 쓰이는 어노테이션)
      • 예시
        • 숫자등으로 부여 가능하다. 높은 숫자가 우선 순위
        • 가장 높은 우선순위 : @Order(Ordered.HIGHEST_PRECEDENCE)
        • 숫자를 더하면 더 올라간다: @Order(Ordered.HIGHEST_PRECEDENCE+2)
  • 비동기 동기

    • 기본적으로는 동기(Synchronized)

    • 비동기로 실행하고 싶다면 핸들 메소드에 @Async와 함께 사용한다.

    • @Async만 붙인다고 비동기로 실행되는 것이 아니라 비동기를 On해야 한다.

    • @EnableAsync를 하면 이제 비동기가 On되서 비동기로 작동된다.

    • 원래는 쓰레드풀 관련 설정을 더 해야한다. 나중에 Sync에 대해서 더 공부.

    • 비동기인경우 각각의 쓰레드 풀에서 따로 돌고 그 쓰레드 스케쥴링에 달려 있기 때문에 순서는 당연히 보장이 안되며 @Order도 더이상 의미가 없어진다.

    • 스프링이 제공하는 기본 이벤트

      • ContextRefreshedEvent: ApplicationContext초기화,refresh할 때 발생.
      • ContextStartedEvent: ApplicationContext를 start()하여 라이프사이클 빈들이 시작 신호를 받은 시점에 발생.
      • ContextStoppedEvent: ApplicationContext를 stop()하여 라이프사이클 빈들이 정지 신호를 받은 시점에 발생.
      • ContextClosedEvent: ApplicationContext를 close()하여 싱글톤 빈 소멸 시점에 발생
      • RequestHandledEvent: HTTP 요청을 처리했을때 발생
      • 이 이벤트들은 ApplicationContext 관한 이벤트이므로 event.getApplicationContext()등으로 ApplicationContext를 꺼낼 수도 있다.
      • 스프링 부트에서는 이런 이벤트들이 더 확장되어서 제공된다.

ResourceLoader

역시 ApplicationContext가 구현하는 인터페이스중 하나로 리소스를 읽어오는 기능을 제공하는 인터페이스다. 따라서 ResourceLoader역할도 할 수 있다.

  • 앞서의 인터페이스와 마찬가지로 ApplicationContext로 @Autowired 받을 수 있지만 ResourceLoader로 받는게 직관적
  • getResource(“[문자열]”) 이거 하나분인데 안의 문자열이..복잡하다면 복잡.
러너에서 실행해보는 예시
1
2
3
4
5
6
7
8
@Override
public void run(ApplicationArguments args) throws Exception {
Resource resource = resourceLoader.getResource("classpath:sample.txt");
System.out.println(resource.exists()); //해당 파일 존재 여부 출력
System.out.println(resource.getDescription()); //설명 출력
//readString은 자바11부터 추가 되었다.
System.out.println(Files.readString(Path.of(resource.getURI())));
}

ApplicationContext 정리

지금까지 ApplicationContext의 인터페이스들을 살펴보았음.

  • ApplicationContext은 단순 빈 팩토리기능뿐 아니라 리소스로더, 이벤트 퍼플리셔, 환경등 여러 기능을 가지고 있다.

추상화

스프링 레퍼런스의 많은 양을 차지한다.
수많은 추상화가 있다.

Resource 추상화

  • 특징

    • java.net.URL을 추상화한 것
    • 스프링 내부에서 많이 사용
      ex)xml기반 빈설정시 ClassPathXmlApplicationContext(“URL”)에서 이미 사용.
  • 추상화 이유

    • 클래스패스 기준으로 리소스 읽어오는 기능이 없다.
    • ServletContext를 기준으로 상대 경로로 읽어오는 기능 부재
    • 새로운 핸들러를 등록하여 특별한 URL접미사를 만들어 사용할 수 있지만 구현이 어렵고 편의성 메소드가 부족하다.
  • 인터페이스 모양

    • 상속받은 인터페이스 (interface Resource extends ~ )
    • 주요메소드
      • exist(): 해당 리소스의 존재여부 확인
      • getDescription(): 전체경로 파일이름 또는 실제 URL을
      • 그외에도 엄청 많음
    • 주요구현체
      • URIResource: java.net.URL참고, http,https, ftp, file, jar등 기본지원
      • ClassPAthResource: classpath접두어 지원
      • FileSystemResource
      • ServletCOntextResource: 웹 어플리케이션 루트에서 상대 경로로 리소스 찾기.
        사실상 가장 많이 쓰인* Clas
  • 리소스를 읽어오기

    • Resource타입은 location문자열과 ApplicationContext타입에 따라 결정(resloved)
      • ClassPathXmlApplicatiopnContext -> ClassPathResource
      • FileSystemXmlApplicationContext -> FileSystemResource
      • WebApplicationContext -> ServletContextResource(대부분의 프로젝트)
    • ApplicationContext타입에 상관없이 리소스 타입을 강제하려면 java.net.URL 접두어중 하나를 사용할 수 있다.
      • classpath:config.xml–> ClassPathResource
      • ‘file:///some/resource/config.xml’ –> FileSystemResource
      • 접두어를 쓰는 이방식을 쓰는 게 추천됨
      • 그냥 텍스트만 있으면 이 리소스가 어디서 왔는지 파악이 어려울 수 있음
      • 접두어가 있으면 좀더 명시적이어서 좋음.
      • 예를들어 WebApplicationContext의 경우 ServletContextResource가 쓰이고 이는 웹 어플리케이션루트, 컨텍스트패스부터 찾게 되는데 스프링 부트의 임베디드 톰캣은 컨텍스트 패스가 기본값이 설정되어 있지 않으므로 리소스를 찾을수 없게 된다. 이때 명시적으로 classpath접두사를 사용하면 ClassPathResource가 사용되기 때문에 위의 문제가 사라진다.

Validation 추상화

Valididation: 어플리케이션에서 사용하는 객체 검증용 인터페이스
–> org.springframework.validation.Validator

특징

  • 스프링 MVC에서 주로 사용

  • 그렇다고 웹 계층 전용 은 아님, 모든 계층에서 사용 가능

  • 구현체중 하나. JSR303(Bean Validation 1.0), JSR-349(Bean Validation 1.1)지원

  • Bean Validation 2.0.1(최신버전)도 지원

  • Bean Validation이 제공하 여러 Validation 어노테이션 사용해서 검증 가능

  • Bean Validation이란?

    • JAVA 표준 스펙, JAVA EE 스펙중 하나
    • 어노테이션 종류: NotNull, NotEmpty, NotBlank, Email, Size등등
  • DataBinder에 들어가 바인딩 할 때 같이 사용되기도 한다.

  • 인터페이스 : 2가지 메소드 구현 필욧

    • boolean supports(Class clazz):
      • 어떤 타입의 객체를 검증할 때 사용할 것인지 결정
    • void validate)Object obj, Errors e):
      • 실제 검증 로직을 이 안에서 구현
      • 구현할 떄 ValidationUtils 사용하며 편리함
      • 꼭 ValidationUtils을 쓸 필요 없고 원하면 내가 직접 처리 후 errors에 추가
      • 특정필드 에러인 경우 errors.rejectValue에 담고 전반적인 에러인 경우
        errors.reject에 담으면 된다.

1.원시적인 예제

EventValidator.java title이 empty,space인지 검증한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
public class EventValidator implements Validator {

@Override
public boolean supports(Class<?> clazz) {
return MyEvent.class.equals(clazz);
}

@Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "title",
"empty","empty title is not allowed");
}
}
  • ValidationUtil을 써서 간편하게 처리 가능.
  • rejectIfEmptyOrWhitespace 인자
    • 필드, errcode, defaultMessage
  • errcode
    • 실제 메세지를 가져 올 수 있는 메세지 키값에 해당하는 코드
    • 위에서 메세지 처리 담당하는 MessageResolver부분이랑 연동 가능
    • empty.title 이렇게 적지 않은이유? 자동생성이 있음. 이따가 결과에서 확인.
  • default Message
    • errCode로 메세지를 찾지 못했을 때 디폴트로 사용될 메세지
AppRunner의 run에서 테스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void run(ApplicationArguments args) throws Exception {
MyEvent event = new MyEvent(this,100, "");
EventValidator eventValidator = new EventValidator();
Errors errors = new BeanPropertyBindingResult(event,"eventName");

eventValidator.validate(event, errors);

System.out.println(errors.hasErrors());

errors.getAllErrors().forEach(e -> {
System.out.println("=====err code =====");
Arrays.stream(e.getCodes()).forEach(System.out::println);
System.out.println("기본메세지: "+e.getDefaultMessage());
});
  • 이벤트의 title은 에러를 일으키기 위해서 빈값을 주었음
  • BeanPropertyBindingResult
    • Errors 인터페이스의 구현체중 하나
    • 실제 스프링 MVC 사용시 이 구현체를 자동으로 생성해서 파라미터로 넘어간다.
    • 그래서 이 구현체 클래스를 직접 사용할 일은 거의 없다.
    • 지금 이것도 원시적인 테스트 샘플이라 이런 식으로 작성
    • 다만 Errors인터페이스는 자주 접하게 된다.
결과 출력
1
2
3
4
5
6
7
true
=====err code =====
notempty.eventName.title
notempty.title
notempty.java.lang.String
notempty
기본메세지: empty title is not allowed
  • validate후 당연히 에러가 생겼기때문에 true가 출력
  • 내가 설정한 notempty외에 3개의 에러코드가 자동으로 생성
  • 필요하면 이 코드를 이용해 프로퍼티등에서 메세지를 출력 가능

2. 최근

  • 직접적으로 validator를 사용하지 않는다.
    • 무조건 안만드는게 아니라 어노테이션 처리로 가능한 간단한 검증은 만들 필요가 없음
    • 즉 직접 validator 인터페이스 구현 클래스를 만들 필요 없음
    • 해당 클래스의 인스턴스를 만들 필요도 당현이 없음
  • 스프링 부트 2.0.5 이상 버전 사용시에는 스프링(스프링부트가 아닌)이 제공하는
    LocalValidatorFactoryBean를 빈으로 자동 등록 -> @Autowired로 받을 수 있다.
  • LocalValidatorFactoryBean는 JSR-380(Bean Validator 2.0.1) 구현체로
    hibernate-validator 사용.
    validator를 주입받아 사용한다.
    1
    2
    3
    4
    5
    6
    @Autowired
    Validator validator;
    ...
    ...
    validator.validate(event, errors);// 기존 eventValidator대체
    ...
    실행하면 에러가 없다고 뜬다. 에러를 검증할 내용이 없기 때문이다.
이제 Bean Validator 어노테이션을 직접 이벤트에 달면 처리된다.
1
2
@NotEmpty
private String title;

이제 실행해보면 정상적으로 에러가 발생해서 1번의 내용대로 출력이 된다.

  • NotEmpty 말고도 @Min, @Email등 많이 존재
  • default 메세지도 맞게 생성해준다.
  • 복잡한 비지니스 로직 검증이 필요한 경우엔 기존처럼 validator를 만들어 쓴다.

Data Binding 추상화

  • Data Binding

    • 기술적 관점 : 프로퍼티 값을 타겟 객체에 설정하는 기능
    • 사용자 관점 : 사용자 입력 값을 어플리케이션 도메인 모델에 동적으로 변환해
      넣어주 는 기능
    • 결론 : 사용자의 입력값은 거의 대부분 문자열인데 이를 객체가 가지고 있는 int,
      long, Boolean, Date 등 뿐 아니라 Event, Book 같은 도메인 타입으로도 변환해서 넣어주는 기능
  • 인터페이스 : org.springframework.validation.DataBinder

    • 로드 존슨이 만든 굉장히 오래된 인터페이스지만 지금도 널리 쓰인다.
    • PropertyEditor
      • 스프링 3.0 이전까지 DataBinder가 변환 작업 사용하던 인터페이스
      • 예전 xml설정된 문자열을 빈이 가진 적절한 타입으로 변환할 때도 사용되었다.
      • SpEL에서도 사용.
      • 쓰레드 세이프 X (상태정보저장하므로 싱글톤 빈으로 등록해서 쓰다가는…)
        (밑에서 예제로 설명)
      • Object와 String간의 변환만 가능해서 사용범위가 제한적
      • 그래도 그런 경우가 대부분이라 잘 사용해왔음.

1. 가장 고전적인 Data Binding 예제

이벤트 콘트럴러 예제
1
2
3
4
5
6
7
8
@RestController
public class EventController {
@GetMapping("/event/{eventDomain}")
public String getEvent(@PathVariable EventDomain eventDomain) {
System.out.println(eventDomain);
return eventDomain.getId().toString();
}
}
  • 개념

    • 입력은 “/evnet/1”, “/evnet/2”식으로 들어오며 {}에는 id가 들어온다
    • 컨트롤러는 들어오는 숫자를 EventDomain 타입으로 변환해서 스프링이 받는다.
    • 이후 컨트롤러안에서는 이 도메인 타입으로 코딩을 한다.
      테스트 클래스
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      package me.rkaehdaos.demo1;

      import org.junit.Test;
      import org.junit.runner.RunWith;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
      import org.springframework.test.context.junit4.SpringRunner;
      import org.springframework.test.web.servlet.MockMvc;

      import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
      import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
      import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

      @RunWith(SpringRunner.class)
      @WebMvcTest
      public class EventControllerTest {
      @Autowired
      MockMvc mockMvc;

      @Test
      public void getTest() throws Exception {
      mockMvc.perform(get("/event/1"))
      .andExpect(status().isOk())
      .andExpect(content().string("1"));
      }
      }
  • 결과

    • 테스트를 하면 에러 발생
      -> 2018-12-23 12:14:25.997 WARN 13200 — [ main] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException: Failed to convert value of type ‘java.lang.String’ to required type ‘me.rkaehdaos.demo1.EventDomain’; nested exception is java.lang.IllegalStateException: Cannot convert value of type ‘java.lang.String’ to required type ‘me.rkaehdaos.demo1.EventDomain’: no matching editors or conversion strategy found]
    • 설명 그대로 입력값으로 들어온 숫자(String타입)을 이벤트 타입으로 변환을 못한다.
    • 이에 매칭되는 에디터나 컨버전 전략을 찾지 못했다고 나왔다.
    • 변환하기 위한 PropertyEditor가 필요하다.

PropertyEditor 구현방법

  1. java.beans.PropertyEditor를 implement 한다.
    * 구현해야할 메소드가 너무 많다.
  2. PropertyEditorSupport를 상속한다.
    * 구현할 메소드만 구현하면 된다. 보통 getAsText, setAsText를 구현한다.
PropertyEditorSupport상속한 PropertyEditor
1
2
3
4
5
6
7
8
9
10
11
public class EventEditor extends PropertyEditorSupport {
@Override
public String getAsText() {
EventDomain eventDomain = (EventDomain)getValue();
return eventDomain.getId().toString();
}

@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(new EventDomain(Integer.parseInt(text)));
}
  • setAsText내부의 setValue를 통해 Value를 설정하였다.
  • getAsText내부의 getValue를 가져왔다.
  • 위에서의 value가 PropertyEditor가 가지고 있는 값이며, 이는 각 쓰레드에서
    공유 되고 있다. 이 값은 stateful(상태정보)이며 쓰레드 세이프 하지 않다.
  • 따라서 PropertyEditor의 구현체들은 그냥 빈으로 등록하면 안된다.
  • 굳이 쓰려면 빈의 스코프를 쓰레드스코프로 하면 그나마 쓸 수 있지만 비추
  • 그러면 빈으로 등록하지 않으면서 어떻게 사용이 가능할까?
컨트롤러에서 사용할 바인더 등록방법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class EventController {

@InitBinder
public void init(WebDataBinder webDataBinder) {
webDataBinder.registerCustomEditor(EventDomain.class, new EventEditor());
}


@GetMapping("/event/{eventDomain}")
public String getEvent(@PathVariable EventDomain eventDomain) {
System.out.println(eventDomain);
return eventDomain.getId().toString();
}
}
  • 전역적인 방법도 존재한다, 스프링 MVC 더 공부할 때

  • 위의 @InitBinder가 바인더 등록 부분이다.

  • WebDataBinder가 바로 DataBinder의 구현체중 하나다.

  • 결론

    • 이런 프로퍼티 리턴은 편리하지가 않다.
    • 구현도 어렵고 쓰레드 세이프하지도 않아서 빈등록도 위험하다.
    • Object와 String간의 변환만 가능해서 사용범위가 제한적
    • 스프링 3 이전에서 쓰던 기능이다.

2. Converte와 Formatter

위에서 봤던 PropertyEditor의 단점 때문에 Converter라는게 생겼다.

  • Converter

    • 기존 단점 (쓰레드위험, String과 Object변환만 지원등등)을 커버
  • 스프링 3부터 들어온 기능

  • S타입을 T타입으로 변환이 가능한 매우 일반적인 변환기

    위의 PropertyEditor와 동일한 기능의 Converter.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class EventConverter {
    @Component //빈 등록 가능
    public static class StringToEventConverter implements Converter<String, EventDomain>{
    @Override
    public EventDomain convert(String source) {
    return new EventDomain(Integer.parseInt(source));
    }
    }

    public static class EventToStringConverter implements Converter<EventDomain, String>{
    @Override
    public String convert(EventDomain source) {
    return source.getId().toString();
    }
    }
    }
  • 예제에서 PropertyEditor의 역할을 대신하고 있다.

  • 상태정보가 없으므로 Stateless하며 빈등록이 가능하다.

  • ConverterRegistry에 등록해야 사용이 가능한 저 인터페이스를 직접 쓸 일은 거의 없다

  • 스프링부트 없이 MVC를 쓴다면 다음과 같이 가능하다.

    스프링부트 없이 레지스트리에 컨버터를 등록하는 방법
    1
    2
    3
    4
    5
    6
    7
    8
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new EventConverter.StringToEventConverter());
    registry.addConverter(new EventConverter.EventToStringConverter());
    }
    }
  • 이제 InitBinder부분을 지우거나 주석처리후 실행해보면 테스트가 잘 돌아간다.

  • 컨트롤러에 요청된 값이 컨버터에서 정상적으로 eventDomain타입으로 바뀌었다.

  • 위와 같은 간단한 Integer String 변환등은 기본적으로 등록된 컨버터들이 해준다.

  • 결론

    • 너무 제너럴 하다.
    • 웹쪽의 경우 사용자 입력값이 거의 문자열
    • 또 messageSource를 통해 다국화 메세지 기능
    • 좀더 웹쪽에 특화된 인터페이스가 필요 -> 이것이 스프링이 제공하는 Formatter
  • Formatter

    • PropertyEditor 대체제
    • Object <-> String 변환 담당
    • 다국화 제공
    • FormatterRegistry에 등록해서 사용
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @Component //쓰레드 세이프하므로 빈등록 가능
      public class EventFormatter implements Formatter<EventDomain> {
      @Override
      public EventDomain parse(String source, Locale locale) throws ParseException {
      return new EventDomain(Integer.parseInt(source));
      }
      @Override
      public String print(EventDomain object, Locale locale) {
      return object.getId().toString();
      }
      }
  • Formatter : 소스만 설정한다.

  • 구현 메소드는 2가지이며 PropertyEditor와 비슷하다.

  • Locale 정보 기반으로 바꿀 수 있다는것은 다른점이다.

  • 쓰레드 세이프 하며 빈등록 가능

  • 빈 등록이 가능하다는 뜻은 빈 주입도 받을수 있다는 뜻이므로 messageSource등을
    주입받아서 locale등과 결합하여 메세지 출력등이 가능하다.

    WebConfig.java 앞에서 등록한 컨버터는 주석처리하고 Formmatter만 등록
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
    //converter 처리한것은 주석처리
    // FormmatterRegistry는 ConverterRegistry를 상속한 것이므로
    // Converter도 등록이 가능하다.
    /*

    registry.addConverter(new EventConverter.StringToEventConverter());
    registry.addConverter(new EventConverter.EventToStringConverter());
    */
    registry.addFormatter(new EventFormatter());

    }
    }
  • ConversionService

    • 타입을 변환하는 작업은 DataBinder가 아닌 Converter와 Formatter를 활용할 수 있게 해주는 서비스
    • 위에처럼 registry에 Formmatter를 등록하면 ConversionService에 등록되는 것이고 ConversionService를 통해서 실제 변환하는 작업을 하는 것이다.
    • 스프링MVC, 빈(Value)설정, SpEL에서 사용
    • DefaultFormattingConversionService : 스프링이 제공하는 구현체중 하나
      • FormatterRegistry 기능 -> Converter Registry 구현
      • ConversionService 기능
      • ConversionService 기능 ->
      • 위 두 인터페이스 전부 implement 한 구현체임
      • 여러 기본 컨버터,포맷터 등록함.
  • Spring boot

    • 기본 제공하는 구현체가 WebConversionService
    • WebConversionService는 DefaultFormattingConversionService를 상속하여
      더 많은 기능을 구현
    • Formmatter와 Converter 빈을 찾아서 자동으로 등록해 준다.
      • 지금처럼 WebMvcConfigurer를 새로 만들 필요 자체가 없어진다.
      • registry 등록 부분 없애고 Converter의 각 static을 빈등록하면 잘된다.
      • 이번엔 Formmatter를 빈등록하면 테스트는 에러, 브라우저 테스트는 잘된다.
      • @WebMvcTest: 슬라이싱 테스트, 웹관련 빈만 등록(컨트롤러류)
      • 실제테스트 결과 Converter시는 잘되고 Formmatter는 빈등록 안하는 듯
      • 이경우 @WebMvcTest({EventFormatter.class, EventController.class}) 식으로
        빈등록이 필요한 클래스들을 따로 넘겨면 테스트가 잘 된다.
      • 테스트시 필요한 빈등록되는 리스트들을 명시적으로 하는 것도 좋은 방법이다.
  • 기타

  • Formmatter 추천 방법: 보통 웹관련이므로.

  • JPA사용시엔 JPA 컨버터가 자동 등록 됨

  • 등록된 컨버터나 포맷터 리스트 볼 수 있는 방법?

    • ConversionService 를 출력해보면 많은 컨버터들이 등록 되어 있음을 알 수 있다.
    • toString()에 출력되게 다 되어 있다.

SpEL

  • Spring Expression Language

  • 스프링 EL이라고 많이 부른다.

  • 객체 그래프를 조회하고 조작하는 기능을 제공

  • Unified EL과 비슷하지만, 메소드 호출 지원, 문자열 템플릿 기능도 제공

  • OGNL, MVEL, JBOss EL등 자바에서 사용할 수 있는 여러 EL이 존재하지만, SpEL은
    모든 스프링 프로젝트 전반에 걸쳐 사용할 EL을 목표로 해서 Core단에 들어가 있다.

  • Core단에 들어가 있다고 해서 Core에서만 쓰이는것이 아니라 스프링 시큐리티, JPA,
    타임리프 및 스프링 클라우드의 여러 곳에서 쓰이고 있음.

  • 스프링 3.0 부터 지원하기 시작

  • Properties, Arrays, Lists, Maps, Indexers 다 지원

    • 인라인 리스트나 인라인 맵도 가능
    • 메소드 호출 기능도 존재
    • 레퍼런스를 한번 살펴보도록 하자
  • 실제 쓰임

    • 표현식 : #{“표현식”}
    • 프로퍼티 참조 : ${“프로퍼티”}
      SpEL 표현식 예제
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15

      @Value("#{10+10}") // #{"표현식"}
      int value;

      @Value("#{'hello'+ ' '+'world'}") // 문자열 연산도 가능
      String greeting;

      @Value("#{1 eq 1}") //boolean 논리 처리도 가능
      boolean trueOrFalse;

      @Value("${ahn.value}") //프로퍼티 참조는 $를 이용해서 가능
      int number; //프로퍼티파일 내에서는 표현식 불가하다.

      @Value("#{${ahn.value} eq 100}") //표현식 안에 프로퍼티를 넣는 식도 가능
      boolean trueOrFalse2;
  • Advanced

    • 이해를 높이려면 ExpressionParser와 Context(EvaluationContext등의)를 보자
    • 필요하면 ExpressionParser를 코딩으로 작성 가능
    • 더 깊게 가면 config를 넣을 수도 있음
      직접 코딩 예
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
             ExpressionParser parser = new SpelExpressionParser();
      Expression expression = parser.parseExpression("1+1");
      Integer value2 = expression.getValue(Integer.class);
      System.out.println(value2);
      ```
      * 이 Spel도 위에서 봤던 ConversionService를 사용해서 해당 타입으로 변환한 것이다.

      ## 스프링 AOP

      ### 1.기본 전체적으로
      * AOP 구현체 제공
      * 자바의 AspectJ와 연계할 수 있는 기능도 제공
      * 이를 바탕으로 스프링 트랜잭션이나 캐시등의 다른 스프링 기능도 작동 됨
      * AOP
      * Aspect를 모듈화 할 수 있는 프로그래밍 기법
      * OOP를 보완하는 수단.
      * 흩어진 관심사(Concern)들을 모을 수 있다.
      * 트랜잭션처리, 로깅등의 횡단 관심사
      * 용어들
      * 많이 어려운 것은 아닌데 안쓰면 햇갈릴 때도 있음
      * Aspect : 흩어진 관심사를 모은 하나의 모듈로 Advice와 Pointcut이 들어간다.
      * Advice : 관심사 작동 하는 부분, 해야할 일
      * Target: : AOP가 적용될 객체
      * JoinPoint : 합류 지점
      * 기본 시점 : 메소드 실행 시점
      * 다양한 조인 포인트가 있음
      * PointCut : Advice가 적용 될 부분으로 Join포인트중 하나

      * AOP 적용 방법
      * 컴파일 : 자바파일을 .class로 만들때 바이트코드를 조작해서 .class를 생성
      * 로드 타입 : .class를 로딩시 로드타임 위빙을 해서 로딩. 메모리에 해당 기능 생성
      * 런타임 : 빈을 만들때(런타임) 프록시 빈을 만든다.

      * AOP 구현체
      * 많다. 위키피디아를 보면 각 언어별 구현체까지 확인 가능하다.
      * AspectJ
      * 엄청나게 많은 조인포인트와 엄청나게 많은 기능을 제공
      * 스프링 AOP
      * 국한적 기능
      * **프록시 기반의 AOP** 구현체
      * **스프링 빈에만 AOP를 적용** 할 수 있다.
      * 스프링AOP의 런타임 위빙이 가장 현실적인 방법이다.
      * AspectJ긔 많은 조인포인트가 필요한 경우 별도의 컴파일러로 컴파일하거나
      별도의 자바 에이전트를 설정해서 로드타임 위빙을 사용하는 방법으로 연동한다.

      ### 프록시기반 AOP
      * 스프링 AOP는 **프록시 기반의 AOP** 구현체
      * **스프링 빈에만 AOP를 적용** 할 수 있다.
      * 모든 AOP기능을 제공하는 것이 목적이 아니라, 스프링IoC와 연동하여 엔터프라이즈
      어플리케이션에서 가장 흔한 문제에 대한 해결책을 제공하는 것이 목적.

      * 프록시 패턴
      * 디자인 패턴
      * 클라이언트는 인터페이스를 바라본다.
      * 프록시객체는 해당 인터페이스를 구현하며 실제 객체를 바라본다.
      * 목적: 접근 제어 또는 부가 기능 추가
      * 클라이언트 코드와 기본 실제 객체의 코드 변경없이 목적 달성 가능

      * 문제점
      * 매번 프록시 클래스를 작성해야 하는가?
      * 여러 클래스 여러 메소드에 적용하려면?
      * 객체들 관계가 매우 복잡할 때는?

      * 스프링 AOP의 해결방법
      * 스프링 Ioc컨테이너가 제공하는 기반 시설과 다이나믹 프록시를 혼합해서 복잡한
      문제를 심플하게 해결하려 함
      * 다이나믹 프록시
      * 동적으로(런타임에) 프록시 객체 생성하는 방법
      * 자바가 제공하는 방법은 인터페이스 기반 프록시 생성
      * CGlib는 클래스 기반 프록시도 지원
      * 스프링 IoC : 기존 빈을 대체하는 동적 프록시 빈을 만들어 등록 시켜 준다.
      * 클라이언트 코드 변경 없음
      * AbstractAutoProxyCreator implement BeanPostProcessor
      * BeanPostProcessor: 빈이 등록후 빈을 가공할 수 있는 라이프 사이클
      * 빈이 등록이 되면 AbstractAutoProxyCreator라는 빈 포스트 프로세서로
      해당 빈을 감싸는 프록시 빈을 생성후 그 빈을 등록까지 해줌.

      ### @AOP
      * 어노테이션 기반의 스프링 @AOP
      * 보통 start-web에 왠만한건 다 있었지만 AOP는 별도의 의존성 추가가 필요

      #### 의존성 추가

      ```properties
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
      </dependency>

Aspect 정의

  • @Aspect
  • 빈으로 등록해야하니까 (ㅓ컴포넌트 스캔을 사용한다면) @Component도 추가
    @AOP 예제 execution표현식을 잘 볼 것
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Component
    @Aspect
    public class PerfAspect {

    @Around("execution(* com.example..*.EventService.*(..))")
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable{
    //ProceedingJoinPoint : 리플렉션쪽의 메서드를 래핑하고 있다고 생각하자.
    long begin = System.currentTimeMillis();
    Object retVal = pjp.proceed(); //Method 의 Invoke라고 생각하자
    System.out.println("걸린시간 :");
    System.out.println(System.currentTimeMillis()-begin);
    return retVal;
    }
    }
  • around를 이용하면 메소드 앞 뒤에 부가 기능 가능
  • execution을 이용하면 손쉽게 많은 메소드를 선택이 가능하다.
  • 어노테이션으로 등록하는것이 더 유용할 수도 있다.
    • 규칙중에 특정 메소드를 빼고 싶은 경우
execution대신 @annotation으로 표현식을 바꿈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
@Aspect
public class PerfAspect {

// @Around("execution(* com.example..*.EventService.*(..))")
@Around("@annotation(PerfLogging)")
public Object logPerf(ProceedingJoinPoint pjp) throws Throwable{
//ProceedingJoinPoint : 리플렉션쪽의 메서드를 래핑하고 있다고 생각하자.
long begin = System.currentTimeMillis();
Object retVal = pjp.proceed(); //Method 의 Invoke라고 생각하자
System.out.println("걸린시간 :");
System.out.println(System.currentTimeMillis()-begin);
return retVal;
}
}

  • execution만으로도 처리가 가능하지만 excution은 논리 연산등이 되지 않기에 불편.

  • 어노테이션을 쓰면 필요한 메소드마다 어노테이션을 달아야함.

  • 불편해보이나 IDE의 도움을 못받는 경우도 있는 걸 감안하면 훨씬 유용

  • 표현식에 bean(빈이름)으로 하면 빈을 참조해서 해당 빈의 모든 public 메소드를 타겟

  • 어드바이스 정의

    • @Before
    • @AfterReturning
    • @AfterThrowing
    • @Around

Null-safety

스프링 5에 추가된 Null 관련 어노테이션
–> (툴의지원을 받아) 컴파일 시점에 최대한 NullPointerException을 방지하는 것

  • @NonNull
  • @Nullable
  • @NonNullApi (패키지 레벨 설정)
  • @NonNullFields (패키지 레벨 설정)
  • 메소드 붙이면 리턴값에
  • 인자에 붙이면 해당 인자값에
  • JPA, 리액터쪽에서도 지원

Related POST

공유하기