Dockerfile을 사용한 tomcat에 War file 배포하기.

|

📦 Docker 실전 가이드 시리즈 (전체 4편)

  1. Docker 기본 명령어 정리
  2. Dockerfile로 Tomcat에 War 파일 배포하기 (현재 글)
  3. Docker로 Jenkins 설치하기
  4. Docker로 MySQL 설치하기 (한글 인코딩 포함)

사내에서 사용하는 백오피스 시스템을 Dockerfile를 통해 배포할 일이 생겨 설정 후 간단하게 작성해본다.
오늘 배포 관련해서 로컬에서만 처음 설정해 본 내용이라 학습하는대로 내용은 보충을 좀 해야겠다.

[도커 명령어 참고 : http://jmlim.github.io/docker/2019/02/25/docker-command/]

1. 도커 설치 관련

$ sudo apt-get install docker.io  ( 작성자는 이 명령어만 사용하여 설치하였다. )

도커 설치 후 현재 접속중인 사용자에게 권한을 주어야 사용가능하다.

sudo usermod -aG docker $USER 또는 권한을 줄 User의 ID

2. 소스의 루트경로에 Dockerfile 작성

FROM tomcat:8.0.51-jre7-alpine  # docker 안의 tomcat8 셋팅, 사내 백오피스는 자바 7 사용.

ENV TZ=Asia/Seoul ## 타임존 서울로 설정 후 적용
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN rm -Rf /usr/local/tomcat/webapps/ROOT # 기존 도커안의 tomcat의 루트 경로 삭제 
COPY target/배포후 만들어지는 WAR 파일 이름.war /usr/local/tomcat/webapps/ROOT.war # 루트 경로에 복사
ENV JAVA_OPTS="-Dserver.type=dev" # 서버타입 설정, 프로퍼티 파일로 개발 운영 환경 구분..

3. 소스를 내려받은 후 소스의 루트 경로에서 docker build 하기.

$ git clone 소스경로
$ mvn clean package
$ docker build -t ticketmall_backoffice .

4. 이미지 생성 된것 확인

$ docker images

REPOSITORY              TAG                  IMAGE ID            CREATED             SIZE
ticketmall_backoffice   latest               51af9b31cb45        1 hours ago         301MB
tomcat                  8.0.51-jre7-alpine   2304e340e238        10 months ago       145MB

5. 위에서 만든 도커 컨테이너 실행

$ docker container run -p 80:8080 -it ticketmall_backoffice 

80 포트로 컨테이너에 접근할 시 8080으로 포워딩 해줌. 톰캣은 잘 실행된다.

참고자료
https://www.youtube.com/watch?v=GdnZkiif64Q

git merge 후 pull 실패 시 해결 방안 - You have not concluded your merge (MERGE_HEAD exists)

|

증상

You have not concluded your merge (MERGE_HEAD exists)

해결방법

1. 머지 취소

 git merge --abort

2. 충돌 해결

3. 병합을 추가 하고 커밋

 git status
 git commit -am "커밋 내용"

커밋을 제대로 하지 않았을 경우 아래 메세지가 뜰 수 있음.
Pulling is not possible because you have unmerged files

4. 다시 내려 받기

git pull

출처 :

  • https://gutmate.github.io/2018/04/18/git-pull-fail/

스프링부트 + jsp 환경에서 tiles 사용하기 (desktop, mobile 접근 구분하여 페이지 분기)

|

스프링부트 + jsp + tiles 설정이 이미 되어 있다고 가정한다.

[스프링부트 + jsp + tiles 설정 참고 : http://jmlim.github.io/spring/2019/02/08/spring-boot-tiles/]

테스트한 버전은 2.x 버전이며 스프링 부트 버전에 따라 버전을 조금 다르게 가야할 수도 있을 것 같다.

[2026년 추가] 이 글에서 쓴 spring-mobile 프로젝트는 이미 End of Life 상태다 (2022년에 spring-attic으로 아카이브됨, 이 글에서 받아 쓴 2.0.0.M2가 사실상 마지막으로 나온 버전이고 정식 릴리즈는 끝내 안 나왔다). 지금 새로 이런 기능이 필요하다면 이 라이브러리를 설치하려 하지 말 것 — 대신 아래 두 방향 중 하나를 권장한다.

  1. 서버사이드 분기 자체를 없애고 반응형(Responsive) 프론트엔드로 대체. 요즘은 하나의 뷰를 CSS 미디어 쿼리로 데스크탑/모바일에 맞게 다르게 보여주는 게 표준이라, 이 글에서 하는 “URL 앞에 mobile/을 붙여 아예 다른 JSP를 렌더링”하는 방식 자체가 트렌드에서 많이 벗어났다.
  2. 정말 서버에서 기기 종류를 구분해야 한다면 User-Agent 헤더를 직접 파싱하는 가벼운 라이브러리(ua-parser류)를 쓰거나, 간단한 정규식으로 직접 판별하는 필터를 만드는 편이 이제 존재하지 않는 프레임워크에 의존하는 것보다 유지보수하기 쉽다.

1. pom.xml 에 spring mobile 추가.

스프링 모바일 2.x 버전은 아직 정식버전이 나오지 않았으므로 마일스톤에서 내려받는다. (스프링 마일스톤 레포지토리를 추가하여 내려받음.)

....

  <!-- 스프링 마일스톤 repo -->
 <repositories>
     <repository>
         <id>spring-milestones</id>
         <name>Spring Milestones</name>
         <url>https://repo.spring.io/milestone</url>
     </repository>
 </repositories>
....

  <!-- 스프링 모바일 -->
  <dependency>
      <groupId>org.springframework.mobile</groupId>
      <artifactId>spring-mobile-starter</artifactId>
      <version>2.0.0.M2</version>
  </dependency>

2. yml or properties 설정 추가.

yml 사용 시

spring: 
  mobile:
    devicedelegatingviewresolver:
      enabled: true

properties 사용 시

spring.mobile.devicedelegatingviewresolver.enabled: true

이렇게만 추가해도 컨트롤러의 요청 메소드에 Device 정보를 argument 로 받을 수 있다.

@RequestMapping("/")
public void home(Device device) {
    if (device.isMobile()) {
        logger.info("Hello mobile user!");
    } else if (device.isTablet()) {
        logger.info("Hello tablet user!");
    } else {
        logger.info("Hello desktop user!");         
    }
}


3. TilesConfig 에 모바일 분기 관련 설정 추가.

  1. tiles Configurer 에 tiles-mobile.xml 설정 추가.
  2. LiteDeviceDelegatingViewResolver 설정 추가.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mobile.device.view.LiteDeviceDelegatingViewResolver;
import org.springframework.web.servlet.view.tiles3.TilesConfigurer;
import org.springframework.web.servlet.view.tiles3.TilesView;
import org.springframework.web.servlet.view.tiles3.TilesViewResolver;

@Configuration
public class TilesConfig {

    @Bean
    public TilesConfigurer tilesConfigurer() {
        final TilesConfigurer configurer = new TilesConfigurer();
        configurer.setDefinitions(new String[]{"/WEB-INF/tiles/tiles.xml",
             // 모바일 분기 관련 설정 추가부분..
                "WEB-INF/tiles/tiles-mobile.xml"});
        configurer.setCheckRefresh(true);
        return configurer;
    }

    @Bean
    public TilesViewResolver tilesViewResolver() {
        final TilesViewResolver tilesViewResolver = new TilesViewResolver();
        tilesViewResolver.setViewClass(TilesView.class);
        return tilesViewResolver;
    }

    /** 
    * 모바일 분기 관련 설정 추가부분..
    */
    @Bean
    public LiteDeviceDelegatingViewResolver liteDeviceDelegatingViewResolver() {
        LiteDeviceDelegatingViewResolver resolver = new LiteDeviceDelegatingViewResolver(this.tilesViewResolver());
        
        /** 
          tilesViewResolver의 Order 값을 최상위로 높여 주지 않으면  앞서 프로퍼티에서 생성한
          LiteDeviceDelegatingViewResolver에의해 ContentNegotiatingViewResolver 에서 org.springframework.web.servlet.view.JstlView 이 실행되어 Tiles가 동작하지 않게 된다.
        */
        resolver.setOrder(0);
        resolver.setMobilePrefix("mobile/");
        resolver.setEnableFallback(true);
        return resolver;
    }
}

4. tiles-mobile.xml 설정 추가. (기존 tiles.xml 에서 mobile 키워드 추가.)

ex1) name = main-layout -> mobile-main-layout
ex2) value = /WEB-INF/views/main/body/{1}.jsp -> /WEB-INF/views/mobile/main/body/{1}.jsp


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE tiles-definitions PUBLIC
       "-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN"
       "http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>

   <!-- main -->
   <definition name="mobile-main-layout" template="/WEB-INF/views/mobile/main/layout/base-main.jsp">
       <put-attribute name="header" value="/WEB-INF/views/mobile/common/layout/header.jsp" />
       <put-attribute name="body" value="" />
       <put-attribute name="footer" value="/WEB-INF/views/mobile/common/layout/footer.jsp" />
   </definition>
   <definition name="mobile/main/*" extends="mobile-main-layout">
       <put-attribute name="body" value="/WEB-INF/views/mobile/main/body/{1}.jsp" />
   </definition>
   <definition name="mobile/main/*/*" extends="mobile-main-layout">
       <put-attribute name="body" value="/WEB-INF/views/mobile/main/body/{1}/{2}.jsp" />
   </definition>
   <definition name="mobile/main/*/*/*" extends="mobile-main-layout">
       <put-attribute name="body" value="/WEB-INF/views/mobile/main/body/{1}/{2}/{3}.jsp" />
   </definition>

</tiles-definitions>

경로 참고 이미지

5. Controller 에서 페이지 호출

위 설정대로 모바일로 접근 시 경로 리턴값에 자동으로 mobile/ 이 붙게 된다.

아래 LiteDeviceDelegatingViewResolver 소스 부분 참고

ex) mv.setViewName(“main/index”);
로 리턴시 mv.setViewName(“mobile/main/index”); 로 설정값이 바뀜



@Controller
@RequestMapping("/")
public class MainController  {

    @GetMapping(value = {"", "index"})
    public ModelAndView index() {

        ModelAndView mv = new ModelAndView();
        
        .....
        로직로직
        ....
        
        mv.setViewName("main/index");
        return mv;
    }
}

LiteDeviceDelegatingViewResolver.java

public class LiteDeviceDelegatingViewResolver extends AbstractDeviceDelegatingViewResolver {
    private String normalPrefix = "";
    private String mobilePrefix = "";
    private String tabletPrefix = "";
    private String normalSuffix = "";
    private String mobileSuffix = "";
    private String tabletSuffix = "";
 .....
  
   // 1. 앞에 키워드를 붙이는 부분이며 아까 설정에서 mobilePrefix 를 mobile/ 로 설정하였다. 
    protected String getDeviceViewNameInternal(String viewName) {
        RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
        Assert.isInstanceOf(ServletRequestAttributes.class, attrs);
        HttpServletRequest request = ((ServletRequestAttributes)attrs).getRequest();
        Device device = DeviceUtils.getCurrentDevice(request);
        SitePreference sitePreference = SitePreferenceUtils.getCurrentSitePreference(request);
        String resolvedViewName = viewName;
        if (ResolverUtils.isNormal(device, sitePreference)) {
            resolvedViewName = this.getNormalPrefix() + viewName + this.getNormalSuffix();
        } 
        // 2. 모바일 디바이스일 경우 이부분이 조건으로 타게되며 그 경우에 모바일 prefix 인 mobile/ 이 붙에 됨. 
        else if (ResolverUtils.isMobile(device, sitePreference)) {
            resolvedViewName = this.getMobilePrefix() + viewName + this.getMobileSuffix();
        } else if (ResolverUtils.isTablet(device, sitePreference)) {
            resolvedViewName = this.getTabletPrefix() + viewName + this.getTabletSuffix();
        }

        return this.stripTrailingSlash(resolvedViewName);
    }

    private String stripTrailingSlash(String viewName) {
        return viewName.endsWith("//") ? viewName.substring(0, viewName.length() - 1) : viewName;
    }
}

참고:

  • https://mycup.tistory.com/208
  • http://projects.spring.io/spring-mobile/

스프링부트 + jsp 환경에서 tiles 사용하기

|

최근의 트렌드와는 거리가 좀 있는 구성이지만 백엔드 웹 개발자가 화면 개발하기엔 나름 최적의 구성이라고 생각한다.

ex : 백오피스 형태의 admin 페이지

스프링부트 + jsp 가 설정이 이미 되어 있다고 가정한다.

[스프링부트 + jsp 설정 참고 : http://jmlim.github.io/spring/2018/12/13/spring-boot-jsp-setting/]

1. pom.xml 에 아래 의존성 추가.

....

  <!--- JSTL 필요 : JSTL Dependency를 Maven에 추가해줘야 함.-->
  <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
  </dependency>
  
 <dependency>
      <groupId>org.apache.tiles</groupId>
      <artifactId>tiles-jsp</artifactId>
      <version>3.0.7</version>
  </dependency>
....

2. TilesConfig 클래스를 추가하고 아래 설정을 추가한다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.tiles3.TilesConfigurer;
import org.springframework.web.servlet.view.tiles3.TilesView;
import org.springframework.web.servlet.view.tiles3.TilesViewResolver;

@Configuration
public class TilesConfig {

    @Bean
    public TilesConfigurer tilesConfigurer() {
        final TilesConfigurer configurer = new TilesConfigurer();
        //해당 경로에 tiles.xml 파일을 넣음
        configurer.setDefinitions(new String[]{"/WEB-INF/tiles/tiles.xml"});
        configurer.setCheckRefresh(true);
        return configurer;
    }

    @Bean
    public TilesViewResolver tilesViewResolver() {
        final TilesViewResolver tilesViewResolver = new TilesViewResolver();
        tilesViewResolver.setViewClass(TilesView.class);
        return tilesViewResolver;
    }
}

3. tiles.xml 설정 추가.


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE tiles-definitions PUBLIC
       "-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN"
       "http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>

   <!-- main -->
   <definition name="main-layout" template="/WEB-INF/views/main/layout/base-main.jsp">
       <put-attribute name="header" value="/WEB-INF/views/common/layout/header.jsp" />
       <put-attribute name="body" value="" />
       <put-attribute name="footer" value="/WEB-INF/views/common/layout/footer.jsp" />
   </definition>
   <definition name="main/*" extends="main-layout">
       <put-attribute name="body" value="/WEB-INF/views/main/body/{1}.jsp" />
   </definition>
   <definition name="main/*/*" extends="main-layout">
       <put-attribute name="body" value="/WEB-INF/views/main/body/{1}/{2}.jsp" />
   </definition>
   <definition name="main/*/*/*" extends="main-layout">
       <put-attribute name="body" value="/WEB-INF/views/main/body/{1}/{2}/{3}.jsp" />
   </definition>

</tiles-definitions>

경로 참고 이미지

4. main-layout 에 해당하는 base-main.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8">
    <title>ㅎㅎㅎㅎ</title>
    <!--<meta name="viewport" content="width=device-width, initial-scale=1.0">-->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    
  </head>
   <body>
    <section class="content">
      <tiles:insertAttribute name="header"/> <!--  /WEB-INF/views/common/layout/header.jsp -->
      <tiles:insertAttribute name="body"/> <!-- body -->

      <tiles:insertAttribute name="footer"/> <!-- /WEB-INF/views/common/layout/footer.jsp -->
    </section>
  </body>
</html>


5. Controller 에서 페이지 호출

tiles.xml 설정에서 main/* 를 호출하는 것과 같고 body 안의 index.jsp 페이지를 호출함



@Controller
@RequestMapping("/")
public class MainController  {

    @GetMapping(value = {"", "index"})
    public ModelAndView index() {

        ModelAndView mv = new ModelAndView();
        
        .....
        로직로직
        ....
        
        mv.setViewName("main/index");
        return mv;
    }
}

[2부 - 모바일 분기 추가 설정하기 : http://jmlim.github.io/spring/2019/02/09/spring-boot-tiles-mobile/]


jQuery datepicker 사용 시 종료일을 시작일보다 큰 날짜만 지정 가능하도록 설정하기.

|

datepicker 관련 Jquery UI load

<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>

달력을 표시할 startDate, endDate 에 해당하는 inputbox 추가.

<input type="text" id="startDate"/>
<input type="text" id="endDate"/>

datepicker 적용.

<script type="text/javascript">
$(document).ready(function () {
	$("#startDate").datepicker({
		dateFormat: "yy-mm-dd", // 날짜의 형식
		minDate: 0,
		nextText: ">",
		prevText: "<",
		onSelect: function (date) {
			var endDate = $('#endDate');
			var startDate = $(this).datepicker('getDate');
			var minDate = $(this).datepicker('getDate');
			endDate.datepicker('setDate', minDate);
			startDate.setDate(startDate.getDate() + 30);
			endDate.datepicker('option', 'maxDate', startDate);
			endDate.datepicker('option', 'minDate', minDate);
		}
	});
	$('#endDate').datepicker({
		dateFormat: "yy-mm-dd", // 날짜의 형식
		nextText: ">",
		prevText: "<"
	});
});
</script>