본문 바로가기

Java

[Java] 고정 길이 레코드 처리 유틸리티

반응형

(언젠가 필요한 순간에 빠르게 찾으려고 작성합니다.)

 

이 코드는 고정 길이 레코드 처리 유틸리티입니다.

주로 레거시 시스템 연동, 금융권 전문 통신, EDI(전자문서교환) 시스템에서 활용되는 코드로 사용될 것 같아요.

 

이 코드를 **"데이터 변환 계층(Data Transformation Layer)"**으로 보면 이해하기 쉽습니다.

  • 입력: 가변 길이 문자열 (사람이 읽기 쉬운 형태)
  • 출력: 고정 길이 바이트 레코드 (시스템이 처리하기 쉬운 형태)

 

아래와 같은 경우에 활용할 수 있을것 같네요. :)

1. 금융권 전문 통신

계좌번호: "1234567890" → 20바이트 고정 길이로 패딩
거래금액: "1500000" → 15바이트 우측 정렬 + 제로 패딩

2. EDI 시스템 연동

상품 정보를 고정 포맷으로 변환하여 파트너사와 데이터 교환

3. 레거시 메인 프레임 연동

COBOL 시스템과의 데이터 호환성 확보

 

 

import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.lang.reflect.Array;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 고정 길이 레코드 처리 유틸리티 클래스
 * 
 * 주요 용도:
 * - 금융권 전문 통신 데이터 처리
 * - EDI(전자문서교환) 시스템 연동
 * - 레거시 메인프레임 시스템과의 데이터 호환
 * 
 * @author YourName
 * @version 2.0
 */
public class FixedLengthRecordUtil {
    
    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FixedLengthRecordUtil.class);
    
    // 상수 정의로 매직 넘버 제거
    public static final String DEFAULT_CHARSET_UTF8 = "UTF-8";
    public static final String DEFAULT_CHARSET_EUC_KR = "EUC-KR";
    public static final char SPACE_CHAR = ' ';
    public static final char ZERO_CHAR = '0';

    /**
     * 바이트 길이만큼 자르고 공백으로 우측 패딩
     * 
     * 예시: "Hello" → "Hello     " (10바이트 기준)
     *
     * @param input 입력 문자열
     * @param byteLength 목표 바이트 길이
     * @param charset 문자 인코딩 (UTF-8: 한글 3byte, EUC-KR: 한글 2byte)
     * @return 고정 길이로 패딩된 문자열
     * @throws UnsupportedEncodingException 지원하지 않는 인코딩
     */
    public static String byteSubstringWithPadding(String input, int byteLength, String charset) 
            throws UnsupportedEncodingException {
        
        // 입력값 검증
        if (byteLength < 0) {
            throw new IllegalArgumentException("바이트 길이는 0 이상이어야 합니다.");
        }
        
        if (isEmpty(input)) {
            return createPaddedString("", byteLength, SPACE_CHAR);
        }

        // 개행 문자 제거
        input = input.replaceAll(System.getProperty("line.separator"), "");

        int actualByteLength = input.getBytes(charset).length;

        if (actualByteLength < byteLength) {
            // 부족한 길이만큼 공백 패딩
            int paddingLength = byteLength - actualByteLength;
            return input + createPaddedString("", paddingLength, SPACE_CHAR);
            
        } else if (actualByteLength > byteLength) {
            // 바이트 길이 초과 시 안전하게 자르기
            return trimToByteLength(input, byteLength, charset);
            
        } else {
            // 정확히 일치하는 경우
            return input;
        }
    }

    /**
     * 바이트 범위로 문자열 자르기 (startIndex < x <= endIndex)
     * 
     * @param input 입력 문자열
     * @param startIndex 시작 바이트 인덱스 (포함하지 않음)
     * @param endIndex 종료 바이트 인덱스 (포함)
     * @param charset 문자 인코딩
     * @return 지정된 바이트 범위의 문자열
     * @throws UnsupportedEncodingException 지원하지 않는 인코딩
     */
    public static String byteSubstring(String input, int startIndex, int endIndex, String charset) 
            throws UnsupportedEncodingException {
        
        if (isEmpty(input)) {
            return "";
        }
        
        if (startIndex < 0 || endIndex < startIndex) {
            throw new IllegalArgumentException("잘못된 인덱스 범위입니다.");
        }

        StringBuilder result = new StringBuilder();
        int currentBytePos = 0;
        
        for (char ch : input.toCharArray()) {
            int charByteLength = String.valueOf(ch).getBytes(charset).length;
            currentBytePos += charByteLength;

            if (currentBytePos > startIndex && currentBytePos <= endIndex) {
                result.append(ch);
            }
            if (currentBytePos > endIndex) {
                break;
            }
        }

        return result.toString().trim();
    }

    /**
     * 레코드 포맷 길이 검증
     * 
     * @param record 검증할 레코드
     * @param expectedLength 예상 길이
     * @param charset 문자 인코딩
     * @return 포맷이 올바르면 true, 그렇지 않으면 false
     * @throws UnsupportedEncodingException 지원하지 않는 인코딩
     */
    public static boolean isValidRecordFormat(String record, int expectedLength, String charset) 
            throws UnsupportedEncodingException {
        
        if (isEmpty(record)) {
            return expectedLength == 0;
        }
        
        return record.getBytes(charset).length == expectedLength;
    }

    /**
     * 파일 존재 여부 확인
     * 
     * @param filePath 파일 경로
     * @return 파일이 존재하고 읽기 가능하면 true
     */
    public static boolean isFileExists(String filePath) {
        if (isEmpty(filePath)) {
            return false;
        }
        
        Path path = Paths.get(filePath);
        return Files.exists(path) && Files.isReadable(path);
    }

    /**
     * 파일 내용을 라인별로 읽기
     * 
     * @param filePath 파일 경로
     * @param charset 문자 인코딩 (기본값: EUC-KR)
     * @return 파일 내용 리스트
     * @throws Exception 파일 읽기 실패
     */
    public static List<String> readFileLines(String filePath, String charset) throws Exception {
        if (!isFileExists(filePath)) {
            throw new IllegalArgumentException("파일이 존재하지 않거나 읽을 수 없습니다: " + filePath);
        }

        try {
            Path path = Paths.get(filePath);
            String charsetName = isEmpty(charset) ? DEFAULT_CHARSET_EUC_KR : charset;
            
            return Files.lines(path, Charset.forName(charsetName))
                    .collect(Collectors.toList());
                    
        } catch (Exception e) {
            log.error("파일 읽기 오류 - 경로: {}, 오류: {}", filePath, e.getMessage(), e);
            throw new Exception("파일 읽기 중 오류가 발생했습니다: " + e.getMessage(), e);
        }
    }

    /**
     * 파일 내용 읽기 (기본 EUC-KR 인코딩)
     */
    public static List<String> readFileLines(String filePath) throws Exception {
        return readFileLines(filePath, DEFAULT_CHARSET_EUC_KR);
    }

    /**
     * 객체 null/empty 검사
     * 
     * @param obj 검사할 객체
     * @return null 또는 empty면 true
     */
    public static boolean isEmpty(Object obj) {
        if (obj == null) {
            return true;
        }

        if (obj instanceof CharSequence) {
            return ((CharSequence) obj).length() == 0;
        }
        if (obj.getClass().isArray()) {
            return Array.getLength(obj) == 0;
        }
        if (obj instanceof Collection) {
            return ((Collection<?>) obj).isEmpty();
        }
        if (obj instanceof Map) {
            return ((Map<?, ?>) obj).isEmpty();
        }

        return false;
    }

    /**
     * 안전한 문자열 → Integer 변환
     * 
     * @param input 변환할 문자열
     * @param defaultValue 변환 실패 시 기본값
     * @return 변환된 정수값
     */
    public static Integer safeStringToInt(String input, Integer defaultValue) {
        if (isEmpty(input) || isEmpty(input.trim())) {
            return defaultValue;
        }
        
        try {
            return Integer.parseInt(input.trim());
        } catch (NumberFormatException e) {
            log.warn("정수 변환 실패 - 입력값: '{}', 기본값 사용: {}", input, defaultValue);
            return defaultValue;
        }
    }

    /**
     * 안전한 문자열 → Integer 변환 (기본값 0)
     */
    public static Integer safeStringToInt(String input) {
        return safeStringToInt(input, 0);
    }

    /**
     * 안전한 문자열 → Long 변환
     * 
     * @param input 변환할 문자열
     * @param defaultValue 변환 실패 시 기본값
     * @return 변환된 Long 값
     */
    public static Long safeStringToLong(String input, Long defaultValue) {
        if (isEmpty(input) || isEmpty(input.trim())) {
            return defaultValue;
        }
        
        try {
            return Long.parseLong(input.trim());
        } catch (NumberFormatException e) {
            log.warn("Long 변환 실패 - 입력값: '{}', 기본값 사용: {}", input, defaultValue);
            return defaultValue;
        }
    }

    /**
     * 안전한 문자열 → Long 변환 (기본값 0L)
     */
    public static Long safeStringToLong(String input) {
        return safeStringToLong(input, 0L);
    }

    /**
     * 숫자 문자열 여부 검사 (개선된 버전)
     * 
     * @param input 검사할 문자열
     * @return 숫자로 변환 가능하면 true
     */
    public static boolean isNumeric(String input) {
        if (isEmpty(input) || isEmpty(input.trim())) {
            return false;
        }
        
        try {
            Double.parseDouble(input.trim());
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    /**
     * 에러 메시지 포매팅 (가변 인자 지원)
     * 
     * @param messageTemplate 메시지 템플릿
     * @param args 가변 인자
     * @return 포매팅된 메시지
     */
    public static String formatErrorMessage(String messageTemplate, Object... args) {
        if (isEmpty(messageTemplate)) {
            return "알 수 없는 오류가 발생했습니다.";
        }
        
        try {
            return String.format(messageTemplate, args);
        } catch (Exception e) {
            log.warn("메시지 포매팅 실패 - 템플릿: '{}', 인자: {}", messageTemplate, Arrays.toString(args));
            return messageTemplate; // 포매팅 실패 시 원본 반환
        }
    }

    // === 추가된 유틸리티 메서드들 ===

    /**
     * 숫자를 고정 길이로 제로 패딩
     * 
     * 예시: leftPadWithZero(123, 7) → "0000123"
     * 
     * @param number 숫자
     * @param totalLength 총 길이
     * @return 제로 패딩된 문자열
     */
    public static String leftPadWithZero(long number, int totalLength) {
        return String.format("%0" + totalLength + "d", number);
    }

    /**
     * 문자열을 지정된 문자로 우측 패딩
     * 
     * @param input 입력 문자열
     * @param totalLength 총 길이
     * @param paddingChar 패딩 문자
     * @return 패딩된 문자열
     */
    public static String rightPadWith(String input, int totalLength, char paddingChar) {
        if (isEmpty(input)) {
            input = "";
        }
        
        if (input.length() >= totalLength) {
            return input.substring(0, totalLength);
        }
        
        return input + createPaddedString("", totalLength - input.length(), paddingChar);
    }

    /**
     * 바이트 길이 기준으로 문자열 안전하게 자르기
     */
    private static String trimToByteLength(String input, int maxByteLength, String charset) 
            throws UnsupportedEncodingException {
        
        StringBuilder result = new StringBuilder();
        int currentByteLength = 0;
        
        for (char ch : input.toCharArray()) {
            int charByteLength = String.valueOf(ch).getBytes(charset).length;
            
            if (currentByteLength + charByteLength > maxByteLength) {
                break;
            }
            
            result.append(ch);
            currentByteLength += charByteLength;
        }
        
        // 남은 공간을 공백으로 채우기
        int remainingBytes = maxByteLength - currentByteLength;
        if (remainingBytes > 0) {
            result.append(createPaddedString("", remainingBytes, SPACE_CHAR));
        }
        
        return result.toString();
    }

    /**
     * 지정된 문자로 채워진 문자열 생성
     */
    private static String createPaddedString(String base, int paddingLength, char paddingChar) {
        if (paddingLength <= 0) {
            return base;
        }
        
        StringBuilder sb = new StringBuilder(base);
        for (int i = 0; i < paddingLength; i++) {
            sb.append(paddingChar);
        }
        return sb.toString();
    }
}
반응형

'Java' 카테고리의 다른 글

[Java] 스레드 일시정지, Thread sleep  (0) 2020.10.05
[Java] String 문자열 자르기 : substring() 이용  (0) 2020.10.05

❥ CHATI Github