iTextPDF와 Commons-Net으로 PDF 생성 및 FTP 서버 업로드 기능 구현하기

Daniel·2024년 2월 21일
0

Back-End

목록 보기
33/42

들어가며

필자가 맡고있는 서비스에서 사용자 정보로 pdf 파일(계약서)을 생성해 이용기관 ftp 서버에 저장을 시켜줘야하는 기능을 추가로 개발해야하는 상황이였습니다.
우선 필자는 서버에 들어온 사용자 정보를 이용해 서버에서 pdf 파일을 생성 후 바로 이용기관의 ftp 서버로 보내주면 되지 않을까? 하는 가벼운 마음으로 시작했습니다.

기능을 구현하며 알게된 내용이나, 어려웠던 점을 기록하기위해 이 포스트를 작성합니다.

사용기술

  • iTextPDF (compile 'com.itextpdf:itextpdf:5.5.13.3')
    텍스트나 html 등의 문서를 pdf로 만들어주는 자바 라이브러리

  • Commons-Net (compile 'commons-net:commons-net:3.9.0')
    다양한 프로토콜(FTP, TFTP, SMTP, ...)에 대한 지원을 할 수 있도록 도와주는 자바 라이브러리
    클라이언트 모듈을 제공한다.

  • JAVA 1.8

  • Gradle 4.10.3

  • SpringBoot 2.1.5

구현 목표
서버로 들어온 사용자 정보를 이용해 pdf파일(계약서)을 생성 후 ftp 서버에 업로드 시킨다.

기술 소개

ITextPDF

iTextPDF는 자바에서 PDF 파일 생성 및 조작을 위한 오픈 소스 라이브러리입니다. 다양한 기능을 제공하며, PDF 파일 생성, 내용 수정, 템플릿 사용, 이미지 및 테이블 추가, 서명 및 암호화 등이 가능합니다.

주요 기능:

  • PDF 파일 생성 및 편집
  • 텍스트, 이미지, 테이블 등 다양한 콘텐츠 추가
  • 서식 및 스타일 설정
  • 폰트 임베딩
  • 서명 및 암호화
  • PDF 양식 처리
  • 바코드 생성
  • 페이지 번호 매기기
  • 머리글 및 바닥글 설정
  • 북마크 생성
  • 다국어 지원

장점:

  • 강력하고 다양한 기능
  • 사용하기 쉬운 API
  • 오픈 소스
  • 활발한 커뮤니티
  • 다양한 플랫폼 지원

활용 예시:

  • 보고서 및 문서 생성
  • 견적서 및 청구서 발행
  • 전자 계약서 작성
  • 제품 매뉴얼 제작
  • 폼 및 설문조사 생성
  • 명함 및 카드 제작
  • 데이터 시각화

Commons-Net

Commons-Net은 Apache에서 제공하는 자바 라이브러리로, FTP, SMTP, POP3, Telnet 등 다양한 네트워크 프로토콜을 지원합니다. FTP 서버 연결, 파일 업로드 및 다운로드, 디렉토리 관리 등의 기능을 제공합니다.

주요 기능:

  • FTP 서버 연결 및 관리
  • 파일 업로드 및 다운로드
  • 디렉토리 생성 및 삭제
  • 파일 이름 변경 및 삭제
  • 파일 정보 확인
  • 멀티 스레드 지원
  • 프록시 서버 지원
  • SSL/TLS 지원

장점:

  • 다양한 네트워크 프로토콜 지원
  • 사용하기 쉬운 API
  • 오픈 소스
  • 활발한 커뮤니티

활용 예시:

  • 파일 공유 및 업로드
  • 자동 백업 및 복구
  • 웹 서버 관리
  • 데이터 전송
  • 시스템 관리
  • 네트워크 모니터링

기능 구현

  • PDF 파일 생성:
    - iTextPDF Document 객체 생성
    - PDF 내용 추가 (텍스트, 이미지, 테이블 등)
    - PDF 파일 저장

  • FTP 서버 업로드:
    - Commons-Net FTPClient 객체 생성
    - FTP 서버 연결
    - PDF 파일 업로드
    - FTP 서버 연결 종료

코드 예시

필자는 위와같이 구성해보았습니다.

  • ContractSaveService
    계약서를 만들고, 업로드 시키는 비즈니스 로직을 수행하기위한 클래스

  • FtpFileSender
    Ftp 연결을 하고 업로드시켜주는 클래스

  • Controller
    사용자 정보를 받아 비즈니스 로직(ContractSaveService)을 호출한다.

  • PdfMaker
    Pdf파일에 내용을 채워 최종 업로드 될 pdf 파일을 만드는 클래스

PdfMaker : pdf 파일을 생성 하는 객체

아래의 메서드를 통해 Document 객체 생성과 PDF 내용을 추가해주었습니다.

public byte[] createPdf(Map<String, Object> data) throws IOException, DocumentException {
	
	try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {   // ByteArrayOutputStream -> AutoCloseable 하므로 try-with-resources 사용
		
		// PDF 파일 자체를 나타내는 Document 객체 초기화
	    Document document = new Document(PageSize.A4, 5, 5, 10, 10);  
	    
	    // Document 객체와 ByteArrayOutputStream 을 연결하여 PDF 파일을 메모리에서 생성하도록 함
	    PdfWriter.getInstance(document, out);  
	    
	    // Document 객체를 열고 작성 후 닫음
	    document.open();  
	    document.add(contractMaker(data));  
	    document.close();  
	    
	    // PDF 파일을 FTP 서버로 보낼 수 있도록 PDF 파일의 내용이 저장된 바이트 배열 반환
	    return out.toByteArray();  
	}
}

PDF 내용추가 상세코드(예시)

private PdfPTable contractMaker(Map<String, Object> data) throws DocumentException, IOException {  
	
    // 문서에 사용될 폰트 설정  
    BaseFont baseFont = BaseFont.createFont("/fonts/malgun.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);  
    BaseFont boldFont = BaseFont.createFont("/fonts/malgunbd.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);  
    Font font = new Font(baseFont, 7);  
    Font bold = new Font(boldFont, 15);  
    
    // PdfPTable 을 사용해 계약서를 그리기위해 객체 초기화 PdfPTable(컬럼수) = 8개의 컬럼셀을 가진 테이블
    PdfPTable table = new PdfPTable(8);  
    table.setWidthPercentage(90);  // 페이지에서 테이블이 차지할 너비 비율을 설정
	
	// 셀 초기화
	PdfPCell cell = new PdfPCell(new Phrase("A", font));
	table.addCell(cell); // 테이블에 초기화된 셀 입력
	table.completeRow(); // 테이블에 입력된 셀들을 가지고 행 마무리
	
    return table;  
}

위코드는 PDF에 테이블을 그리는 예시 입니다. (실제 코드는 비즈니스와 관련이 있어 예시 코드로 대체했습니다.)
PdfPTable, PdfPCell 클래스의 메서드를 사용하시면 더 멋진 테이블을 완성하실 수 있습니다.

FtpFileSender : FTP와 관련된 기능을 수행하는 객체(연결설정, 업로드)

  • 기능
    FTP 서버와의 연결을 한다.
    pdf 파일을 업로드 한다.
package com.imax.biz.controller.kakaoCert;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;

import org.apache.commons.net.PrintCommandListener;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPConnectionClosedException;
import org.apache.commons.net.ftp.FTPReply;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class FtpFileSender {
	
	private static final FTPClient ftpClient = new FTPClient();
	
	@Value("${FTP.user}")
	private String user;
	@Value("${FTP.pw}")
	private String pw;
	@Value("${FTP.addr}")
	private String addr;
	
	// FTP 서버 연결
	public void open() throws IllegalAccessException {
		
		ftpClient.setControlEncoding("UTF-8"); // 파일 인코딩 설정
		ftpClient.addProtocolCommandListener(
			new PrintCommandListener(new PrintWriter(System.out), true)); // 연결할 때 일반적인 응답 출력용
			
		try {
			ftpClient.connect(addr);
			
			// 정상적으로 연결이 되었는지?
			int reply = ftpClient.getReplyCode();
			if (!FTPReply.isPositiveCompletion(reply)) {
				ftpClient.disconnect();
			}
			
			// 타임아웃 설정
			ftpClient.setSoTimeout(30000);
			
			ftpClient.enterLocalPassiveMode();
			// 로그인 정보
			ftpClient.login(user, pw);
			
			// 파일 타입 설정(default FTP.ASCII_FILE_TYPE)
			ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
			
			boolean temp = ftpClient.changeWorkingDirectory("/MIS/Kakao/InterTalk/");
			
		} catch (IOException e) {
			throw new IllegalAccessException("FTP 연결 설정 중 오류가 발생했습니다.");
		}
	}
	
	// FTP 서버 연결 후 파일 업/다운 로드 후 서버 연결 종료
	public void close() throws IllegalAccessException {
		try {
			ftpClient.logout();
			ftpClient.disconnect();
		} catch (IOException e) {
			throw new IllegalAccessException("FTP 연결 해제 중 오류가 발생했습니다.");
		}
	}
	
	// 파일 업로드
	public boolean upload(String remoteFilePath, byte[] pdfBytes) throws IllegalAccessException, IOException {
		open();
		ftpClient.enterLocalPassiveMode();
		
		String pwd = ftpClient.printWorkingDirectory();
		String uploadFolder = remoteFilePath.substring(0, 6);
		
		if (!pwd.substring(pwd.length() - 6).equals(uploadFolder)) {
			// 현재 디렉토리가 uploadFolder  아니면 현재월 디렉토리 만들고 그 안에 업로드
			ftpClient.makeDirectory(uploadFolder);
			ftpClient.changeWorkingDirectory(uploadFolder);
		}
		
		// 현재 디렉토리가 uploadFolder 이고 현재 월이 맞다면? 업로드
		System.out.println("업로드 시작");
		return execute(remoteFilePath, pdfBytes);
	}
	
	private boolean execute(String remoteFilePath, byte[] pdfBytes) throws IllegalAccessException {
		try (InputStream inputStream = new ByteArrayInputStream(pdfBytes)) {
			
			OutputStream outputStream = ftpClient.storeFileStream(remoteFilePath);
			
			if (outputStream != null) {
				// Set buffer size (e.g. 8192)
				int bufferSize = 8192;
				byte[] buf = new byte[bufferSize];
				int bytesRead;
				while ((bytesRead = inputStream.read(buf)) != -1) {
					outputStream.write(buf, 0, bytesRead);
				}
				outputStream.close();
				System.out.println("업로드 성공" + remoteFilePath);
				return true;
			} else {
				System.out.println("업로드 실패" + remoteFilePath);
				return false;
			}
		} catch (FTPConnectionClosedException e) {
			System.err.println("FTP 서버 연결중 오류 " + e.getMessage());
			return false;
		} catch (IOException e) {
			System.err.println("업로드 하는 중 오류 " + e.getMessage());
			return false;
		} finally {
			close();
		}
	}

} // end class

FTP의 접속정보 같은 경우 .properties 파일에서 가져와 설정 해주었습니다.

open()
연결설정을 위한 메서드입니다.
FTP 서버와의 연결을 수행합니다.

close()
사용했으면 닫아줘야겠죠? FTP 서버와의 연결을 닫아주기위한 메서드입니다.

upload(String remoteFilePath, byte[] pdfBytes)
저장될 FTP서버의 경로, 문서의 byte 를 파라미터로 받아
원하는 디렉토리 경로 확인 및 생성 후
.excute() 메서드를 통해 FTP서버의 해당경로에 byte 데이터를 Stream을 사용해 써줍니다.(업로드)

execute(String remoteFilePath, byte[] pdfBytes)
실제 업로드를 수행하는 메서드

막혔던 부분

Active / Passive 모드

FTP 서버에 pdf 문서를 저장하기위해 .storeFileStream() 메서드를 사용했지만 시도할 때마다 타임아웃이 나올때까지 동작하지 않는 문제가 있었습니다.

원인은 mode 설정이였습니다.

FTP 서버에 연결할 때 사용할 수 있는 두 가지 모드는 Active ModePassive Mode 입니다.

  • Active Mode : 클라이언트의 접속 요청 / 서버의 데이터 채널 연결

    	1. 클라이언트가 서버에 접속 요청을 보냅니다.
    	2. 클라이언트는 데이터 전송을 위해 임의의 포트를 열고 서버에 알립니다.
    	3. 서버는 클라이언트의 IP 주소와 포트 번호를 사용하여 데이터 연결을 요청합니다.
       
    	   장점
    		   서버 설정이 간단합니다.
    		   방화벽 설정이 용이합니다.
    
    	   단점
    		   클라이언트가 임의 포트를 사용하기 때문에 방화벽에 의해 차단될 가능성이 높습니다.
    		   클라이언트 IP 주소가 공개되어야 합니다.
  • Passive Mode : 접속과 데이터채널의 연결을 모두 클라이언트가 수행

    	1. 클라이언트가 서버에 접속 요청을 보냅니다.
    	2. 서버는 임의의 포트를 열고 클라이언트에게 알립니다.
    	3. 클라이언트는 서버의 IP 주소와 포트 번호를 사용하여 데이터 연결을 요청합니다.
    
    	장점
    		방화벽에 의해 차단될 가능성이 낮습니다.
    		클라이언트 IP 주소가 공개되지 않습니다.
    
    	단점
    		서버 설정이 더 복잡할 수 있습니다.
    		서버에는 사용하는 임의 포트를 알아야 하기 때문에 방화벽 설정이 더 복잡할 수 있습니다.

Active mode의 경우 위에서 설명한데로 데이터 채널을 서버가 요청하기 때문에
기본 포트인 20(변경을 하였다면 변경된 데이터 채널 포트) 이 서버쪽에는 아웃바운드, 클라이언트 쪽에는 인바운드 설정이 되어 있어야 합니다. 

Passive mode의 경우 데이터 채널을 클라이언트가 임의로 지정하게 됩니다. (1024 ~ 65535 사이의 사용 가능한 포트)
혹은 포트의 범위를 설정할 수 있습니다. 그렇게 되면 해당 범위 내의 포트가 FTP 서버에 인바운드 설정이 되어 있어야 합니다.

profile
응애 나 애기 개발자

0개의 댓글