작년 말부터 준비한 토비의 스프링 3 증보판이 마무리되서 지난 주말부터 인터넷 서점에서 예판을 시작했다. 실제 출간일은 이달 말이다.

이번 증보판은 스프링 3.0을 기준으로 설명한 토비의 스프링 3 내용에 스프링 3.1내용을 200여페이지 추가한 것이다. 덕분에 책을 두 권으로 분리해야 했고 두권을 합쳐 전체 페이지 수가 1700이 넘게 되었다. 분책을 하다보니 각 권만 선택해 책을 보는 독자들의 편의를 위해 앞의 부속글과 부록, 찾아보기 등이 양 권에 모두 들어가서 페이지 수가 좀 더 늘어났다.

Vol1은 예전 1부 내용에 user 예제 코드를 스프링 3.1의 DI 스타일로 업그레이드 하는 과정과 그 특징을 설명하는 내용을 추가했다. Vol2는 예전 2부의 스프링 기술 설명에 각 장마다 스프링 3.1에 새로 등장한 내용을 다루는 절을 추가했다. 스프링 3.0 내용도 일부 추가된 것이 있다. 3.0.3이후에 나온 mvc 네임스페이스의 일부 태그에 관한 설명이 MVC를 다루는 장에 추가됐다.

책이 두 권으로 분권됐기 때문에 낱권으로 구매가 가능하다. 물론 세트로 구입할 수도 있다.

그 밖에 하고 싶은 얘기가 정말 많긴 한데… 이번엔 그냥 여기까지만.

성윤님이 코드로 등록해 로그를 찍어 공개하는 바람에 김이 빠져버린 퀴즈. 원래 문제만 보고 푸는 퀴즈였는데…

어쨌든 답을 정리하고 설명해보자. 매번 로그 찍어가며 원하는 요청 조건을 찾아낼 수는 없으니까 각 요청조건의 적용 규칙을 알아야지.

@RequestMapping의 각 항목은 RequestCondition 타입으로 만들어진 요청조건 클래스로 정의된다. @RequestMapping의 엘리먼트 타입은 모두 배열이니 {}를 이용해 복수개 설정이 가능하다. 또, 타입레벨과 메소드 레벨 양쪽에 정의가 가능하다. 최종 적용은 메소드 단위이니 타입 레벨의 조건과 어떻게 결합되는지 알아야 한다. 여기에 수퍼 클래스나 인터페이스의 타입&메소드 레벨 조건도 결합할 수 있으니 단순하게 봐도 조합 방법이 엘리먼트 값 여러 개 * 메소드&타입 * 수퍼 타입&메소드 해서 최소 8가지. 문제는 요청조건마다 엘리먼트 값의 조합이나 메소드&타입 조합방법이 다르다는 것. 

문제를 살펴보자.

1.
@Controller
@RequestMapping({“/a”, “/b”})
public class MyController {
  @RequestMapping({“/c”, “/d”})
  public void hello() {}

이건 URL경로 조건. 디폴트 엘리먼트라 value로 해도 되고, 단독으로 사용되면 엘리먼트 이름은 생략 가능.

각 엘리먼트 값의 조합은 OR. 메소드만 놓고 보면 /c 또는 /d.

타입과 메소드의 조합도 OR이지만 단순히 a,b,c,d로 조합하는게 아니라 타입 경로+메소드 경로로 결합 가능한 모든 경우로 조합한다는 것이 특징이다. 결국 /a/c, /a/d, /b/c, /b/d의 네 가지 조합이 만들어지고 OR로 결합되니 이 중의 하나면 된다. AND라면 말이 안되니까. 한가지 주의할 점은 다른 조건과 다르게 URL을 조건을 지정안 한다고 조건을 다 만족하는게 아니다. URL조건이 없으면 ""인데 suffix / 규칙 때문에 "/”나 마찬가지다. 결국 서블릿 매핑의 루트 URL에 연결된다.

최종 요청조건은 {[/a/c || /a/d || /b/c || /b/d], methods=[], params=[], headers=[], consumes=[], produces=[], custom=[]} 으로 표현된다. 스프링 로그를 찍어보면 조합된 조건이 이렇게 간결한 식으로 나온다.

2.
@Controller
@RequestMapping(params={“a”, “b”})
public class MyController {
  @RequestMapping(params={“c”, “d”})
  public void hello() {}

파라미터 조합은 AND다. 엘리먼트 값들도 AND로. 타입, 메소드도 AND로 따라서 최종 조건은 한 가지다. 위의 매핑이라면 파라미터에 a,b,c,d 네 가지가 다 존재해야지만 조건 만족.

조건식은 {[], methods=[], params=[a && b && c && d], headers=[], consumes=[], produces=[], custom=[]}

3.
@Controller
@RequestMapping(headers={“a”, “b”})
public class MyController {
  @RequestMapping(headers={“c”, “d”})
  public void hello() {}

헤더 조건도 파라미터와 동일하게 타입, 메소드, 엘리먼트 값이 모두 AND로 조합된다. 매핑 가능한 요청조건은 한 가지.

조건식은 {[], methods=[], params=[], headers=[a && b && c && d], consumes=[], produces=[], custom=[]}

여기까지는 쉬운데 다음부터는 좀 정확한 이해가 필요하다.

4.
@Controller
@RequestMapping(headers={“a”, “Content-Type=application/json”, “Content-Type=multipart/form-data”})
public class MyController {
  @RequestMapping(headers={“c”, “d”})
  public void hello() {}

타입에 헤더 조건이 3개, 메소드에 2개다. 헤더 조건은 AND 조합이라고 했으니 결국 조건은 한 가지?

아니다. 두 가지다. header로 정의했더라도 Context-Type은 무시된다. 헤더 조건은 a,c,d 세 가지만 적용된다. Content-Type은 대신 3.1에 새로 등장한 consumes 조건으로 바뀐다. 따라서 타입에 붙은 두 개의 Content-Type 헤더 조건 두 가지는 consumes에 정의된 것으로 취급되는데, consumes는 헤더 값을 이용하지만 다른 헤더와 달리 AND가 아니라 OR조건이다. 따라서 헤더 조건 3가지가 AND로 묶이고, Content-Type 두 가지는 OR로 묶여서 최종적으로 2가지 요청조건이 만들어진다.

Content-Type은 headers에서 무시하고 몰래 consumes로 처리하는 이유는 뭔가? headers에서 안 쓸 거면 에러를 내든가하지. 왜냐하면 3.0에서 작성된 코드를 3.1에서 사용될 수 있도록 하기 위해서다.  구버전 호환성을 목숨처럼 지키는 스프링 다운 해결책이다.

조건식은 {[], methods=[], params=[], headers=[a && c && d], consumes=[application/json || multipart/form-data], produces=[], custom=[]}

5.
@Controller
@RequestMapping(headers={“a”, “b”})
public class MyController {
  @RequestMapping(headers={“c”, “d”, “Content-Type=application/json”}, consumes=”multipart/form-data”)
  public void hello() {}

4번 문제의 응용이다. 일반 헤더 조건은 AND로 결합해서 한 가지. Content-Type헤더는 consumer로 넘어가므로 consumes가 결국 두 가지가 된다. consumes는 OR이니까 최종 요청조건은 두 가지.

조건식은 {[], methods=[], params=[], headers=[a && b && c && d], consumes=[application/json || multipart/form-data], produces=[], custom=[]}

6.
@Controller
@RequestMapping(consumes={“application/xml”, “application/x-www-form-urlencoded”})
public class MyController {
  @RequestMapping(consumes={”multipart/form-data”, “application/json”})
  public void hello() {}

consumes는 다른 조건과 다르게 메소드 조건이 타입 조건을 오버라이딩하는 식이다. 타입만 있으면 타입 조건을 쓰지만 메소드 조건이 있으면 타입 조건을 무시한다. 그래서 이건 메소드 조건 두 가지만 OR로 결합되서 두 가지.

조건식은 {[], methods=[], params=[], headers=[], consumes=[multipart/form-data || application/json], produces=[], custom=[]}

 

정답은 4,1,1,2,2,2

 

그럼 수퍼 클래스나 인터페이스의 타입&메소드 레벨에 붙은 @RequestMapping과의 조합은? 그건 http://toby.epril.com/?p=1207 에서 설명한 스프링의 일반적인 오버라이딩스러운 규칙을 적용하면 된다.

사실 심심하지는 않다. 스프링 3.1의 내용을 추가한 토스3 개정판 원고를 쓰느라 저녁과 주말의 개인시간을 모두 바쳐야 하는 폐인 생활을 하고 있을 뿐.

아무튼 @RequestMapping에 들어가는 7가지 조건의 조합을 정리하다 보니 이런 내용은 간단한 퀴즈로 만들면 재밌겠다 싶다.

컨트롤러의 @RequestMapping 설정을 보고 hello() 라는 메소드가 호출되게 할 수 있는 HTTP 요청의 개수가 얼마나 될지 세어보는 간단 문제.

먼저 간단한 샘플.

@Controller
public class MyController {
  @RequestMapping({“/a”, “/b”})
  public void hello() {}

이 컨트롤러의 hello 메소드가 실행되게 만들 수 있는 HTTP 요청 조건은 2가지다. URL이 /a 인 경우와 /b인 경우. 물론 나머지 조건(파라미터, 요청 메소드, 헤더 등등)은 어떻든 상관없고.

이제 실전 문제.

1.
@Controller
@RequestMapping({“/a”, “/b”})
public class MyController {
  @RequestMapping({“/c”, “/d”})
  public void hello() {}

2.
@Controller
@RequestMapping(params={“a”, “b”})
public class MyController {
  @RequestMapping(params={“c”, “d”})
  public void hello() {}

3.
@Controller
@RequestMapping(headers={“a”, “b”})
public class MyController {
  @RequestMapping(headers={“c”, “d”})
  public void hello() {}

4.
@Controller
@RequestMapping(headers={“a”, “Content-Type=application/json”, “Content-Type=multipart/form-data”})
public class MyController {
  @RequestMapping(headers={“c”, “d”})
  public void hello() {}

 

5.
@Controller
@RequestMapping(headers={“a”, “b”})
public class MyController {
  @RequestMapping(headers={“c”, “d”, “Content-Type=application/json”}, consumes=”multipart/form-data”)
  public void hello() {}

 

6.
@Controller
@RequestMapping(consumes={“application/xml”, “application/x-www-form-urlencoded”})
public class MyController {
  @RequestMapping(consumes={”multipart/form-data”, “application/json”})
  public void hello() {}

1번부터 6번까지 컨트롤러의 hello() 메소드를 실행하게 만드는 HTTP 요청의 가짓수는 각각 몇개일까?

1,2,3번은 쉽고, 4,5,6번은 각 요청 조건의 결합 특성을 바르게 알고 있으면 어렵지 않다. 문제를 가능한 쉽게 만들기 위해서 한번에 나오는 조건의 종류는 최소화 했고, 인터페이스나 수퍼 클래스에 붙은 @RM의 경우도 안 넣었다.

코멘트에 답 주시는 분 중에서 정답자를 뽑아서 다음 달에 할까 하는 토스3 독자모임/세미나에 참가 우선권을 드리도록 하겠다.

아.. 아무도 관심이 없을지도. 원고나 쓰러 가야지.

애노테이션은 애물단지다. 사용하기 편리해보이는 듯 하지만 이를 이용하도록 라이브러리나 프레임워크를 만든다면, 일반적인 자바의 타입 구조를 활용해서 만드는 것보다 몇배의 수고가 들어간다. 애노테이션은 타입 정보가 아니기 때문에 정보를 읽고 활용하려면 리플렉션 API를 사용해야 한다. 게다가 상속이나 구현을 통해서 정보가 전이되지도 않는다. 메타 애노테이션도 마찬가지다.

애노테이션을 이용해서 코드를 작성하도록 뭔가 만들어보려면 그래서 이런 애노테이션의 제약에 일정한 관례를 부여해야 한다.

스프링을 보자면,

컴포넌트 스캔에서는  클래스의 메타 애노테이션을 모두 뒤져서 @Component가 어느 단계라도 존재하면 빈 후보로 선택한다. 그래서 @Configuration만 달아도 스캔된다. @Configuration 애노테이션의 정의를 보면

@Component
public @interface Configuration {

처럼 @Component가 메타 애노테이션으로 달렸으니까.

그런데 메타 애노테이션에 뭔가 존재한다고 이게 자동으로 애노테이션에 상속되는 것이 아니니, 메타 애노테이션에 뭔가 있는지 알려면 리플렉션을 이용해서 계속 뒤져야 한다.  클래스와 인터페이스로 계층구조를 만들어면 타입정보가 상속되기 때문에 손쉽게 타입 체크를 하거나 다형성을 활용할 수 있는 것과 대조된다. 게다가 정해진 규칙도 없으니 만든 사람이 정한 규칙을 모르면 헷갈린다.

그래서 Josh Bloch은 인터페이스로 타입 정보를 넣을 수 있는데다 애노테이션을 사용하지 말라고 이펙 자바 2에서 경고하기도 했다.

또 다른 문제는 수퍼 메소드나 수퍼 클래스, 인터페이스 등에 붙은 애노테이션 정보가 자신에게도 적용되냐 이거다. 당연히 애노테이션에는 상속 개념이 없으니 될지 안될지는 그걸 활용하는 프레임워크나 라이브러리 개발자가 어떤 식으로 애토네이션 정보를 찾아가느냐에 달렸다. 수퍼에 있으면 상속을 하는지, 서브에도 달렸으면 오버라드를 하는지, 결합해서 새로운 조합을 만드는지, 에러를 내는지 등등은 순전히 개발자 맘이다.

@Transactional이나 @RequestMapping등을 생각해보면 될 것이다. @Transactional에 대체(fallback) 룰이 적용되는 것이지 @RM에서 상속시 서브 메소드/타입의 애노테이션 정보가 수퍼 것을 오버라이드하는지 어쩌고의 여부는 토스3 같은 쓸데 없어 보이는 것까지 다 다루는 하드코어 스프링 책을 보지 않으면 알기 쉽지 않을 것이다.

애노테이션에는 일관성도 없고 사용방식도 멋대로기 때문에 상당히 불편하다. API Doc을 종일 들여다 보면서 개발해야 실수 하지 않고 겨우 개발이 가능한 위험한 라이브러리를 다루는 느낌이다.

그래도 타입 정보를 이용하기 보다는 관례와 대충대충을 사랑하는 개발자들의 입맛에 잘 맞게 때문에 계속 사용될 듯 하다. 자바 코드는 무늬만 놔두고 애노테이션만 왕창 넣어서 사용하는 프로그래밍 방법이 나올지도 모르겠다. 요즘 작성하는 하이버네이트 엔티티 클래스에선 애노테이션이 나머지 코드보다 양이 많다. Lombok으로 G/S까지 없앴으니 필드 뿐인데 거기에 관계 정보, 매핑 정보, 스키마 정보, 검증 정보 등의 코드와 XML등을 대체해준다는 각종 메타 정보가 듬뿍 달라 붙는다.

애노테이션은 이제 DSL로 사용되는 것 같다.  새로운 애노테이션 종류가 나오면 새로운 언어를 익히는 마음으로 문법과 용법(usage) 등을 익혀야 할 것이다.

내가 애노테이션 정보를 직접 가져와 사용할 필요가 있을 때는 스프링에 있는 AnnotationUtils의 findAnnotation() 메소드를 애용한다. 이 메소드는 @RequestMapping을 비롯한 여러 애노테이션을 다룰 때 사용된다. 메소드/타입 정보와 애노테이션을 파라미터로 받아서 메소드/타입에 해당 애노테이션이 있는지 찾아 있으면 그 정보를 돌려준다. 없으면 수퍼 타입/메소드에 애노테이션이 있는지 Object까지 올라가면서 몽땅 뒤진다.

그래서 이 findAnnotation()을 사용해서 애노테이션 정보를 다루는 기능은 수퍼 클래스/메소드에 동일한 타입의 애노테이션이 있는 경우 마치 오버라이딩한 것처럼 보인다. 수퍼에만 있으면 그걸 사용하는데 서브에도 또 있으면 먼저 발견하니까 수퍼는 그냥 무시하기 때문에 그렇게 보일 뿐이지 상속되는 거나 오버라이드 되는 건 아니다.

대충 스프링에는 이런 오버라이딩 비스무리한 관계가 있지만 모든 애노테이션에 대해서 다 그렇다고 볼 수는 없고, 애노테이션 정보를 메소드와 타입 레벨의 것을 조합되서 사용되기도 하고, 조합했을 때 OR로 하기도 하고 AND로 하기도 하고, 모든 가능한 조합을 다 만들기도 하고, 한쪽을 배제하고 다른 쪽의 정보를 우선하기도 한다. 꼼꼼하게 애노테이션을 이용하는 프레임워크가 아니면 잘못 사용해도 에러를 내지도 않는다. 아무튼 애물단지다.

오늘의 결론은 "애노테이션을 사용하도록 프레임워크나 라이브리를 만드는 개발자들은 애노테이션 사용 방법에 대해서 문서 좀 제대로 만들란 말이야!"

맨날 소스 뒤져가며 어떻게 사용해야 할지 찾기 정말 피곤해.

제목을 원래 "WebApplicationInitializer을 이용한 컨텍스트 초기화"라고 쓰려고 했다가 바꿨다. 제목 낚시의 달인 영회에게 배운대로 제목에서 친근하지 않은 단어는 빼기로.

서블릿 3.0을 사용하면 기존에 web.xml을 이용해서 서블릿 컨텍스트를 초기화하던 작업을 여러 파일로 쪼갤 수도 있고, 자바 코드를 이용할 수도 있다. 요즘엔 스펙 문서 읽는 게 너무 지루해서 기피하다보니 나도 서블릿 3.0 스펙을 꼼꼼하게 읽어본 것은 아니지만, 대충 봐도 그런 내용이 나온다.

자바코드와 web.xml 파일을 같이 사용할 수도 있고, 아예 web.xml 없이 자바코드만으로 초기화 작업을 할 수도 있다. 어짜피 web.xml은 ServletContext 오브젝트 초기화하는 데 사용되던 메타정보니까 직접 ServletContext를 다루면 되는 것이겠지.

이때 사용되는게 javax.servlet.ServletContainerInitializer인데 좀 특이하다. META-INF/services/javax.servlet.ServletContainerInitializer 파일(단순 텍스트 파일이다) 안에 구현 클래스를 지정해야 하고, 이게 또 jar 파일내에 있어야 하는 것 같다. web.xml이라는 간단한 표준 설정 파일을 없앴기 때문에 웹 앱 내의 모든 클래스, 심지어 jar 내의 클래스도 다 뒤져야 하는 부담이 생긴다. 그거 말고도 스캐닝할게 많은데. 그래서 스캐닝의 부담을 줄이기 위해서 javax.servlet.ServletContainerInitializer(이게 파일 이름이다)를 쓴 꼼수가 참 눈물난다. web.xml 같은 XML 없이도 개발이 가능하다고 큰 소리치긴 해야 겠는데, 그래도 ServletContainerInitializer 위치는 정해진데 명시해줘야 할 것 같고, 그렇다고 initializer.xml을 만들라고 하자니 애개 XML 또 들어갔네 할 것 같고. 그래서 파일 이름을 javax.servlet.ServletContainerInitializer으로 결정. 그래 XML 없다. 인정.

아무튼 ServletContainerInitializer는 jar 안에 META-INF/.. 과 함께 있을 경우에 동작하는데, 얘를 구현한 클래스에 @HandlesTypes를 사용하면 @HandlesTypes 안에 지정한 타입의 클래스를 모조리 찾아서 onStartup()의 파라미터로 넣어준다.

public interface ServletContainerInitializer {
    void onStartup(Set<Class<?>> classes, ServletContext ctx) throws ServletException;
}

이 방식은 jar로 패키징된 프레임워크가 애플리케이션내에 특정 타입으로 작성된 클래스를 사용해서 서블릿이나 필터, 리스너 형태의 프레임워크 모듈에서 사용하는 것을 가능하게 해준다. 예를 들어, 필터를 사용하는 보안 프레임워크를 만들었다고 하자. 여기에 사용자가 보안과 관련된 설정을 SecurityConfig이라는 인터페이스를 구현한 클래스를 만들어서 그 안에 configSecurity() 메소드 에서 구현하게 만들도록 한다. 그리고 보안 프레임워크 jar안에 ServletContainerInitializer를 구현한 초기화 클래스 SecurityInitializer를 만들고, META-INF에 등록하고, 그리고 @HandlesTypes(SecurityConfig.class)를 붙여주면, 서블릿 3.0 컨테이너는 SecurityConfig 타입의 클래스를 모조리 찾아서 SecurityInitializer의 onStartup()에 넘겨준다. 그러면 여기서 SecurityConfig 클래스로 작성된 보안관련 설정을 configSecurity() 메소드를 실행해가며 수집한 뒤에 이를 초기 설정으로 해서 보안관련 Filter를 셋업하고, 얘를 최종적으로 ServletContext에 필터로 등록해주면 된다.

보이는 것과 다르게 사용하기는 어렵지 않다.

XML 파일 같은 중앙집중형 설정방식이 사라졌을 때 어떻게 접근해야 할지에 대한 나쁘지 않은 아이디어 같다.

아무튼 스프링도 이걸 이용한다. spring-web.jar에 ServletContainerInitializer를 구현한 클래스가 있다. 당연히 META-INF/…도. @HandlesTypes은 WebApplicationInitializer다. 스프링의 ServletContainerInitializer 구현 클래스인 SpringServletContainerInitializer는 직접 서블릿 컨텍스트에 뭔가를 등록하지 않는다. 대신 그 작업을 WebApplicationInitializer로 다 위임한다. SpringServletContainerInitializer는 @Order 정보를 가지고 실행 순위 조종해주는 정도.

이제 WebApplicationInitializer를 만들어서 여기서 원하는 대로 앱 컨텍스트 만들고 리스너, 서블릿 등에 등록하고, 지지고 볶고 하면 된다. @HandlesTypes은 설정정보 수집 내지는 실제 등록될 서블릿이나 필터가 소모할 대상정도고, 서블릿 컨텍스트 초기화는 ServletContainerInitializer 구현 클래스내에서 일어나는 것이 원래 서블릿 3.0의 의도라고 생각되는데, 스프링은 이를 개발자가 작성하는 초기화 클래스에서 직접 컨텍스트 초기화(주로 스프링 컨텍스트 등록)에 사용하도록 확작한 셈이다. 스프링의 SpringServletContainerInitializer 문서를 보면, 그래서 주절주절 이에 대해서 설명이 많은데. 눈이 침침해서 다 읽기가 싫으네.

아무튼 어제 밤에 살펴본 SpringServletContainerInitializer와 WebApplicationInitializer에 대한 소감은 이정도로 마무리. 서블릿 3.0을 본격적으로 쓰기 시작하게 되면 그때 제대로 살펴봐야지.

web.xml 없이 WebApplicationInitializer에서 스프링 컨텍스트 초기화&등록 하는 것과 관련된 자세한 설명은…

© 2017 Toby's Epril Suffusion theme by Sayontan Sinha