개발관련/(과거)메모

다양한 스프링 활용기(지속적인 업데이트 - 수정:20.12.31)

동팡 2020. 12. 31. 00:09

목차

  • XML Spring 설정을 Java Config 변경
  • 설정 값을 빈으로 등록해서 사용하기 
  • 선택적인 빈주입
  • 데코레이터 프록시 페턴 예
  • Controller 존재 이유
  • Profile을 통한 프로젝트 내의 2개 프로젝트
  • 스프링 테스트
  • Async/스케줄링 통한 무결한 비동기 처리(나중에 작성)
  • Boot를 통한 배치 프로그램/CS 프로그램(나중에 작성)
  • Spring Cache 사용기(링크 제공, 나중에 작성)
  • Spring 삼대장 AOP 이론(링크 제공, 나중에 작성)
  • 간단 시리즈
    • 목록형 API 도메인 오브젝트 구조 잡기(간단간단!, 나중에 작성)
    • 메시지 컨버터 적극 활용(간단간단!, 나중에 작성)
    • 스프링 Bean과 Thread-Safe(간단간단, 링크제공)
    • 스프링 메시지 컨버터 Enum과 함께(링크 제공)

XML Spring 설정을 Java Config 변경

SpringMVC를 사용할 때 Spring을 위한 기본 설정이 필요하다. Spring을 개발해 본 사람의 경우, servlet-context.xml, application-context.xml, root-context.xml 등 다양한 이름의 xml 설정 파일을 봤을 것 이다. 또는 AppConfig.class, WebConfig.class와 같은 Java Config를 확인할 수 있을 것이다. 또는 Annotation을 통해 설정할 수 있다(Annotation의 경우, XML 설정 방식, Java 설정 방식 두 방식 다 사용하기 때문에 분류를 하지 않았다.). 

 

결론을 얘기하면 Java Config를 추천한다. 

 

Java Config 방식은 아마... Spring 3.1 부터 지원하기 시작하였다. 우리는 이미 3.1에서부터 Java Config를 사용할 수 있었다. 필자의 경우, Spring을 XML 설정 방식으로 처음 접했다. 물론 Spring 버전은 3.1.x RELEASE 이었다(충분히 사용할 수 있었다.). 추 후, Java Config의 매력에 빠져버려 필자 프로젝트의 Spring 설정을 XML-> JavaConfig로 전부 다 바꿔버렸다. 

 

일단 XML vs Java Config 

답은 없고 취향 차이이다. 

필자의 경우, 추천하는 이유는 다음과 같다.

  • 확장성
    • Bean의 등록/가공/데코레이팅을 유연하게 할 수 있으며 약간의 수정으로 많은 확장성을 확보할 수 있다.
  • 직관적(가독성 좋음)
    • 코드를 보면 어떤 빈을 어떻게 등록하고 싶은지 바로 알 수 있다. 
    • XML으로 했을 경우, 스프링의 DTD 또는 XML 스키마의 이해가 필요하다. 이해가 필요하다는 것은 직관을 해할 수 있다.
  • 구현 난이도
    • Spring 사용자 = Java 언어 사용자, 최소 Java 기본은 알고 있는 사용자이다. 
    • Spring에서의 @Configuration, @Bean의 어노테이션 의미와 객체를 인스턴스화 할 수 있으면 바로 가능하다.
    • XML으로 했을 경우, 스프링 XML 스키마에 대해 공부가 필요하다.

당장의 아래 코드를 XML로 해보자. 난 잘 모르겠다.

@Bean
public IUserService userService( IUserDao usreDao, IKeyDao keyDao, BussUserService bussUserService, BusinessServerInfoService businessServerInfoService, PlatformCommonHandler platformCommonHandler, ObjectMapper objectMapper ) {
	if ( wasPropertiesBean.getAuthProjectMode() == EnumAuthProjectMode.MOA ) 
		return new MoaUserService( new DefaultUserService( usreDao, keyDao ), usreDao, bussUserService, businessServerInfoService, platformCommonHandler );
	else
		return new DefaultUserService( usreDao, keyDao );
}

추상체 IUserService 빈을 등록한다.

구현체는 설정 값에 따라 달라진다. 

그리고 달라지는 구현체는 데코레이터 페턴을 적용한다. 

위의 사항은 Java 개발자는 다 알고 있는 사항이다. XML 어떻게 구현해야하는지 막막하다...(내가 XML 설정에 능숙하지 못한 부분도 있다(분명 방법은 있다.))

 

설정 값을 빈으로 등록해서 사용하기 

SpringMVC에서 설정 값을 Java primitive type 또는 객체로 변형하여 바로 사용하는데 문제가 있었다. 

Spring의 PropertySource를 통해[@PropertySource("classpath:conf/wasSetting.properties")] properties의 값들을 갖고와도 전부 String으로 갖고있다. 또는 java.util.Properties를 통해 갖고 온다. 이 때 문제점은 완전히 Java primitibve type 또는 객체로 가공되지 않은 설정 값이다.

 

설정 값을 application 실제 로직에서 사용할 때 다음과 같은 문제점이 발생할 수 있다. 예는 아래와 같다.

 

  • HexString/Base64는 byte array 변형이 필요 
  • boolean은 == 연산이 아닌 equals 연산
  • 위와 비슷한 맥락으로 무수한 문제 발생 

그래서 WAS 초기화 작업을 할 때 설정 값에 대해 다음과 같이 진행한다.

  • primitive type 또는 객체 변경 작업을 진행한다. 
    • On/Off 설정은 boolean으로 변경
    • HexString/Base64는 byte[]으로 변경
    • 특정 범위 내에 String들은 Enum으로 변경
    • SecretKey, KeyStore, X509Certificate 같이 설정 값으로 객체화가 필요할 때는 객체 인스턴스화 진행
    • 숫자 값은 int 타입으로 변경
  • 그리고 해당 "상태"를 갖고 있는, 즉 설정을 갖고 있는 객체 생성 및 Bean을 등록
  • 이 때 주의할 사항은 원래 Bean의 경우, 절대 상태를 갖고 있으면 안된다(상태는 클래스 맴버 변수를 의미한다.).
  • 여러 쓰레드가 동시에 사용하는 Bean이기 때문에 절대 상태를 갖고있으면 안된다.
  • 그러나, 지금의 경우는 final 처럼 사용하는 "설정 값" 이며, getter만 제공해주기 때문에 괜찮다.
  • 아래와 같이 Bean을 생성해준다.
	@Bean
	public WasPropertiesBean wasPropertiesBean( /*Autowired*/ Environment env, ResourceLoader resourceLoader, ObjectMapper objectMapper ) throws CertificateException, IOException {
		return new WasPropertiesBean( env, resourceLoader, objectMapper );
	}

위와 같이 Bean을 등록하면 1) Spring-Context 내부에서는 어디든지 설정 값을 쉽게 부를 수 있으며 2) 이미 가공(타입변환)한 설정 값이기 때문에 바로 사용이 가능하다. 

 

위의 WasPropertiesBean의 생성자는 설정값을 가공하여 Runtime에서 바로 사용할 수 있도록 가공작업을 진행한다.

생성자의 작업을 @Value 어노테이션으로 충분히 해소할 수 있다. @Value의 경우 막강한 SpEL을 제공하기 때문에 다양하게 활용 가능하다. 심지어 객체 인스턴스화 까지 가능하다. (혹시 몰라 검색해보니 있네..) 그래도 설정값을 객체화 하는 것은 단순하게 EL로 해결하기 쉽지는 않다. 그래서 위의 방식을 좀 더 권고 하고 싶다(자기합리화).

 

선택적인 빈주입 / 데코레이터 프록시 페턴 예

뭔가 겹치는 부분이 상당해서 합쳐버렸다.

 

[선택적인 빈주입]

 

Spring의 최강 장점 선택적인 빈 주입. 필자는 이에 맞는 정확한 용어가 없어서 매번 "선택적인 빈주입을 통해 확장성을 재고한다."라 표현한다.  

선택적인 빈 주입을 설명하면 다음과 같다.

  • 소스단에서는 추상체를 반환한다(인터페이스). 
  • 어플리케이션 Runtime에서(초기화 할 때) 사용하고자 하는 구현체를 선택한다(선택적인 빈 주입).

[데코레이터 프록시 페턴]

 

Spring의 세가지 특장점 중 1개는 감싸기. AOP이다. Spring이 제공해주는 AOP를 사용하지 않고 우리가 Spring Component를 이용하여 쉽게 만들 수 있다. 빛을 바라는 용례는 다음과 같았다.

  • Scheduler 기능 개발 -> 납품 
  • 그런데, Scheduler에 감사로깅 작업이 필요한 것을 6개월 지나고 알았다.
  • 포인트는 다음과 같다.
  • 1)이미 위의 기능은 납품이 되었고 해당 로직은 수정하기 죽어도 싫었다. 2) 하위 호환을 무시할 수 없었다(감사로깅을 하기 위해서는 DB 테이블이 있어야하는데 없는 사이트가 존재). 3) 스케줄러 감사로깅은 공통 로직이다. 
  • 위의 사항은 다음과 같이 해결이 가능하다.
@Bean
public ISchedulerOperation delLogOperation( SchedulerDao dao, SchedulerAuditLogService schedulerAuditLogService) {
	if( wasPropertiesBean.isSchedulerAuditMode() ) {
		return new AuditSchedulerOperation( new DelLogSchedulerOperation( dao ), schedulerAuditLogService );
	} else {
		return new DelLogSchedulerOperation( dao );
	}
}
			
@Bean
public ISchedulerOperation tokenExpDtOperation( SchedulerDao dao, SchedulerAuditLogService schedulerAuditLogService) {
	if( wasPropertiesBean.isSchedulerAuditMode() ) {
		return new AuditSchedulerOperation( new TokenExpDtSchedulerOperation( dao ), schedulerAuditLogService );
	} else {
		return new TokenExpDtSchedulerOperation( dao );
	}
}

@Bean
public ISchedulerOperation tokenValidOperation( SchedulerDao dao, SchedulerAuditLogService schedulerAuditLogService) {
	if( wasPropertiesBean.isSchedulerAuditMode() ) {
		return new AuditSchedulerOperation( new TokenValidSchedulerOperation( dao ), schedulerAuditLogService );
	} else {
		return new TokenValidSchedulerOperation( dao );
	}
}

위의 코드는 선택적인 빈주입과 빈의 데코레이팅을 조합한 빈 주입이라 할 수 있다. 

위와 같은 빈주입으로 다음의 문제점을 해결하였다.

  • 1)이미 위의 기능은 납품이 되었고 해당 로직은 수정하기 죽어도 싫었다.
    • 데코레이팅으로 기존 로직은 보존 하고 추가 로직은 감싸았다(AOP와 같은 맥락)
  • 2) 하위 호환을 무시할 수 없었다(감사로깅을 하기 위해서는 DB 테이블이 있어야하는데 없는 사이트가 존재).
    • 설정 값에 의해 스케줄링의 로깅 유무를 선택하였다(선택적인 빈 주입).
  • 3) 스케줄러 감사로깅은 공통 로직이다. 
    • AuditSchedulerOperation을 통해 코드 중복 없이 해소하였다. 

Controller 존재 이유

결론을 얘기하면 서비스 영역에 절대 HttpServletRequest(Servlet), WebRequest(Spring_Servlet), WebHeader(Spring_Servlet), HttpSession(Servlet) 등과 같은 Servlet 기술 스택에 있는 객체를 절대 넘기지 말자.

 

스프링은 작은 단위부터 큰 단위 까지 엄청난 모듈화를 지향하며 Spring은 계층형 아키텍쳐를 지향한다. 대표적으로 스프링의 계층형 아키텍쳐는 다음과 같다.

  • 프레젠테이션 계층: 웹 계층, UI 계층, MVC 계층
  • 서비스 계층: 매니저 계층, 비지니스 로직 계층
  • 데이터 엑세스 계층: DAO 계층, EIS 계층

서비스 계층, 실제 비지니스 로직 계층은 POJO(Plain Old Java Object)로 작성되어 있어야한다.

먼저 POJO에 대해 이해가 필요하다. 

POJO는 쉽게 얘기하면 Pure Java로 생성된 Java Object를 의미한다. 어렵게 서술하면 다음과 같다.

POJO는 객체지향 원리에 충실하며, 환경과 기술에 종속되지 않고 어플리케이션의 핵심기능을 담아 설계하는 방식의 프로그래밍법이다. 프레임워크 환경에서 POJO가 될 수 있는 조건 자체가 장점이다. 

Servlet 기술 스텍에 있는 객체를 서비스 계층에 전달하면 안되는 이유는 계층 간에 침범이 있기 때문이다. 계층 간의 침범은 코드의 결합도를 높이고 응집도를 낮춘다. 결국에 해당 객체는 재사용을 하기 어려운 상황에 도래하게 된다. 재사용하기 어려운 코드는 확장성과 유연성을 저해한다. 즉, 각 계층은 자신의 계층의 책임에만 충실해야한다.

 

그래서 계층 간의 경계를 넘길 때 반드시 특정 계층에 종속되지 않는 오브젝트 형태로 변환해줘야 한다. 즉 Pure Java 객체를 넘기는 것이 제일 바람직하다. 

 

그러면 Controller에서는 어떤 것을 하는 것인가? 대략적으로 Servlet 기술 스텍에서 비지니스 핵심 로직으로 가기전 까지와 그 후의 작업을 해줘야한다. 예는 다음과 같다.

  • ServletRequest에서만 해줄 수 있는 작업
  • Spring Message Converting과 그것의 후처리
  • Http Session 핸들링
  • 기타 Http Request/Response 핸들링

또한, 계층간의 전송은 DTO(Data Transtfer Object)를 통해 전송하며 해당 사항을 도메인 오브젝라 칭하기도 한다. DTO에 단순 getter/setter 메소드만 구현하지말고(단순 저장 X) 도메인 오브젝트를 가공할 수 있는 메소드까지 지원하자(매번 도메인 오브젝틀 가공할 때 서비스 객체(Bean)을 주입할 수 없는 노릇이니...) 해당 내용은 토비 스프링 1권 807p-풍성한 도메인 오브젝트 방식에서 자세하게 확인할 수 있다. 

 

위의 내용은 토비 스프링 1권-9장-782p 부터 간략하게 정리한 것이다.

(그리고 예전에는 Spring에서 Servlet WebApplicationContext, Root WebApplicationContext 분리한 것에 대해 이해하지 못 했다. 그러나, 계층형 아키텍쳐 개념을 보고, 이 가치관을 지향(도모)하기 위해 프레젠테이션 계층의 Context와 서비스 계층의 Context를 분리한 것이 아닐까? 생각이 든다.

 

많은 개발자들이 계층간의 침범을 범한다. 이 침범은 Spring이 지향하는 "비침투 기술, 비지니스 로직에만 집중" 철학을 해할 우려가 있다!

 

이론 말고 실제로 프랜잰테이션 기술 스텍에 있는 객체를 서비스 계층에 침범시키면 어떤일이 생길 수 있을까?

  • 해당 사항은 Spring Boot에서 확실하게 보여진다.
  • Spring Boot는 Servlet, Netty, Jetty,(non-servlet),reactive 등 다양하게 활용 가능하다.
  • 이 때 위의 특정 기술 스텍(예: Servlet)에 있는 객체를 사용했고,
  • 이 객체를 서비스 계층에 넘겼고,
  • 프랜잰테이션 계층에서 기술 스텍을 변경했을 때(Servlet을 사용하지 않고 Netty를 사용했을 때)
  • 해당 서비스 계층도 수정해야 하는 불상사가 생긴다(이 현상을 결합도가 높다라 칭한다).
  • 당연히... Netty에서 HttpServletRequest를 불러올 수도 사용할 수도 없는 노릇이니까...

Profile을 통한 프로젝트 내의 2개 프로젝트

솔직히 Spring의 Profile 용도를 dev/test/prod(개발/테스트/제품)으로 나눈 것으로만 인지했었다. 

그러나, 일을 진행하며 아래와 같은 상황이 생겼고, 이 때 Profile이 빛을 바랬다(솔직히 이렇게 쓰는게 맞는지는 모르겠지만 아래의 상황에서는 많은 도움이 됐다.).

  • 현재 제품의 A 제품에 특화되어 있는 인증서버 B가 존재한다. 
  • SSO 기능이 요구되는 프로젝트가 생겼다.
  • 기존 자사 SSO 제품 C는 덩치가 너무 커서 현재 프로젝트에 부합하지 않다.
  • Semi-SSO D를 개발하기로 했지만,
  • A 제품에 특화되어 있는 인증서버 B를 개량하기로 했다.
  • 그러나, 새로 프로젝트를 생성하면 안되며 인증서버 B 프로젝트 내부에서 진행해야한다.
  • 인증서버 B 프로젝트는 2개의 별개 다른 기능(인증서버 B 로직, Semi-SSO D 로직)을 담아야한다.
  • Semi-SSO D의 기능은 인증서버 B와 로직적으로 공통된 부분이 꽤 존재한다.
  • 즉, Semi-SSO D와 인증서버 B의 교집합은 50%정도였다. 
  • 또한 Semi-SSO를 위한 웹 관리 도구도 개발이 필요했다. 

솔직히 위와 같은 "추가 기능" 서비스 계층의 로직은 선택적인 빈주입과 데코레이팅을 통해 충분히 기능 확장을 할 수 있다. 

 

그런데 다음과 같은 문제점이 있다.

  • 인증서버 B 제품은 Semi-SSO D 제품의 기능을 포함하지만
  • 인증서버 B만을 위한 제품 납품을 했을 때 Semi-SSO D 기능을 사용하면 안된다(역 또한 같다).

위는 표면적인 관점이고, 개발적인 관점은 다음과 같다.

  • Controller는 선택적인 빈주입을 어떻게해야하는지...?
  • Interceptor/Filter/AOP들도 선택적으로 적용 해야하며
  • 정적 리소스 접근 제한이 필요하며(인증서버 B는 Semi-SSO D의 정적 리소스에 접근하면 안되니..)
  • 기존 인증서버 B는 인터페이스 변경 없이 무결하게 돌아가야하며..

선택적인 빈 주입과 데코레이팅을 통해(Interceptor/Filter/AOP/서비스 계층 빈/데이터 엑세스 빈) 로직의 기능 확장은 무리 없이 진행하였다. 

WebConfig(Servlet-Context 설정) 설정 또한 분기문(인증서버 B or Semi-SSO D)을 태워 다르게 설정해줬다. 

 

그런데 프레젠테이션 계층의 선택적인 빈주입과 공통사항을 추출하는게 만만치 않았다. 

그리고 같은 @RequestMapping을 같은 것을 어떻게 해소하는지 답이 안나왔었다. 

 

그냥 아래와 같이 Profile을 통해 해봤는데 잘 되더라...

@RestController
@RequestMapping("/authentication")
@Profile("kms")
public class AuthContorller {
	// 중략
}

@RestController
@RequestMapping("/authentication")
@Profile("moa")
public class MoaAuthContorller {
	// 중략
}

그리고, Profile의 Active가 필요했다. Profile Active는 web.xml 또는 JVM 옵션을 통해 가능하지만... 

필자 입장은 설정 값의 관리 포인트가 늘어나는 것은 지양한다.

Spring Doc를 확인하니 programmatically하게 설정이 가능했다. 아래와 같다.

@Component
public class StartupAppListener implements ApplicationListener<ContextRefreshedEvent>  {

	private final ConfigurableEnvironment env;
	private final WasPropertiesBean propertiesBean;
	
	@Autowired
	public StartupAppListener( ConfigurableEnvironment env, WasPropertiesBean propertiesBean ) {
		this.env = env;
		this.propertiesBean = propertiesBean;
	}
	
	@Override
	public void onApplicationEvent(ContextRefreshedEvent event) {
		setProfiles();
		
        // 중략
        
		if ( propertiesBean.getAuthProjectMode() == EnumAuthProjectMode.MOA )  {
			// certain init
		} else {
			// certain init
		}
		
		// 중략
	}
	
	// 중략
    
	private void setProfiles() {
		if ( propertiesBean.getAuthProjectMode() == EnumAuthProjectMode.MOA ) {
			env.setActiveProfiles( "moa" );
		} else {
			env.setActiveProfiles( "kms" );
		}
	}	
    
    // 중략
}

Profile을 통해 해소하였다. 해답은 어찌 됐지만 정답인지는 모르겠다. 

 

스프링 테스트

SpringMVC를 사용한다는 것은 WAS를 사용한다는 것이다.

스프링 테스트를 사용하는 이유

  • 테스트를 위해 말도 안되는 반복적인 WAS Startup/Stop은 많은 시간 소요
  • 개선 및 추가 개발, 유지보수 건을 진행할 때 많은 시간 소요
  • 이클립스 사용자는 필 사용(툴이 너무!!!! 너무!!!!!! 느림)

위와 같은 상황에는 SpringJUnit을 통해 완파할 수 있다. 

처음에 테스트 케이스 잡고 작성하는게 귀찮고 시간이 오래갈지라도 "프로젝트 유지보수" 기간에는 확실하게 빛을 바란다. 

 

필자는 Spring MockMvc를 통해 테스트를 진행한다.

 

일반 HTTP API 테스트 코드는 아래와 같다.

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes= {WebConfig.class, AppConfig.class})
@Transactional
@ActiveProfiles("kms") // profile == kms : change extended AbstractTestCase
public class BusinessServInfoTest extends AbstractTestCase {
	
	@Test
	public void run() throws Exception {
		createBussServerInfo();
	}
	
	private int createBussServerInfo() throws Exception {
		// GIVEN
		String jsonData = getObjectMapper().writeValueAsString( getGivenDatas() );
		
		// WHEN THEN		
		MvcResult ret = newInstanceMocMvc( getContext() ).perform( post( AbstractTestCase.BUSINESS_SERVER_INFO_URI )
				.header( HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE )
				.header( HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE )
                		.header( HttpHeaders.CONTENT_ENCODING, "UTF-8" )
                		.header( HttpHeaders.AUTHORIZATION, getToken() )
                		.header( AbstractTestCase.X_KMS_SIG_MATERIAL, getTestAdminSigMat() )
                		.header( AbstractTestCase.X_KMS_SIGNATURE, getTestAdminSig() )
                		.content( jsonData )
		)
		.andDo( print() )
		.andExpect( status().isOk() )
		.andReturn();

		JSONArray jarr = new JSONArray( ret.getResponse().getContentAsString() );
		return jarr.getJSONObject( 0 ).getInt( "seqId" );
	}

	private List<BusinessServerInfo> getGivenDatas() {
		// GIVEN
		List<BusinessServerInfo> voList = new ArrayList<BusinessServerInfo>();
		BusinessServerInfo vo = new BusinessServerInfo();
		Map<String, String> testExtendedData = new HashMap<String, String>();
		
		testExtendedData.put( "test", "test" );
		
		// 중략 ..
        	// vo setter 작업..
        	// ..
        
		voList.add( vo );
		
		return voList;
	}
}

..

public abstract class AbstractTestCase {
	
    // 중략..
    
    	@Autowired
	private WebApplicationContext context;    
    
	@Before
	public void setUp() throws Exception {
        objectMapper = new ObjectMapper();

        objectMapper.setSerializationInclusion( JsonInclude.Include.NON_NULL );
        objectMapper.setDateFormat( new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss") );
        
        createTestAdminSignature();
        createTestKmsSignature();
        
        this.token = getToken( id, pw, authState );
    }
    // 중략..
    public MockMvc newInstanceMocMvc( WebApplicationContext ctx ) {
        return MockMvcBuilders.webAppContextSetup( ctx ).build();
    }
}
  • 추상 클래스를 통해 공통사항은 super로 분리
  • Transactional 어노테이션을 통해 테스트가 완료되면 DB Rollback을 한다.
  • ContextConfiguration 어노테이션을 통해 설정 값의 위치를 표현한다.
  • WebAppConfiguration 어노테이션을 통해 ApplicationContext를 사용하겠다는 의사 표시를 한다.
  • 그리고, ApplicationContext를 통해 MockMvc를 생성
  • 위의 테스트는 여러 HTTP header와 Body를 삽입하고 200 리턴을 받으면 성공하는 것이다.
  • getGivenDatas()를 수정하여 상황에 인자 값을 대응할 수 있다.

일반 세션이 요하는 HTTP API는 아래와 같다.

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes= {WebConfig.class, AppConfig.class})
@Transactional
@ActiveProfiles("moa") // profile == kms : change extended AbstractTestCase
public class BusinessServInfoTest extends AbstractTestCase {

	@Test
	public void run() throws Exception {
		int seqId = createMoaBussServerInfo();
		getBussServerInfoOne( seqId );
	}

	// 중략 ...
	private int createMoaBussServerInfo() throws Exception {
		// GIVEN
		String jsonData = getObjectMapper().writeValueAsString( getGivenDatas() );

		// WHEN THEN
		MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post( AbstractTestCase.BUSINESS_SERVER_INFO_URI )
				.session( getSession() )
				.header( HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE )
				.header( HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE )
				.header( HttpHeaders.CONTENT_ENCODING, "UTF-8" )
				.content( jsonData )
				.with( csrf() );

		MvcResult ret = newInstanceMocMvc( getContext() ).perform( builder )
				.andDo( print() )
				.andExpect( status().isOk() )
				.andReturn();

		JSONArray jarr = new JSONArray( ret.getResponse().getContentAsString() );
		return jarr.getJSONObject( 0 ).getInt( "seqId" );
	}
}

..

public abstract class AbstractTestCase {
	@Autowired
	private WebApplicationContext context;    
    
    @Autowired
    private Filter springSecurityFilterChain;
    
	@Before
    public void setUp() throws Exception {
   		setTestTokenValue( "579260d73e1ea7e5360748065e33025d22c6eb7c" );
    	session = new MockHttpSession();
        objectMapper = new ObjectMapper();

        objectMapper.setSerializationInclusion( JsonInclude.Include.NON_NULL );
        objectMapper.setDateFormat( new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss") );
    }
    
	public MockHttpSession getSession() {
		TokenVo tokenVo = new TokenVo();
		tokenVo.setUserId( id );

		session.setAttribute( CommonConstants.SSO_ASK_VAL, testTokenValue );
		session.setAttribute( "tokenVo", tokenVo );
		session.setAttribute( "sessionDatas", "{\"id\":\"test-auth-moa\", \"role\":\"admin\"}" );
        
		return session;
    }
}
  • 대략적인 공통설명은 위와 같다. 추가된 상항은 다음과 같다.
  • Session이 요구되기 때문에 MockSession을 생성한다.
  • 세션의 필요한 값은 임의로 지정한다.
  • 또한 Spring Security CSRF가 되어있기 때문에 설정을 추가해준다(정확하게 원리는 모르겠지만 더미 CSRF 토큰생성 인것 같기는 하는데 불확실하다.).

테스트 툴을 어떤 것을 활용해도 상관없지만, 중요하게 생각하는 사항은 다음과 같다.

  • WAS Startup/Stop 없이 테스트
  • Spring Context 내에서의 자유로운 테스트
  • 응집도가 높은 테스트 케이스 산출