기본 콘텐츠로 건너뛰기

Spring MVC & Tiles 3: XML 없는 Java 코드로 동적 레이아웃 구현하기

BACKEND / SPRING

Spring MVC & Tiles 3: XML 없는 Java 코드로 동적 레이아웃 구현하기

Apache Tiles의 번거로운 XML 설정에서 벗어나, DefinitionsFactory와 WildcardHelper를 활용해 유연하고 타입 안전한 동적 레이아웃 시스템을 구축하는 방법을 소개합니다.

1. 왜 XML 대신 Java Config인가?

Spring MVC 프로젝트에서 Apache Tiles는 뷰 레이아웃을 구성하는 강력한 도구입니다. 하지만 전통적인 tiles-defs.xml 방식은 프로젝트 규모가 커질수록 관리가 어려워지고 비효율적일 수 있습니다. Java 기반 설정을 도입하면 다음과 같은 확실한 이점을 얻을 수 있습니다.

  • 동적 생성의 유연성: 비즈니스 로직에 따라 뷰 정의를 런타임에 동적으로 생성하거나 변경할 수 있습니다.
  • 타입 안전성(Type Safety): XML의 텍스트 기반 설정과 달리, 컴파일 시점에 경로 오류나 오타를 감지할 수 있습니다.
  • 유지보수 용이성: 장황한 XML 태그 없이 필요한 로직만 간결하게 Java 메소드로 관리할 수 있습니다.

2. 핵심 구현: DefinitionsFactory와 WildcardHelper

이 구현의 핵심은 Tiles의 DefinitionsFactory 인터페이스를 직접 구현하여 런타임에 정의(Definition)를 제공하는 것입니다. 여기에 WildcardHelper를 결합하면 XML의 와일드카드 기능보다 더 강력한 패턴 매칭이 가능합니다.

2.1. 기본 레이아웃 정의

먼저 고정된 헤더/푸터와 동적으로 변하는 콘텐츠 영역을 정의하는 메소드를 작성합니다. 이는 XML의 <definition> 태그를 Java 코드로 옮긴 것과 같습니다.

2.2. 와일드카드 패턴 매칭 로직

컨트롤러가 반환한 뷰 이름(예: dddd/board)이 정의된 맵에 직접 없을 경우, WildcardHelper를 통해 패턴 매칭을 시도하는 로직이 필요합니다. 이를 통해 dddd/*와 같은 패턴을 미리 정의해두면, dddd/userList, dddd/admin 등 다양한 요청을 하나의 설정으로 처리할 수 있습니다.

3. [실전 예제] 전체 소스 코드 구현

아래 코드는 DefinitionsFactory를 상속받아 직접 구현한 설정 클래스입니다. 맵(Map)을 이용해 기본 정의를 관리하고, WildcardHelper를 이용해 동적 매칭을 수행합니다.

import org.apache.tiles.Definition;
import org.apache.tiles.definition.DefinitionsFactory;
import org.apache.tiles.request.Request;
import org.apache.tiles.wildcard.WildcardHelper;
// 기타 필요한 import 생략...

public class TilesDefinitionsConfig implements DefinitionsFactory {

    private static final Map<String, Definition> tilesDefinitions = new HashMap<>();
    private static final WildcardHelper wildcardHelper = new WildcardHelper();

    // 생성자에서 기본 레이아웃 정의 초기화
    public TilesDefinitionsConfig() {
        // 1. 기본 레이아웃 (Base Definition)
        addDefinition("base.layout", "기본 타이틀", "/WEB-INF/views/layout/defaultLayout.jsp");
        
        // 2. 와일드카드 패턴 정의 (예: "site/*" 요청 처리)
        // {1}은 실제 요청된 View 이름으로 치환됩니다.
        addDefinition("site/*", "서브 페이지", "/WEB-INF/views/{1}.jsp");
    }

    /**
     * Definition 추가 헬퍼 메소드
     */
    private void addDefinition(String name, String title, String bodyAttribute) {
        Map<String, Attribute> attributes = new HashMap<>();
        attributes.put("title", new Attribute(title));
        attributes.put("header", new Attribute("/WEB-INF/views/layout/header.jsp"));
        attributes.put("body", new Attribute(bodyAttribute)); // 동적 콘텐츠 영역
        attributes.put("footer", new Attribute("/WEB-INF/views/layout/footer.jsp"));

        tilesDefinitions.put(name, new Definition(name, new Attribute("/WEB-INF/views/layout/mainLayout.jsp"), attributes));
    }

    /**
     * 핵심 로직: 요청된 View 이름(name)에 해당하는 Definition 반환
     */
    @Override
    public Definition getDefinition(String name, Request tilesContext) {
        // 1. 정확히 일치하는 정의가 있으면 반환
        if (tilesDefinitions.containsKey(name)) {
            return tilesDefinitions.get(name);
        }

        // 2. 일치하는 게 없으면 와일드카드 패턴 매칭 시도
        for (Map.Entry<String, Definition> entry : tilesDefinitions.entrySet()) {
            String key = entry.getKey();
            int[] pattern = wildcardHelper.compilePattern(key);
            
            // 매칭 성공 시 변수(vars)에 치환될 값들이 담김
            List<String> vars = wildcardHelper.match(name, pattern);

            if (vars != null) {
                // 부모 Definition을 복제하여 새로운 Definition 생성
                Definition parentDef = entry.getValue();
                Definition newDef = new Definition(parentDef);
                newDef.setName(name);

                // 속성 내의 {1}, {2} 등을 실제 값으로 치환 (예: {1} -> board/list)
                for (String attrName : parentDef.getAttributeAttributeMap().keySet()) {
                    Attribute attr = newDef.getAttribute(attrName);
                    if (attr != null && attr.getValue() instanceof String) {
                        String converted = wildcardHelper.convertParam((String) attr.getValue(), vars);
                        attr.setValue(converted);
                    }
                }
                return newDef;
            }
        }
        return null; // 매칭 실패
    }
}

4. Spring 설정 빈(Bean) 등록

작성한 TilesDefinitionsConfig를 Spring의 TilesConfigurer에 연결해주어야 합니다. 보통 WebMvcConfig 클래스에 아래와 같이 등록합니다.

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Bean
    public TilesConfigurer tilesConfigurer() {
        TilesConfigurer configurer = new TilesConfigurer();
        // 앞서 만든 Java Config 클래스를 DefinitionsFactory로 지정
        configurer.setDefinitionsFactoryClass(TilesDefinitionsConfig.class);
        
        // 추가 설정 (CheckRefresh 등)
        configurer.setCheckRefresh(true);
        return configurer;
    }

    @Bean
    public TilesViewResolver tilesViewResolver() {
        TilesViewResolver resolver = new TilesViewResolver();
        resolver.setOrder(1); // 우선순위 1등
        return resolver;
    }
}

🚀 결론: 이제 XML 파일을 열어보지 않고도 Java 코드 내에서 뷰의 논리적 구조를 관리할 수 있습니다. WildcardHelper를 활용하면 수백 개의 페이지도 단 몇 줄의 패턴 정의로 처리가 가능해집니다.

댓글

이 블로그의 인기 게시물

Java Servlet Request Parameter 완전 정복 — GET/POST 모든 파라미터 확인 & 디버깅 예제 (Request Parameter 전체보기)

Java Servlet Request Parameter 완전 정복 — GET/POST 모든 파라미터 확인 & 디버깅 예제 Java Servlet Request Parameter 완전 정복 웹 애플리케이션에서 클라이언트로부터 전달되는 Request Parameter 를 확인하는 것은 필수입니다. 이 글에서는 Java Servlet 과 JSP 에서 GET/POST 요청 파라미터를 전체 출력하고 디버깅하는 방법을 다양한 예제와 함께 소개합니다. 1. 기본 예제: getParameterNames() 사용 Enumeration<String> params = request.getParameterNames(); System.out.println("----------------------------"); while (params.hasMoreElements()){ String name = params.nextElement(); System.out.println(name + " : " + request.getParameter(name)); } System.out.println("----------------------------"); 위 코드는 요청에 포함된 모든 파라미터 이름과 값을 출력하는 기본 방법입니다. 2. HTML Form과 연동 예제 <form action="CheckParamsServlet" method="post"> 이름: <input type="text" name="username"><br> 이메일: <input type="email" name="email"><b...

PostgreSQL 달력(일별,월별)

SQL 팁: GENERATE_SERIES로 일별, 월별 날짜 목록 만들기 SQL 팁: GENERATE_SERIES 로 일별, 월별 날짜 목록 만들기 데이터베이스에서 통계 리포트를 작성하거나 비어있는 날짜 데이터를 채워야 할 때, 특정 기간의 날짜 목록이 필요할 수 있습니다. PostgreSQL과 같은 데이터베이스에서는 GENERATE_SERIES 함수를 사용하여 이 작업을 매우 간단하게 처리할 수 있습니다. 1. 🗓️ 일별 날짜 목록 생성하기 2020년 1월 1일부터 12월 31일까지의 모든 날짜를 '1 day' 간격으로 생성하는 쿼리입니다. WITH date_series AS ( SELECT DATE(GENERATE_SERIES( TO_DATE('2020-01-01', 'YYYY-MM-DD'), TO_DATE('2020-12-31', 'YYYY-MM-DD'), '1 day' )) AS DATE ) SELECT DATE FROM date_series 이 쿼리는 WITH 절(CTE)을 사용하여 date_series 라는 임시 테이블을 만들고, GENERATE_SERIES 함수로 날짜를 채웁니다. 결과 (일별 출력) 2. 📅 월별 날짜 목록 생성하기 동일한 원리로, 간격을 '1 MONTH' 로 변경하면 월별 목록을 생성할 수 있습니다. TO...

CSS로 레이어 팝업 화면 가운데 정렬하는 방법 (top·left·transform 완전 정리)

레이어 팝업 센터 정렬, 이 코드만 알면 끝 (CSS 예제 포함) 이벤트 배너나 공지사항을 띄울 때 레이어 팝업(center 정렬) 을 깔끔하게 잡는 게 생각보다 어렵습니다. 화면 크기가 변해도 가운데에 고정되고, 모바일에서도 자연스럽게 보이게 하려면 position , top , left , transform 을 정확하게 이해해야 합니다. 이 글에서는 아래 내용을 예제로 정리합니다. 레이어 팝업(center 정렬)의 기본 개념 자주 사용하는 position: absolute / fixed 정렬 방식 질문에서 주신 스타일 top: 3.25%; left: 50%; transform: translateX(-50%) 의 의미 실무에서 바로 쓰는 반응형 레이어 팝업 HTML/CSS 예제 1. 레이어 팝업(center 정렬)이란? 레이어 팝업(레이어 팝업창) 은 새 창을 띄우는 것이 아니라, 현재 페이지 위에 div 레이어를 띄워서 공지사항, 광고, 이벤트 등을 보여주는 방식을 말합니다. 검색엔진(SEO) 입장에서도 같은 페이지 안에 HTML이 존재 하기 때문에 팝업 안의 텍스트도 정상적으로 인덱싱될 수 있습니다. 즉, “레이어 팝업 센터 정렬”, “레이어 팝업 만드는 방법”과 같이 관련 키워드를 적절히 넣어주면 검색 노출에 도움이 됩니다. 2. 질문에서 주신 레이어 팝업 스타일 분석 질문에서 주신 스타일은 다음과 같습니다. <div class="layer-popup" style="width:1210px; z-index:9001; position:absolute; top:3.25%; left:50%; transform:translateX(-50%);"> 레이어 팝업 내용 <...