Jsoup을 이용하여 HTML Parser 만들기

Aryumka·2023년 4월 10일
0

Java

목록 보기
1/1

1. 문제 인식

위와 같은 시스템 메일 전송 시 클라이언트의 입력을 받지 않고 서버에서 발송하게 됩니다. 직접 구현할 수도 있고 메일침프스티비 등 메일 전송 서비스를 이용하기도 합니다.

저희 회사에서 서비스 중인 프로그램들은 이 기능을 직접 구현하고 있습니다.

@Service
public class UserMngSvc {
	public AUTH SendNum(HttpServletRequest req, String email) throws Exception{
    	MAIL mail = new MAIL();
        StringBuffer mailForm = new StringBuffer();
	  	String content = "컨텐츠입니다";
	  	String title = "타이틀입니다";
	  	String num = "123456";

        mailForm.append("<div style=''>");
        mailForm.append("    <table width='100%' border='0' cellspacing='0' cellpadding='0' style='");
        mailForm.append("		font-size:1rem;font-family:Helvetica ,sans-serif;letter-spacing: -0.03em; word-break:break-all;'>");
        mailForm.append("		<tbody>");
        mailForm.append("        	<tr>");
        mailForm.append("        	   <td style='width: 100%;'><img src='' alt='logo' height='32px' style='margin: 20px auto;'/></td>");
        mailForm.append("        	</tr>");
        mailForm.append("        	<tr>");
        mailForm.append("        	    <th style=''>");
        mailForm.append("        	        <span id='mail_title'>타이틀</span><span> "+ title +"</span><br/>");
        mailForm.append("        	    </th>");
        mailForm.append("        	</tr>");
        mailForm.append("        	<tr>");
        mailForm.append("        	    <td style='padding-top: 50px'>");
        mailForm.append("        	        <div style=''> 내용내용 <br>설명설명<br>"+num+"<center><h3>"+ content +"</h3></center></div>");
        mailForm.append("        	    </td>");
        mailForm.append("        	</tr>");
        mailForm.append("        </tbody>");
        mailForm.append("        <tfoot>");
        mailForm.append("        <tr>");
        mailForm.append("            <td style='text-align: left;'>");
        mailForm.append("            </td>");
        mailForm.append("        </tr>");
        mailForm.append("        </tfoot>");
        mailForm.append("    </table>");
        mailForm.append("</div>");

        mail.setMSG(mailForm.toString());
...
}

메일에 들어갈 HTML코드를 위와 같이 StringBuffer를 이용해 작성합니다. 메일에 들어갈 내용이 달라질 경우 변하는 텍스트 값을 변수로 넣습니다. 그러나 위와 같은 코드 작성에는 다음과 같은 문제가 있습니다.

  • 생산성 저하
    디자이너가 html을 작성한 이후 그 html을 개발자가 자바 코드로 덧붙이는 작업을 중복으로 진행해야 합니다. 또한 html만을 검증하기 위해서 관련 기능을 모두 수행하거나 자바코드를 다시 html로 변환해 브라우저에 띄워봐야 합니다.

  • 코드의 가독성 저하
    로직의 흐름과 상관없는 문자열을 덧붙이는 코드가 지나치게 길어집니다. 코드가 지저분해지고 로직 파악에 방해가 됩니다.

  • 응집도 저하
    같은 양식의 html이라도 메소드(또는 클라이언트)가 다르거나 메소드 안에서 변수로 처리할 요소가 다르면 중복된 코드가 여러 곳에 분산될 수 있습니다.
    따라서 html 양식이 변경될 경우 중복된 코드를 여러곳에서 수정해야 하는 번거로움이 생기며 어떤 자바 코드에서 어떤 양식을 사용했는지 또한 알기 힘들어집니다.

  • 역할과 책임이 불분명
    애초에 java단에서 구체적인 HTML 코드를 갖고 있는 것이 문제입니다. 화면 영역과 서버 영역의 책임이 모호해집니다. 해당 양식의 html이나 css가 변경될 경우 디자이너가 직접 자바코드를 수정하거나 개발자가 java로 씌여진 html을 수정해야 합니다.

2. 문제 해결

HTMLParser 유틸리티 클래스를 만들어 위 문제를 해결할 것입니다. 해당 클래스에서 제공할 기능은 다음과 같습니다.

  1. 필요한 양식을 불러옴
  2. 해당 양식의 HTML 요소를 자바 DOM 으로 파싱
  3. 달라지는 내용을 동적으로 조작하여 HTML로 반환

여기서 2,3의 기능을 구현하기 위해 Jsoup 라이브러리를 이용할 것입니다.

Jsoup 이란?

자바에서 매우 범용적인 웹 크롤링 라이브러리로 간단한 GET/POST 방식 HTTP 요청도 지원합니다.

  • URL 또는 문서에서 DOM 순회 또는 CSS 선택자를 통해 데이타를 추출.
  • URL 또는 문서에서 가져온 HTML을 파싱하여 요소, 속성, 텍스트 등을 조작
  • 정렬된 HTML 출력
  • 유저가 제출한 컨텐츠를 정제(sanitize)하여 XSS 공격 방지

기존 사내 프로그램들에서 유틸리티 클래스를 싱글톤으로 구현하였으므로 HTMLParser 또한 싱글톤으로 구현했습니다.
기존 유틸리티 클래스를 참고하여 작업하려 했으나 싱글톤 구현방식의 문제를 발견하여 아래와 같은 구조로 구현했습니다. 기존 싱글톤 클래스들의 문제점에 대해서는 따로 정리하여 업로드할 예정입니다.

아래는 HTMLProvider.java의 코드입니다


public final class HTMLProvider {

    private static final HTMLProvider INSTANCE = new HTMLProvider();
    private final Logger logger = LoggerFactory.getLogger(getClass());

    private HTMLProvider(){}

    private ResourceLoader resourceLoader = new DefaultResourceLoader();

    public static HTMLProvider getInstance(){
        return INSTANCE;
    }

    /**
    * html을 document 객체로 변환하여 리턴
     * @param HTMLFileNm 변환할 html 문서 이름 (확장자 없이)
     * @return doc document 객체
    * */
    private Document docBuilder(String HTMLFileNm) {

        //htmlSource : html 파일을 모아놓은 디렉토리의 classpath로부터의 상대경로
        //resource : html 파일의 리소스 descriptor
        //dir : html 파일의 경로
        //doc : html 파일을 파싱, DOM객체처럼 조작 가능한 Document 객체
        String htmlSource = "/static/";
        Resource resource = this.resourceLoader.getResource(htmlSource + HTMLFileNm + ".html");
        String dir = "";
        Document doc = null;

        try {
            dir = resource.getFile().getAbsolutePath();
            Path input = Path.of(dir);
            doc = Jsoup.parse(Files.readString(input), "UTF-8");
        } catch (IOException e) {
            logger.debug( htmlSource+"에 "+HTMLFileNm+".html 이 없습니다. 올바른 파일 위치를 확인해주세요.");
            throw new RuntimeException(e);
        }
        return doc;
    }

    /**
     * 사용자권한관리 > 비밀번호 초기화 시 이메일 폼 html 리턴.
     * @param title 제목
     * @param pwd 초기화 비밀번호
     * @return 비밀번호 초기화 시 이메일 폼 html
     * @throws Exception
     */
    public String getPwdResetForm(String title, String pwd){
        //form: 리턴할 HTML 문자열
        String form = null;

        //docBuilder(필요한 양식의 html 파일 이름)로 Document 객체 생성
        Document doc = docBuilder("resetForm");
        if (doc != null){
            //id 등으로 Document 객체를 조작
            Element mailTitle = doc.getElementById("mail_title");
            mailTitle.text(title);

            Element mailContent = doc.getElementById("mail_content");
            mailContent.text(pwd);

            form = doc.outerHtml();
        }
        return form;
    }
}

아래는 클라이언트에서 호출 예시입니다.

public void resetPWD(HttpServletRequest req, String pwd) throws Exception{
    String title = "교체할 제목";
    String pwd = getRandomPw(8, 15);
    //*********여기서 HTMLParser를 호출*********
    String mailForm = HTMLProvider.getInstance().getPwdResetForm(title, pwd);
....
}

위의 StringBuffer를 이용한 기존 코드에 비해 훨씬 깔끔해진 것을 확인할 수 있습니다.

클라이언트에서는 getPwdResetForm와 같이 각 양식의 html을 리턴하는 메서드만 불러 사용합니다. 물론 HTML은 별도의 디렉토리에 미리 작업된 채로 참조하는 경로에 존재해야 합니다.
제목, 내용, 날짜 등 각 양식에 맞게 변경될 내용은 알맞는 파라미터로 넘겨줍니다. 파라미터를 받아 Document를 조작하고 HTML을 반환합니다.

Document 객체는 Element를 상속하고 ElementNode를 상속하며 Clonable을 구현합니다.
https://jsoup.org/apidocs/org/jsoup/nodes/Document.html

HTML파일을 직접 가져와서 Document로 만드는 일은 docBuilder 메서드에서 담당합니다. docBuilder는 프라이빗 메서드로 양식을 조작하는 다른 메서드에 의해 호출되어 사용되어야만 합니다. 외부에서 docBuilder를 호출하여 사용할 수 없습니다. 이렇게 디자인한 이유는 다음과 같습니다.

  1. 각 양식마다 참조하는 HTML문서나 문서 안에서 동적으로 제어할 내용은 미리 정해져있으므로 클라이언트에서 호출 시 Document 객체를 직접 조작할 필요가 없습니다. Document 객체를 조작하는 건 HTMLParser 한 곳에서만 관여합니다. 만약 양식이 변경되는 등의 이유로 조작할 대상이 바뀐다면 각 양식을 조작하는 메서드의 내용만 수정되어야 합니다.
  2. HTML을 넘겨받기 위해 docBuilder 메서드를 클라이언트에서 매번 부를 필요가 없습니다. Document 객체는 무조건 한번은 생성되어야 하기 때문에 이를 호출 시마다 별도 처리하는 것은 비효율적입니다.

HTMLProvider는 Document를 인스턴스 변수로 놓고 getter를 쓰는 대신 docBuilder메서드를 이용해 넘겨받습니다. 싱글톤 구현 시 thread-safe하지 않은 환경에서 인스턴스 변수 사용은 최대한 지양해야 합니다. 더군다나 유틸리티 클래스는 자신만의 고유한 상태를 가지지 않으므로 인스턴스 변수를 가질 이유가 없습니다.

위처럼 HTMLProvider 유틸클래스를 이용하면 아래와 같은 이점을 가질 수 있습니다.

  • 코드의 가독성이 향상
  • 디자이너는 디자이너대로 HTML을 작성하고 개발자는 DOM요소로 조작만 신경쓰면 됨
  • HTML의 구조나 css 등이 변경되어도 디자이너는 하나의 HTML에서만 수정하면 되고 전체 기능을 다시 테스트할 필요 없음

3. 보완할 점

  • 작업을 하며 예상보다 어려웠던 부분은 HTML파일의 위치를 가져오는 것이었습니다. 최종적으로는 스프링 프레임워크의 ResourceLoader를 사용하여 클래스패스로부터 상대경로를 참조하였습니다. 즉 유틸리티클래스가 프레임워크의 인터페이스에 의존하게 되면서 유연성이 저하됩니다.
    상대경로를 참조하는 법에 관련해서는 따로 정리하여 업로드할 예정입니다.

  • 문서 양식이 변경될 때마다 메서드의 코드를 수정하는 것이 맞는지 의심이 듭니다. 좀 더 객체지향적으로 구성할 수 있으면 좋겠다는 생각이 듭니다.

profile
아류미의 개발기록

0개의 댓글