[Ant] JAR 내부 .class 파일 바꿔치기

FrogRat·2021년 1월 22일
0
post-thumbnail

배경

더 이상 사용하지 않는 사내 레거시 프레임워크를 제거하는 작업 중 기존 프레임워크에서 제공하던 기능들을 모듈화 하여, 스프링 MVC 로 넘어가는 과도기 동안에 두 종류의 프레임워크에서 동시에 사용 가능한, 호환성을 제공하는 모듈을 개발하는 작업을 진행했다.

모듈화 작업 중 데이터 액세스(Data Access) 기능은 레거시 프레임워크가 제공하던 기존의 틀은 그대로 가져가면서 알맹이는 스프링에서 생성/관리하는 데이터 액세스 관련 빈(DataSource, SqlMapClinet 등)을 레거시 껍데기에 주입하여 사용하는 형태로 변경하고자 했다.

하지만 위와 같이 사용 시 레거시 프레임워크 틀에서 제공하는 class 파일에 수정이 필요하여 이를 해결하기 위해 레거시 프레임워크 jar 내에 class 파일을 변경해줘야 했다.

해결

서비스 중인 프로젝트 대부분은 Ant 빌드를 사용하고 있다. Ant에서 제공하는 기본적인 명령어을 이용해 jar 앞축을 풀고, class 파일을 삭제하고, 다른 파일을 추가하고, jar를 다시 압축하는 작업을 할 수 있다. 위 작업을 통해 변경이 필요한 class를 원하는 형태로 동작하는 다른 class로 바꿔치기해줄 수 있었다.

이해를 쉽게 하기 위해 기존 프레임워크에서 제공하는 jar는 module_old, 문제가 되는 클래스 명은 Old, 신규로 제공되는 데이터 액세스(Data Access) 모듈은 module_new라고 가정해보자.

  1. 바꿔치기할 Old.class 파일 생성(module_new.jar 내에 존재)
  2. Ant 빌드 Task 정의
    a. module_old.jar 압축 해제
    b. module_old.jar 내에 Old.class 파일 삭제
    c. module_new.jar 압축 해제
    d. module_new.jar 내에 Old.class 파일을 module_old.jar 압축 해제 경로에 추가
    e. module_old.jar 재압축
  3. 빌드 시 2번에서 정의한 Task 실행되도록 Task 단계 추가

해당 작업 전 몇 가지 주의할 점이 있다.

  • 바꿔치기할 class의 패키지 명, 클래스 명이 module_old에서 기존에 사용하던 Old.class의 것과 동일해야 함.
  • 바꿔치기한 후에도 Old 클래스 사용 시 외부에 제공하는 기능은 변경 전/후 상관없이 동일해야 함.

내장 Ant Task 명 및 속성들

jar

  • destfile: 압축된 jar 최종 path

unjar

  • src: 압축을 풀 jar 전체 path
  • dest: 압축 해제한 파일들이 위치할 디렉터리 path

copy

  • file: 복사할 파일 전체 path
  • todir: 복사한 파일이 위치할 디렉터리 전체 path
  • overwrite: 동일한 이름의 파일 존재 시 덮어쓰기 여부(default: false)
  • verbose: 복사한 파일 관련 로그 남기기(default: false)
  • failonerror: 작업 실패 시 빌드 Stop 여부(default: true)

상세 빌드 스크립트

<?xml version="1.0" encoding="UTF-8"?>
<project default="dist">
  <target name="repackage">
    <property name="line.seperator" value="&#13;&#10;"/>
    <propertyregex property="result.total.lib"
    input="${toString:total.lib}"
    regexp="${spliter.regexp}"
    replace="${line.seperator}"/>

    <property name="new.name.prefix" value="module_new-"/>
    <property name="old.name.prefix" value="module_old-"/>

    <!-- 변경하고자 하는 jar의 full path 찾기 -->
    <for list="${result.total.lib}" delimiter="${line.seperator}" param="select.current.lib">
      <sequential>
      <if>
      <contains string="@{select.current.lib}" substring="${old.name.prefix}"/>
      <then>
      	<property name="old.jar.path" value="@{select.current.lib}"/>
      </then>
      </if>
      <if>
      <contains string="@{select.current.lib}" substring="${new.name.prefix}"/>
      <then>
      	<property name="new.jar.path" value="@{select.current.lib}"/>
      </then>
      </if>
      </sequential>
    </for>

    <!-- module_new.jar 압축 해제 -->
    <property name="new.temp.dir" value="${project.classes.dir}/${new.name.prefix}unjar"/>
    <unjar dest="${new.temp.dir}" src="${new.jar.path}"/>

    <!-- module_old.jar 내에 com/old/Old.class 를 module_new.jar 의 com/old/Old.class 로 변경 -->
    <antcall target="change-class-in-library">
      <param name="source.file.package.name" value="com/old/"/>
      <param name="source.file.classpath" value="${new.temp.dir}/com/old/Old.class"/>

      <param name="destination.lib.name.prefix" value="${old.name.prefix}"/>
      <param name="destination.lib.path" value="${old.jar.path}"/>
    </antcall>

    <delete dir="${new.temp.dir}" failonerror="true"/>
  </target>

  <!-- jar 내에 파일 변경 Task-->
  <target name="change-class-in-library">
    <available property="source.file.exist" file="${source.file.classpath}"/>
    
    <if>
    	<equals arg1="${source.file.exist}" arg2="true"/>
      <then>
        <property name="destination.lib.temp.jar" value="${destination.lib.name.prefix}temp.jar"/>

        <property name="destination.lib.temp.dir"
        value="${project.classes.dir}/${destination.lib.name.prefix}unjar"/>

        <!-- module_old.jar 압축 해제 -->
        <unjar dest="${destination.lib.temp.dir}" src="${destination.lib.path}"/>

        <!-- com/old/Old.class 바꿔치기 -->
        <copy file="${source.file.classpath}"
        todir="${destination.lib.temp.dir}/${source.file.package.name}"
        overwrite="true" verbose="true" failonerror="true"/>

        <!-- module_oldtemp.jar 로 압축 -->
        <jar destfile="${project.webinf.lib.dir}/${destination.lib.temp.jar}">
        <fileset dir="${destination.lib.temp.dir}"/>
        </jar>

        <!-- module_oldtemp.jar 를 module_old.jar 로 바꿔치기 -->
        <copy file="${project.webinf.lib.dir}/${destination.lib.temp.jar}"
        tofile="${destination.lib.path}"
        overwrite="true" verbose="true" failonerror="true"/>

        <!-- module_old.jar 를 압축 해제했던 디렉토리 삭제 -->
        <delete file="${project.webinf.lib.dir}/${destination.lib.temp.jar}" failonerror="true"/>
        <delete dir="${destination.lib.temp.dir}" failonerror="true"/>
      </then>
      <else>
      	<fail message="${source.file.classpath} do not exist."/>
      </else>
    </if>
  </target>
</project>

History

개인적으로 기존 라이브러리 내부 파일을 이와 같은 방식으로 동작 방식을 바꿔 사용하는 것은 특별한 케이스가 아니고서는 매우 위험한 해결 방법이라고 생각한다. 이는 해당 라이브러리 사용 시 개발자가 예상치 못한 다른 기능에 심각한 오류를 발생시킬 수 있다.

사실 처음에 고안된 해결책은 이처럼 빌드 타임에 Ant Task를 통해 자동화된 형태로 class 파일을 변경해주는 방식이 아니었다.

수동으로 jar 파일을 디컴파일하여 원하는 방식으로 동작하는 Java 파일을 넣어주고 재컴파일하여 별도의 버전으로 jar를 생성해주는 방식을 생각했다. 이런 식으로 해결하게 될 경우 해당 jar 를 영문도 모른채 다른 누군가가 사용하게 될 경우 문제가 될 수 있고, 혹여나 아주 희박한 가능성으로 레거시 프레임워크에 패치 작업이 있을 경우가 있을 수 있다.

그래서 이와 같은 불편함을 없애기 위해 컴파일 타임, 빌드 타임 혹은 서버 로딩 타임에 자동화된 형태로 class 파일을 변경해줄 수 있는 방법이 없을까 여러 방법을 찾는 와중에 위와 같이 Ant 빌드를 이용해 빌드타임에 교체해주는 방법을 고안하게 되었다.

참조

링크

profile
병 안에 쥐개구리

0개의 댓글