메모장

블로그 이미지

동팡

https://github.com/ehdvudee

'Spring'에 해당되는 글 3건

제목 날짜
  • Servlet/Spring에서 web.xml은 어떻게 사라졌을까? 2021.04.28
  • 안전한 비동기 처리 전략(Feat. Spring) 2021.04.02
  • 다양한 스프링 활용기(지속적인 업데이트 - 수정:20.12.31) 2020.12.31

Servlet/Spring에서 web.xml은 어떻게 사라졌을까?

개발관련/삽질 2021. 4. 28. 02:09

목차

  • Servlet에서의 web.xml 대체
  • Spring-Legacy에서의 web.xml 대체
  • Spring-Boot에서의 web.xml 대체
  • 결론

Servlet에서의 web.xml 대체

Java6(Servlet 3.0 API)에서 XML 설정을 Java Config와 Annoation으로 대체하고자 하는 시도가 보인다. 

Servlet 3.0에서는 @WebServlet, @WebFilter, @WebListener를 통해 설정의 간소화를 도모하고 있다. 그러나 해당 어노테이션은 한계가 분명하게 보인다.

 

예를 들어, filter의 ordering은 어떻게 할 것인가?

session timeout은 어떻게 처리할 것인가?

security-role은 어떻게 처리할 것인가?

welcome-file-list는 어떻게 할 것인가?

 

annoation을 통해 해소하는데 한계가 있다. 

위와 같은 문제점을 해결하기 위해 ServletContainerInitializer 인터페이스의 ServletContext를 통해 web.xml 설정들을 대체할 수 있다. 쉽게 얘기해서 Spring의 xml 설정을 Java Config로 대체하는 것과 같은 맥락이다. 

 

ServletContainerInitilizer#onStartup은 서블릿 컨테이너에 의해 호출되어야 한다. 그러면 서블릿 컨테이너는 해당 메소드를 호출할 수 있을까? 그러니까 Tomcat은 ServletContainerInitilizer를 어떻게 호출할 수 있을까? ServletContainerInitilizer는 Service Provider Interface(SPI) 개념을 근간한다. 

SPI(Service Provider Interface)는 플러그인/모듈 등의 기능을 확장할 때 사용하는 인터페이스이다. 예는 다음과 같다.
Framework "F"는 "A"라는 API를 제공한다. Framework "F"는 여러 모듈 또는 플러그인이 존재한다. "A" API의 결과는 플러그인 또는 모듈에 따라 결과 값이 다르다. Framework "F"는 해당 플러그인 또는 모듈을 상황에 따라 선택하여 배포할 수 있다. 

SPI를 쓰기위해서는 META-INF/services에 정확한 패키지명과 클래스명을 다음과 같이 기재하며 호출할 수 있다. 

SP 지정: META-INF/services/com.service.MyServiceImpl
SP 호출: ServiceLoader<MyService> services = ServiceLoader.load(MyService.class);

 

ServletContainerInitilizer인터페이스 및 사용방법은 아래와 같다.

package javax.servlet;

...

public interface ServletContainerInitializer {

    public void onStartup(Set<Class<?>> c, ServletContext ctx)
        throws ServletException;
}
@HandlesTypes({MyType.class})
  public class AppInitializer implements ServletContainerInitializer{
..
} 

Set<Class<?>>객체를 통해  javax.servlet.annotation.HandlesTypes에 지정한 클래스 객체를 갖고 올 수 있다. 만약에 해당 어노테이션에 인터페이스를 지정하면 해당 인터페이스를 구현한 모든 클래스를 조회하여 Set에 보관한다. 해당 클래스 객체들은 newInstance()를 통해 활용할 수 있다. 

위와 같은 ServletContainerInitlizer를 통해 프레임워크에 IoC 패턴을 삽입할 수 있다.

Spring-Legacy에서의 web.xml 대체

Spring에서의 web.xml은 WebApplicationInitializer를 통해 해결할 수 있다. 사용자는 WebApplicationInitializer을 구현하여 web.xml을 사용할 수 있다. 또는 AbstractAnnotationConfigDispatcherServletInitializer 추상 클래스를 사용한다. 단순하게 WebApplicationInitializer를 확장하여 사용할 경우, 모든 설정을 일일 설정해야 한다. 그러나, 스프링에서 제공하는 기본 구현 셋(AbstractAnnotationConfigDispatcherServletInitializer) 사용을 통해 간소화할 수 있다. 해당 추상 클래스를 을 통해 Servlet 계층의 스프링 설정, Servivce 계층의 스프링 설정, DispatcherServlet의 경로 지정, 필터 등록을 간결하게 할 수 있다.  

( WebApplicationInitializer, AbstractAnnotationConfigDispatcherServletInitializer의 사용 방법은 별도의 구글링을 통해 쉽게 확인할 수 있다.)

 

여기서 중요한 포인트는 SpringServletContainerInitilizer 클래스이다. 해당 클래스는 javax.servlet.ServletContainerInitializer 인터페이스를 구현한 클래스이다. 코드는 아래와 같다. 

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

	@Override
	public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {

		List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();

		if (webAppInitializerClasses != null) {
			for (Class<?> waiClass : webAppInitializerClasses) {
				// Be defensive: Some servlet containers provide us with invalid classes,
				// no matter what @HandlesTypes says...
				if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
						WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
					try {
						initializers.add((WebApplicationInitializer) waiClass.newInstance());
					}
					catch (Throwable ex) {
						throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
					}
				}
			}
		}

		if (initializers.isEmpty()) {
			servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
			return;
		}

		servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
		AnnotationAwareOrderComparator.sort(initializers);
		for (WebApplicationInitializer initializer : initializers) {
			initializer.onStartup(servletContext);
		}
	}
}
  • HandlesTypes어노테이션에 WebApplicationInitializer 클래스를 지정하였다. 
  • Set에 있는 WebApplicationInitilizer을 구현한 모든 클래스에 대해 newInstance()를 하여 객체를 생성한다.
  • 생성한 객체에 onStartup 메소드를 실행한다. 

SpringServletContainerInitilizer 클래스에서 위와 같은 작업을 해주기 때문에 우리는 Spring에서 AbstractAnnotationConfigDispatcherServletInitializer 또는 WebApplicationInitializer를 구현해서 Servlet Container설정을 간결하게 할 수 있다.

Spring-Boot 에서의 web.xml 대체

Spring-Boot에서는 설정들이 아래와 같이 퍼졌다.

  • DispatcherServletAutoConfiguration을 통해 DispatcherServlet을 자동으로 설정한다.
  • application.yaml을 통한 DispatcherServlet 경로 설정(default는 "/"이다.)
  • FilterRegistrationBean, ServletListenerRegistrationBean을 통해 Servlet Filter와 Listener를 설정한다. 
  • welcome-file-list의 경우, Spring Container 설정으로 해소할 수 있다(ViewControllerRegister).
  • error의 경우, Spring-Boot만의 error 핸들링 스트럭쳐가 존재한다.

Servlet Listener의 경우, Spring ApplicationListener<ContextRefreshedEvent>으로 변경해도 무방할 경우, 해당 클래스로 마이그레이션 하는 것을 권고한다. 어플리케이션 초기화 설정은 Spring Bean을 사용하는 경우 다반사이기 때문이다. 즉 Spring Container 내에서의 초기화 설정을 진행하는 것이 편리하다. 

 

정리하면 Spring-Boot에서의 Servlet Container 설정은 이곳 저곳 퍼진 감이 있다. 그래도 Spring-Legacy와 비교하면 더 간편해졌다고 봐도 무방하다. 

 

결론

본문을 통해 web.xml이 어떻게 사라졌는지 확인할 수 있다. 설정 방법을 보러온 독자의 경우 많은 실망을 했을 수 있다. 다른 많은 블로그에서 다루니까 잘 찾아보길 바란다. 

  • Servlet에서는 web.xml은 SPI 기술을 통해 xml을 없앨 수 있었고,
  • Spring-Legacy에서는 HandlesTypes 어노테이션에 WebApplicationInitilizer을 지정하여 효율적으로 사용할 수 있었고,
  • Spring-Boot에서는 Auto-Configuration, Spring Container 대체, 빈 설정 등으로 간편하게 사용할 수 있었다.

여기서도 느낄 수 있지만 Spring(Boot)의 이해에는 먼저 Servlet의 이해가 정말 필요하다. Servlet 책 1권 정도는 보면 좋을 것 같다(엄진영의 Java 웹 개발 워크북을 추천한다. 시대별 Servlet 프로그래밍을 맛볼 수 있다.).

참고문헌

www.logicbig.com/tutorials/core-java-tutorial/java-se-api/service-loader.html

docs.oracle.com/javaee/6/api/javax/servlet/annotation/HandlesTypes.html

www.logicbig.com/tutorials/java-ee-tutorial/java-servlet/servlet-container-initializer-example.html

offbyone.tistory.com/215

www.baeldung.com/spring-xml-vs-java-config

www.baeldung.com/spring-boot-dispatcherservlet-web-xml

www.baeldung.com/java-web-app-without-web-xml

docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/SpringServletContainerInitializer.html

docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/WebApplicationInitializer.html

docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/support/AbstractAnnotationConfigDispatcherServletInitializer.html

 

저작자표시 비영리 (새창열림)

'개발관련 > 삽질' 카테고리의 다른 글

hashicorp사의 Vault(볼트) - 개요 - 1  (0) 2021.06.29
Spring-Legacy to Spring-Boot 마이그레이션(Feat. 외장/내장-Tomcat/JSP/Multiple-Datasource(With-JNDI)/External-Lib)  (0) 2021.04.28
Apache Commons-DBCP / Hikari-DBCP 정리  (0) 2021.04.05
안전한 비동기 처리 전략(Feat. Spring)  (0) 2021.04.02
Oracle/Tibero CURRVAL 사용 주의사항  (0) 2021.03.29
Posted by 동팡

안전한 비동기 처리 전략(Feat. Spring)

개발관련/삽질 2021. 4. 2. 17:41

목차

1부. 안전한 비동기 처리 설계 경험

  • 개요 
  • 비동기 
  • 저널링
  • 비동기 설계 

2부. Spring을 통한 비동기 처리 개발

  • 개요
  • ThreadPoolTaskExecutor
  • ThreadPoolTaskScheduler

1부. 안전한 비동기 처리 설계 경험

개요

TiberoDBMS를 사용하는 상태, 이 DBMS는 너무 느리다. DBA 또는 DB 엔지니어가 설정을 어떻게 했는지 모르는데 진짜 느리다. 아무리 DBMS 동접자가 많다한들 이런식으로 느린거는 진짜 선을 넘은 것이다. 

각설하고.. 고객님께서 삭제 API가 느리다고 콜이 왔다. 이래저래 DB 쪽에 문제가 많았는데.. 많은 과정을 해결하고 비동기 처리 적용을 하는 방향이 나왔음. 

 

CRU(Create, Read, Update) 부분의 경우, 해당 작업 후, 사용의 여지가 있기 때문에 비동기 처리하기에 적합하지 않다. Delete의 경우, 비동기 처리를 해도 무방하였다. 그러면 비동기를 처리해보자.

비동기

비동기를 잘 설명할 수 있는 생활의 예는 우리는 커피 주문 후 주문대에서 계속 기다리지 않고 벨을 받고 다른 곳에서 핸드폰을 하거나 일행과 대화를 하며 "다른 행위"를 한다. 이게 바로 비동기닼ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

 

동기의 경우, 커피 주문 후 주문대에서 아무것도 안하고 커피만 나올 때 까지 기다리는 것이다. 

 

필자의 몇몇 프로젝트는 비동기를 넣은 용례(아래 그림의 작업을 비동기로 처리함)가 존재한다. 아래 그림의 설명은 다음과 같다.

  • A  프로세스 시작
  • B 프로세스 시작 요청
  • B 프로세스 시작 요청 후 대기하는 것이 아닌 A 프로세스 재개
  • A 프로세스 완료 후, B 프로세스 완료 상태 확인 후, 반환

비동기 처리할 때 조심해야할 사항은 무엇일까? 제일 주의해야할 사항은 작업의 무결한 처리이다. 동기 처리의 경우 눈으로 확인이 가능하지만, 비동기 처리의 경우, 눈으로 확인하기 힘들다. 그래서 우리는 "비동기는 실패할 수 있다." 라는 가정하에 개발을 하였다.

저널링

비동기 처리 기술은 아니지만, 파일 시스템의 변경사항을 반영하기 전에, 저널안에 변경 로그(추적할 수 있는 기록)들을 저널에 기입한다. 파일 시스템 변경사항을 복구할 때 해당 저널들을 참고하여 복구한다.

저널링을 잘 이해하기 좋은 것은 Sqlite 트랜잭션 개념 중, "저널 모드"를 이해하면 좋다. Sqlite는 1개 프로세스만 Write 작업(CRU)을 할 수 있다. Sqlite의 트랜잭션을 진행할 때 1개 프로세스가 DB 파일에 Write 작업을 실시한다. 이 때 생성되는 임시 파일이 JOURNAL 파일이다. 반영사항은 DB파일에 기록하고 복구할 파일? 기록들은 JOURNAL에 기록하는 것이다. 만약 Commit이 아닌 Rollback을 진행할 때 JOURNAL 파일을 통해 Rollback을 실시한다. 이런 저널링은 리눅스 파일 시스템 뿐만아니라 많은 곳에서 활용한다.

 

저널링을 하는 이유는 다음과 같다. 작업 중, 얘기치 못한 이유로 작업을 실패했을 때 해당 지점을 찾아서 복구 또는 작업의 완료를 진행하기 위해서이지 않을까 생각이든다. 또는 시점 복구 기법을 사용할 때 이것을 쓰지 않을까? 생각한다.

 

(리눅스 파일 시스템에서 사용된다.) <<- 이 부분 분석/공부 후 추가 기재 필요

 

(번외로 면접에서 비동기 처리 관련 답변을 했었는데 내가 비동기 개발할 때 저널링 기법을 사용했다. 근데 그 때 당시 저널링 방식이 뭔지 몰랐다. 면접관이 묻더라.. "그런 방식을 뭐라하는지 아세요~?" 생각하다 잘 모르겠습니다. 했는데, 저널링이라고 한번 알아보라고 말씀해주셨다. 정말 고마웠다. 이런 피드백... 근데 Sqlite 분석할 때 분석했던 내용이더라... 에휴... 근데 몰랐었음... 그리고 이 글을 작성하는 동기도 주셨지... )

 

저널링의 발전이 DB redo log가 아닐까 생각이 든다... 

비동기 설계 

위에서의 설명과 같이 우리는 완벽한 저널링은 아니더라도 저널링 기법을 본 받아서 비동기 처리 프로세스를 설계해야한다. 

(아래 그림은 기존 설계 사항에서 몇개 삭제하였다.)

순서는 다음과 같다.

  • 클라이언트의 삭제 요청이 들어온다.
  • Server A는 삭제하고자하는 정보들의 식별정보와 삭제 요청한 주체에 대한 정보들을 특정 디렉토리에 기재한다(저널링, 간단한 저널링이다.).
  • 파일 입력 완료 후, 비동기 쓰레드 풀에 notify를 진행한다.
  • Cleint에 삭제 성공을 반환한다(202 Accepted).
  • 비동기 쓰레드 풀은 특정 쓰레드를 지정하여 로직을 진행한다.
  • 삭제하고자하는 식별정보는 파일에 입력했지만 메모리에 보관하고 있기 때문에 조회를 실시하지 않아도 된다.
  • 삭제를 진행한다. 
  • 삭제 트랜잭션은 Server A, B에서 일어나기 때문에 DB A, B 를 접근하여 삭제를 실시한다(이것도 비동기 로직을 태운다 ㅋㅋㅋ).
  • 삭제 완료 후, 결과 감사로깅을 진행한다. 
  • 삭제 트랜잭션을 성공적으로 완료하였다. 
  • 파일에 입력한 것(JOURNAL 파일)을 삭제한다.

위의 사항이 기본 순서이다. 그러나 위의 작업 중 얘기치 못한 시스템 오류로 서버가 종료 되었을 경우, 다음과 같이 진행한다.

  • Destroy 스케줄러는 주기적으로 디렉토리에 있는 파일들을 검사한다. 
  • 파일이 있는 경우, 정상적으로 삭제를 하지 못한 것이다. 
  • 또한 이 때 주의할 사항이 Thread-Safe이다.
  • 비동기 쓰레드가 삭제 하고 있을 때 스케줄러가 접근할 수 있다. 
  • Lock 방식으로 Thread-Safe하게 개발한다. 먼저 파일을 획득하고 선점하면 아무도 접근하지 못한다. 
  • 스케줄러는 Lock을 획득하여 삭제를 시도한다. 
  • 스케줄러는 삭제 트랜잭션을 성공적으로 완료한다.
  • 스케줄러에 대한 결과 값을 감사로깅을 한다.

위의 사항에서 "API 감사로그", "스케줄러 감사로그"를 통해 작업의 완료 여부를 확인할 수 있다.

더 나아가 발전하면 Admin 툴을 이용하여 비동기 실패 여부를 모니터링 기능을 제공할 수 있다.

2부. Spring을 통한 비동기 처리 개발 

개요

Spring을 이용하여 비동기 처리 개발할 때 ThreadPoolTaskExecutor와 ThreadPoolTaskScheduler를 사용하였다. 쉬운 비동기 처리를 위해 ThreadPooltaskExecutor를 사용했으며, 비동기 처리의 실패를 보완하기 위해 ThreadPoolTaskScheduler를 통해 실패한 요청에 대해 재시도하는 로직을 개발하였다. 간략하게 위의 두 가지 사항에 대해 알아보자.

ThreadPoolTaskExecutor

 

Spring에서 설정과 어노테이션의 설정으로 간략하게 비동기 처리를 개발할 수 있다. 스프링의 AOP 전략으로 비동기 처리를 깔끔하게 한다 생각해도 좋다(트랜잭션과 비슷한 원리). 트랜잭션 또한 @Transactional 어노테이션을 통해 트랜잭션 관련 코드가 내 비지니스 로직을 침투를하지 않는다. 이것을 가능하게 하는 것이 SpringAOP이며 Spring 비동기도 마찬가지이다. @Async 어노테이션을 통해 비동기 처리 관련 코드가 내 비지니스 로직을 침투하지 않는다. 

 

@Async 어노테이션은 다음과 같이 지정해야한다.

  • 메소드 레벨 
  • public 메소드
  • Async는 또다른 Async를 호출하면 안된다.

비동기 반환 타입은 다음과 같이 진행할 수 있다.

  • void
  • Future<$너의타입>

java.utils.concurrent.Future을 통해 비동기 반환을 받을 수 있다. 그러나 비동기의 반환의 경우 아래와 같이 기다림이 필요하다.

	Future<String> future = asyncAnnotationExample.asyncMethodWithReturnType();

    while (true) {
        if (future.isDone()) {
            System.out.println("Result from asynchronous process - " + future.get());
            break;
        }
        System.out.println("Continue doing something else. ");
        Thread.sleep(1000);
    }

 

Future를 사용할 경우, 다음의 이점이 존재한다.

  • Future<>.get()을 통해 Exception을 메인 쓰레드(비동기를 호출한 부모 쓰레드)로 전파시켜 Exception 핸들링을 할 수 있다. 
  • 그러나 AsyncUncaughtExceptionHandler를 통해 해결할 수 있다.

SimpleAsyncTaskExecutor를 통해 바로 설정을 할 수 있지만 이런 방법을 통해 설정을 할 수 있다.

 

ThreadPoolTaskScheduler

스케줄러는 말마따나 별도의 쓰레드가 백그라운드에서 주기적으로 작업을 해주는 것이다. 그 이상.. 그 이하.. 설명할 것이 없다.. ThreadPoolTaskScheduler는 ThreadPoolTaskExecutor와 같이 TaskExecutor 인터페이스를 구현하였다. 즉, 비동기 쓰레들을 별도 관리 해준다. 

 

@Scheduled 어노테이션을 통해 간단하게 구현할 수 있지만 좀 복잡하게 설정이 필요한 경우가 존재하여 해당 어노테이션으로 구현하지 않고 일부는 수동으로 설정하였다. 

 

예제는 다음과 같다. 

[SchedulerConfig.class]

	@Bean(name="scheduler")
	public ThreadPoolTaskScheduler threadPoolTaskScheduler() throws IOException {
		ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
		threadPoolTaskScheduler.setPoolSize( 2 );
		threadPoolTaskScheduler.setThreadNamePrefix( "ThreadPoolTaskScheduler" );

		return threadPoolTaskScheduler;
	}
    
    	@Bean
	public ISchedulerOperation intValidOperation( SchedulerAuditLogService schedulerAuditLogService, Invoker invoker, SchedulerDao dao ) {
		if ( settings.getSchedulerAuditMode() ) {
			return new AuditSchedulerOperation( new IntValidSchedulerOperation( dao, invoker ), schedulerAuditLogService );
		} else {
			return new  IntValidSchedulerOperation( dao, invoker );
		}
	}

첫 번째 Bean은 ThreadPoolTaskScheduler이다. 가볍게 보고 넘어가도 괜찮다. 

두 번째 Bean은 프록시, 데코레이터 방식을 통해 빈을 상황에 따라 선택적 주입을 실시한다. 위의 코드를 말로 풀면 스케줄러에 대해 감사로그를 실시할 것 인가 안할 것인가 이다.

 

[Scheduler.class]

public abstract class Scheduler {
	
	boolean state = false;
	
	@Autowired
	private SchedulerDao dao;
	
	@Autowired
	private ThreadPoolTaskScheduler threadPoolTaskScheduler;

	@PostConstruct
	public void scheduleRunnableWithCronTrigger() throws IOException {
		if ( isEnabled() ) {
			threadPoolTaskScheduler.schedule( runner(), getTrigger() );
		}
	}
	
	public SchedulerDao getDao() { return this.dao; }
	
	public abstract Runnable runner();
	
	public abstract Trigger getTrigger();

	public abstract boolean isEnabled();
}

Scheduler를 확장한 구현체의 runner메소드에서 ISchedulerOperation인터페이스의 run메소드를 실행한다. 구현체는 별도 첨부를 하지 않았다.

위의 코드와 설정 파일을 통해 구현체를 아래와 같이 구현할 수 있다.

  • 설정파일에 값을 파싱하여 CronTrigger를 통해 스케줄링 시간을 설정할 수 있다.
  • 설정파일 값을 파싱하여 스케줄링 가동 여부를 설정할 수 있다.
  • 위의 인터페이스와 추상 클래스를 통해 통일된 스케줄러 클래스를 개발할 수 있다.

참고문헌

sqlite.org/atomiccommit.html

www.quora.com/What-is-the-difference-between-a-journaling-vs-a-log-structured-file-system

www.baeldung.com/spring-async

www.baeldung.com/spring-task-scheduler

 

저작자표시 비영리 (새창열림)

'개발관련 > 삽질' 카테고리의 다른 글

Servlet/Spring에서 web.xml은 어떻게 사라졌을까?  (0) 2021.04.28
Apache Commons-DBCP / Hikari-DBCP 정리  (0) 2021.04.05
Oracle/Tibero CURRVAL 사용 주의사항  (0) 2021.03.29
LoRa 1.0 취약점 분석  (0) 2021.02.04
LoRaWAN 1.1 보안 부분 분석(LoRa Spec 6장 일부분 번역)  (0) 2021.02.04
Posted by 동팡

다양한 스프링 활용기(지속적인 업데이트 - 수정: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 내에서의 자유로운 테스트
  • 응집도가 높은 테스트 케이스 산출

 

저작자표시 비영리 (새창열림)

'개발관련 > (과거)메모' 카테고리의 다른 글

PostgreSQL 백업 & 복구  (0) 2021.01.20
PostgreSQL 서버 중요 설정 정보  (0) 2021.01.18
REST API 정리  (0) 2020.12.30
Java Performance를 올리는 코딩 Best Practice  (0) 2020.05.20
IntelliJ(JetBrain) 단축키 표(모음) 공유  (1) 2020.05.18
Posted by 동팡
이전페이지 다음페이지
블로그 이미지

https://github.com/ehdvudee

by 동팡

공지사항

    최근...

  • 포스트
  • 댓글
  • 트랙백
  • 더 보기

태그

  • Secret Sharing
  • Shamir Secret Sharing
  • Thread-safe
  • Spring
  • 개발자 책리뷰
  • 책리뷰
  • 자바
  • What is Vault
  • vault tutorial
  • LoRaWA
  • 하시콥 볼트
  • vault
  • 네이버 비즈니스 플랫폼
  • vault 개요
  • 네이버 클라우드 개발자 면접
  • Secret Sharing 이론
  • 간단리뷰
  • 개발자 준비
  • 개발자 글쓰기 책
  • java
  • 이직 느낀점
  • 볼트란
  • NBP
  • 개발자 이직
  • 이직 정보 공유
  • 경력 채용
  • 네이버 클라우드 이직
  • 네이버 클라우드
  • 글쓰기 가이드
  • Hashicorp

글 보관함

«   2025/05   »
일 월 화 수 목 금 토
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

링크

카테고리

메모장 (70)
개발관련 (68)
삽질 (26)
(과거)메모 (27)
강의 (0)
회고 (9)
책 리뷰 (6)
블로그 관리 글(비공개) (0)
일상 (2)
기타 (0)
책 리뷰 (1)
회고 (0)

카운터

Total
Today
Yesterday
방명록 : 관리자 : 글쓰기
동팡's Blog is powered by daumkakao
Skin info material T Mark3 by 뭐하라
favicon

메모장

https://github.com/ehdvudee

  • 태그
  • 링크 추가
  • 방명록

관리자 메뉴

  • 관리자 모드
  • 글쓰기
  • 메모장 (70)
    • 개발관련 (68)
      • 삽질 (26)
      • (과거)메모 (27)
      • 강의 (0)
      • 회고 (9)
      • 책 리뷰 (6)
    • 블로그 관리 글(비공개) (0)
    • 일상 (2)
      • 기타 (0)
      • 책 리뷰 (1)
      • 회고 (0)

카테고리

PC화면 보기 티스토리 Daum

티스토리툴바