Spring Loaded로 Tomcat 재시작 없이 수정한 클래스 반영하기

수정한 클래스 파일만 재로딩하는 Spring Loaded를 tomcat7-maven-plugin, Eclipse WTP와 함께 사용하는 방법을 정리합니다.

Note

2014년에 gist로 작성했던 글을 2026년에 이 블로그로 옮겼습니다. 발행 날짜는 gist 작성일(2014-10-13)에 맞췄고, 내용도 그 시점 기준입니다. 원본에 있던 Eclipse 설정 화면 스크린샷 3장은 원본 이미지 서버가 없어져 싣지 못했습니다.

Spring Loaded는 개발 환경에서 .java 클래스를 수정했을 때 변경된 클래스 파일만 재로딩하는 도구입니다. Local PC에서 수정과 Tomcat 재시작을 반복하는 시간을 줄여 줍니다.

다만 JRebel도 그러하듯이 모든 경우에 완벽한 리로딩이 되지는 않습니다. 메서드 추가·수정은 잘 반영됩니다. 그러나 아래와 같은 경우에는 자동 반영이 되지 않습니다.

  • 상속 구조의 변경

  • Reflection 정보가 Cache된 것

  • XML 설정 수정 (프레임워크에 특화된 구현이 들어가지 않으면 당연히 어렵습니다.)

그래도 많은 경우 Tomcat 재시작 없이 개발을 이어갈 수 있다면 생산성에 도움이 되리라 생각합니다. 이를 tomcat-maven-plugin과 함께 사용하는 방법을 정리합니다. Linux 명령어를 기준으로 썼지만, 윈도우에서도 같은 역할을 하는 작업을 수행하면 됩니다.

다운로드

참조할 디렉터리에 다운로드합니다. tomcat-maven-plugin을 사용할 예정이면 `pom.xml`이 있는 디렉터리에 받기를 권장합니다.

wget -O springloaded-1.2.1.jar http://search.maven.org/remotecontent?filepath=org/springframework/springloaded/1.2.1.RELEASE/springloaded-1.2.1.RELEASE.jar

Spring Loaded의 GitHub에서 최신 버전을 확인한 후 다운로드하는 것이 좋습니다.

tomcat7-maven-plugin으로 실행

1. 환경변수 설정

export MAVEN_OPTS="-javaagent:springloaded-1.2.1.jar -noverify"

2. pom.xml에 Tomcat7 plugin 추가

pom.xml
<plugin>
    <groupId>org.apache.tomcat.maven</groupId>
    <artifactId>tomcat7-maven-plugin</artifactId>
    <version>2.2</version>
    <configuration>
        <path>/</path>
    </configuration>
</plugin>

3. 실행

mvn tomcat7:run

Jetty나 Tomcat 6을 쓰고 싶다면 Local 개발환경에서 WAS를 띄우는 여러가지 방법을 참고합니다.

Eclipse WTP에서 실행

1. java agent 설정

Eclipse 메뉴의 Run > Run Configurations에서 Tomcat 실행 설정을 찾은 다음, Arguments 탭의 VM arguments란에 `-javaagent`를 추가합니다. 한 번이라도 Tomcat을 WTP로 실행해야 해당 설정이 생깁니다.

-javaagent:/다운로드경로/springloaded-1.2.1.jar -noverify

2. 기본 reload 설정 제거

Servers 뷰에서 Tomcat을 선택하고 Overview 탭의 Server Options란에서 'Modules auto reload by default’를 선택 해제합니다. Modules 탭의 해당 모듈 설정에서도 Auto reloading이 해제되어 있는지 확인합니다.

main 메서드를 직접 실행하는 경우

Spring Boot나 Embedded Tomcat, Jetty를 이용해서 main 메서드로 직접 WAS를 띄우는 경우도 있습니다. 이때도 Run > Run Configurations의 Arguments 탭에서 VM arguments란에 같은 -javaagent 설정을 추가합니다.

통합 테스트를 위한 내장 Tomcat, tomcat-bed

통합 테스트 안에서 내장 Tomcat을 띄워 JSP 렌더링까지 검증하는 tomcat-bed 예제를 설명합니다. Spring 테스트 컨텍스트에 bean으로 등록하면 여러 테스트가 WAS 하나를 공유하고, 포트가 이미 사용 중이면 떠 있는 서버를 그대로 씁니다.

Note

이 글은 2026년에 예전 예제 저장소들을 정리하면서 작성했습니다. benelog/tomcat-bed 저장소를 이 블로그 저장소의 examples/tomcat-bed로 병합했고, 발행 날짜는 원본 저장소의 마지막 커밋 날짜(2013-06-29)에 맞췄습니다. 본문도 그 시점의 기술 기준으로 서술했습니다.

tomcat-bed는 통합 테스트 안에서 내장 Tomcat을 띄우는 작은 유틸리티입니다. Spring 테스트 컨텍스트에 bean으로 등록하면 테스트가 시작될 때 WAS를 올리고 끝나면 내립니다. 별도의 배포 작업 없이 JSP 렌더링 결과까지 검증하는 UI 테스트를 JUnit 안에서 실행하려고 만들었습니다.

왜 만들었나

웹 계층을 검증하는 방식은 크게 세 가지입니다.

방식 장점 한계

배포된 서버 대상 UI 테스트

실제 운영과 같은 환경을 검증

테스트 전에 배포 작업이 필요하고, Test Coverage 측정이 어려움

Spring MVC Test (MockMvc)

서블릿 컨테이너 없이 빠르게 실행

Controller가 반환한 view 이름까지만 검증. JSP가 실제로 어떻게 렌더링되는지는 알 수 없음

내장 WAS + UI 테스트 (tomcat-bed)

배포 없이 JSP 렌더링까지 검증. EclEmma로 Eclipse에서 바로 커버리지 확인 가능

실제 Tomcat이 시작되므로 `MockMvc`보다 느림

Spring 3.2에 포함된 Spring MVC Test는 서블릿 컨테이너 없이 DispatcherServlet 계층을 검증하는 좋은 도구입니다. 다만 JSP 렌더링 결과는 검증 범위 밖입니다. 화면까지 확인하려면 실제 서블릿 컨테이너가 필요한데, 매번 배포해서 테스트하기는 번거롭습니다. 그래서 테스트 프로세스 안에서 Tomcat을 직접 띄우는 방식을 시도했습니다. Local 개발환경에서 WAS를 띄우는 여러가지 방법에서 다뤘던 내장 WAS 활용법을 테스트로 가져온 셈입니다.

핵심 클래스 WebApplicationServer

핵심은 WebApplicationServer.java 클래스 하나입니다. Tomcat 6의 org.apache.catalina.startup.Embedded API로 Engine, Host, Context를 조립합니다. 주요 부분만 옮기면 아래와 같습니다.

WebApplicationServer.java
public WebApplicationServer(String contextPath, int port, String appBase) {
	this.port = port;
	this.container = new Embedded();
	this.container.setName("TomcatEmbeddedServer");
	this.appContext = createAppContext(contextPath);
	Host localHost = createHost(appBase, appContext);
	Engine engine = createEngine(localHost);
	this.container.addEngine(engine);

	Connector connector = this.container.createConnector(
			localHost.getName(), port, false);
	this.container.addConnector(connector);
}

@PostConstruct
public void start() throws IOException, LifecycleException {
	log.info("Starting Tomcat... (port={})", port);

	if (isEmptyPort()) {
		this.container.start();
		this.running = true;
		this.servletContext = appContext.getServletContext();
	} else {
		log.info("The port({}) is already occupied.", port);
	}
}

설계 포인트는 세 가지입니다.

  • 배포 없는 실행 : `appBase`를 `src/main`으로 지정하면 war를 만들지 않고 소스 트리의 webapp 폴더를 그대로 서비스합니다.

  • 포트 점유 확인 : 지정한 포트가 이미 사용 중이면 내장 Tomcat을 시작하지 않습니다. PC에 개발용 WAS가 떠 있으면 그 서버를 대상으로 같은 테스트가 그대로 동작합니다.

  • 수명 주기 관리 : @PostConstruct/`@PreDestroy`로 시작과 종료를 선언해서, Spring 컨테이너가 bean의 수명에 맞춰 WAS를 올리고 내립니다.

Spring 테스트와 통합

`WebApplicationServer`를 테스트용 컨텍스트에 bean으로 등록합니다.

applicationContext-ui-test.xml
<util:properties id="testServer" location="classpath:testServer.properties" />

<bean id="was" class="net.benelog.tomcatbed.WebApplicationServer"
	c:contextPath="#{testServer.contextPath}"
	c:port="#{testServer.port}"
	c:appBase="src/main"/>

Spring TestContext 프레임워크는 같은 설정을 쓰는 테스트끼리 `ApplicationContext`를 캐싱해서 공유합니다. 덕분에 여러 테스트 클래스를 함께 실행해도 WAS는 한 번만 시작됩니다.

서버 접속 정보는 별도 속성 파일로 분리했습니다.

testServer.properties
contextPath=/
port=8080
baseUrl=http://localhost:8080

`baseUrl`을 개발 서버나 Staging 서버 주소로 바꾸면 같은 테스트를 다른 환경을 대상으로도 실행할 수 있습니다. 그 경우 해당 포트가 이미 사용 중이 아니므로 내장 Tomcat만 로컬에서 뜨지 않을 뿐, 테스트 코드는 동일합니다.

JWebUnit으로 화면 검증

테스트에서는 JWebUnit으로 실제 렌더링된 화면을 검증합니다. ZipCodePageTest.java는 폼 입력과 전송, 응답 화면 내용까지 확인합니다.

ZipCodePageTest.java
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/applicationContext-ui-test.xml"})
public class ZipCodePageTest {
	@Value("#{testServer.baseUrl}")
	String baseUrl;

	@Before
	public void prepare() {
		setBaseUrl(baseUrl);
		setScriptingEnabled(false);
	}

	@Test
	public void zipCodeForm() {
		beginAt("/zipCodeForm");
		assertFormPresent();
		assertSubmitButtonPresent();

		setTextField("zipCode", "121-270");
		submit();

		assertResponseCode(HttpStatus.OK.value());
		assertTitleEquals("Address");
		assertMatch("서울특별시 마포구 상암동");
	}
}

`@ContextConfiguration`으로 위의 테스트 컨텍스트를 지정하기만 하면, 테스트가 실행될 때 WAS가 준비됩니다. JSP가 렌더링한 제목과 본문 텍스트를 그대로 단언할 수 있다는 점이 `MockMvc`와의 차이입니다.

같은 예제 프로젝트에 두 방식을 나란히 두었습니다. HomeMvcTest.java는 Spring MVC Test로 Controller 계층을, HomePageTest.java는 내장 Tomcat 위에서 같은 화면을 검증합니다. 빠른 피드백은 `MockMvc`로, 화면까지 포함한 검증은 tomcat-bed로 나눠 쓰는 구성입니다.

한계

  • 실제 Tomcat이 시작되므로 Spring MVC Test보다 시작 속도가 느립니다.

  • 태그 라이브러리 선언 파일(*.tld)을 수동으로 등록해야 인식됩니다. 예제에서도 WEB-INF/tld에 JSTL과 Spring 태그의 tld 파일을 직접 넣었습니다.

  • Tomcat 6의 Embedded API를 사용했는데, Tomcat 7부터는 더 간결한 org.apache.catalina.startup.Tomcat 클래스가 이 역할을 대신합니다. 새로 시작한다면 그쪽이 출발점으로 적합합니다.

참고 자료


이 포스트는 Claude Code와 정상혁이 함께 작성했습니다.

Android에서 @Inject, @Test

제가 지난달에 썼던 글이 네이버의 기술블로그인 http://helloworld.naver.com에 공개되었습니다.

Android에서 DI + Test 스타일로 개발을 하는데 겪는 어려움과 이를 극복하는데 도움을 주는 Android annotations와 Robolectric 등의 오픈소스 프레임워크를 비교하고 소개했습니다.