Java의 Reflection을 이용한 API

이미 완성되어 컴파일까지 되어 배포된 프로그램에 대한 기능 확장을 위해서 Reflection API를 사용할 수 있습니다. 이 글은 이 목적을 위해 일단 필요한 코드를 정리한 글 입니다.

Java에서 어떤 타입에 대한 이름을 문자열로만 알고 있을 때, 그 타입의 인스턴스를 생성하기 위한 코드는 다음과 같습니다.

try {
	Class<?> clazz = Class.forName("tstConstructor.service.FirstService");
	//Class<?> clazz = tstConstructor.service.FirstService.class;
	
	Constructor<?> constructor = clazz.getConstructor(String.class);
	Serviceable instance = (Serviceable)constructor.newInstance("Jack");
	instance.run();
} catch (Exception e) {
	e.printStackTrace();
}	

위의 코드에서 FirstService 클래스의 코드는 다음과 같습니다.

package tstConstructor.service;

public class FirstService implements Serviceable {
	private String _name;
	
	public FirstService(String name) {
		this._name = name;
	}
	
	@Override
	public void run() {
		System.out.println("Hello, my name is " + this._name);
	}
}

또한 Serviceable 인터페이스의 코드는 다음과 같습니다.

package tstConstructor.service;

public interface Serviceable {
	void run();
}

또한 Java에서 어떤 타입에 대한 이름을 문자열로만 알고 있을 때, 그 타입의 정적 필드값을 얻기 위한 코드는 다음과 같습니다.

try {
	Class<?> clazz = Class.forName("tstConstructor.service.Gizmo");
	//Field field = Gizmo.class.getField("NAME");
	Field field = clazz.getField("NAME");
	Class<?> fieldType = field.getType();
	if(fieldType == String.class){
		System.out.println(field.get(null).toString());
	} else if(fieldType == double.class){
	    System.out.println(field.getDouble(null)); 
	}
} catch (Exception e) {
	e.printStackTrace();
}

Gizmo 클래스의 코드는 다음과 같습니다.

package tstConstructor.service;

public class Gizmo {
	public static String NAME = "GIZMO";
}

DBMS 살펴보기

운영 중인 시스템에서 사용하는 DBMS를 살펴보는 방식은 해당 DBMS의 내용을 기술해 놓은 문서를 보거나 실제 DBMS에 저장된 데이터를 DB 툴을 통해 살펴보는 방식이 있습니다. 이 포스트는 운영중인 DBMS의 내용을 문서나 DB 툴 없이 java 코드를 통해 살펴보는 내용을 정리한 글입니다.

DBMS를 파악함에 있어서 관련 문서나 담당자의 설명 없이 해당 DBMS에 저장된 데이터를 통해 파악하는 것은 정확하지 않은 추측일 가능성이 큽니다. 관련 문서나 담당자의 설명과 DB 툴이나 이 글의 내용을 통해 DBMS에 실제로 저장된 데이터를 살펴보는 것 모두를 병행하여 코로스체킹(Cross Checking)을 해야 합니다.

흔히 가장 많이 사용하는 DBMS인 MySQL을 토대로 하였고 Java의 JDBC를 사용하였습니다. 테스트한 서버의 MySQL의 버전이 무엇인지는 모르겠으나 사용한 JDBC에 대한 jar 라이브러리는 mysql-connector-java-8.0.22.jar입니다.

이 프로그램의 결과는 해당 DBMS를 JDBC로 연결하고 해당 DB에 저장된 테이블 이름들을 가져온 후 각 테이블에 대해 최근에 저장된 단 3개의 필드값을 지정된 파일에 저장하는 것입니다. 테이블들이 많을 경우 제외할 테이블을 지정할 수 있습니다.

코드는 다음과 같습니다.

package tstMySQL;

import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class MainEntry {
    public static void main(String[] args) {
        File file = new File("d:/text.txt");
        PrintWriter writer = null;
			
        Connection con = null;
        PreparedStatement pstmt = null;   
        ResultSet rs = null;
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            con = DriverManager.getConnection("jdbc:mysql://localhost:3306/geos", "ID", "#PW");
            pstmt = con.prepareStatement("SHOW TABLES");
            rs = pstmt.executeQuery();
            ArrayList<String> tableNames = new ArrayList<String>();
            
            while(rs.next()) {
                String tableName = rs.getString(1);
                tableNames.add(tableName);
            }
            
            rs.close();
            pstmt.close();
            
            writer = new PrintWriter(new FileWriter(file));
            
            String[] ignoredTablesArray = { "etc", "abc" };
            Set<String> ignoredTables = new HashSet<String>(Arrays.asList(ignoredTablesArray));
            
            Iterator<String> iter = tableNames.iterator();
            while(iter.hasNext()) {
            	String tableName = iter.next();
            	if(ignoredTables.contains(tableName)) continue;
            	
            	writer.println("# TableName : " + tableName);
            
            	pstmt = con.prepareStatement("SELECT * FROM " + tableName + " ORDER BY ID DESC LIMIT 3");
            	rs = pstmt.executeQuery();
            	ResultSetMetaData rsmd = rs.getMetaData();        
                int fieldCount = rsmd.getColumnCount();

                while(rs.next()) {
                    for(int iField=0; iField<fieldCount; iField++) {
                        Object obj = rs.getObject(iField+1);
                        String value = obj == null ? "NULL" : obj.toString();
                        String fieldName = rsmd.getColumnName(iField+1);
                        writer.println("  " + fieldName + " : " + value);
                    }
                    writer.println();
                }

                rs.close();
                pstmt.close();
                            }
        } catch(Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if(rs != null) rs.close();
                if(pstmt != null)pstmt.close();
                if(con != null) con.close();
                if(writer != null) writer.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

27번 코드에 DBMS 연결을 위한 정보를 정확히 입력해야 합니다. 42번 코드는 파악할 필요가 없는 테이블의 이름으로 변경하면 되구요. 52번째 코드에서 각 테이블에 대해 살펴보고자 하는 SQL 문을 지정하고 있는데요. 여기서는 최근에 저장된 3개의 레코드만을 조회하고 있습니다. 이 부분은 DBMS의 테이블에 맞는 쿼리로 구성해야 합니다. 최종 결과는 19번 코드에서 지정한 D:/text.txt 파일로 저장됩니다.

JSON에 대한 Java 객체 직렬화, GSON

구글의 GSON은 JSON 데이터를 Java 객체로 생성해주는 라이브러리입니다.

GSON에 대한 jar 라이브러리를 참조(필자는 gson-2.3.1.jar를 사용)합니다. 먼저 첫번째 예제는 JSON 문자열 대한 객체 직렬화입니다.

String strJSON = "{'name': 'Dip2K', 'age': 44 }";

Gson gson = new GsonBuilder().create();
Person person = gson.fromJson(strJSON, Person.class);
System.out.println(person);

strJSON에 담긴 JSON 데이터를 Person이라는 객체로 생성하고 있습니다. Person은 새롭게 정의한 클래스로 다음과 같습니다.

package tstThread;

import java.util.ArrayList;
import java.util.HashMap;

public class Person {
    private String name;
    private int age;
	
    private Car car = new Car("TEST");
	
    public Person(String name, int age) {	
        this.name = name;
        this.age = age;
    }
	
    @Override
    public String toString() {
        return "[" + name + ", " + age + "]";
    }
}

실행 결과는 다음과 같습니다.

[Dip2K, 44]

다음은 자바 객체를 JSON 데이터로 역직렬화하는 코드예입니다.

Person person = new Person("Dip2K", 44);

Gson gson = new GsonBuilder().create();
String strJson = gson.toJson(person);

System.out.println(strJson);

위의 코드에서 사용된 Person 클래스는 정의는 다음과 같습니다.

package tstThread;

import java.util.ArrayList;
import java.util.HashMap;

public class Person {
    private String name;
    private int age;
	
    private Car car = new Car("TEST");
	
    private ArrayList<String> array = new ArrayList<String>();
    private HashMap<String, Integer> map = new HashMap<String, Integer>();
	
    public Person(String name, int age) {	
        this.name = name;
        this.age = age;
		
        array.add("Card1");
        array.add("Card2");
        array.add("Card3");
		
        map.put("KEY1", 100);
        map.put("KEY2", 200);
        map.put("KEY3", 300);
    }
	
    @Override
    public String toString() {
        return "[" + name + ", " + age + ", " + array.get(1) + "]";
    }
}

새롭게 정의된 Person를 보면 내부 필드 객체로 Car 클래스 타입의 객체를 하나 더 갖고 있는데, 이 Car 클래스는 다음과 같습니다.

package tstThread;

public class Car {
    private String name;
	
    public Car(String name) {
        this.name = name;
    }
}

이 예제는 단순한 클래스 객체 뿐만이 아나리 객체 안에 또 다른 객체가 담겨 있을때에 대한 GSON의 역직렬화가 가능하다는 것을 확인하기 위함입니다. 실행해 보면 다음과 같습니다.

{"name":"Dip2K","age":44,"car":{"name":"TEST"},"array":["Card1","Card2","Card3"],"map":{"KEY2":200,"KEY1":100,"KEY3":300}}

이제 위의 예제에서 얻는 JSON 문자열을 다시 Person 객체로 직열화하는 예제입니다.

String strJSON = "{'name':'Dip2K','age':44,'car':{'name':'TEST'},'array':['Card1','Card2','Card3'],'map':{'KEY2':200,'KEY1':100,'KEY3':300}}";

Gson gson = new GsonBuilder().create();
Person person = gson.fromJson(strJSON, Person.class);
System.out.println(person);

결과는 다음처럼 직열화가 잘된것을 확인할 수 있습니다.

[Dip2K, 44, Card2]

[Java] 두 문자열간의 유사도 구하기

두개의 문자열이 있을때, 얼마나 유사한지를 백분율의 개념인 0~1사이의 값으로 확인할 수 있을까? 즉 똑같은 문자열이라면 1을 전혀 다른 문자열이라면 0이라는 값으로 말이다. 구글링해보니 edit distance 계산을 통해 얻을 수 있단다. 가장 일반적은 구현체는 Levenshtein의 Distance Algorithm이라고 하고, 그 구현 함수는 다음과 같다. (출처: http://rosettacode.org/wiki/Levenshtein_distance#Java)

private double similarity(String s1, String s2) {
    String longer = s1, shorter = s2;
	
    if (s1.length() < s2.length()) {
        longer = s2; 
        shorter = s1;
    }
	
    int longerLength = longer.length();
    if (longerLength == 0) return 1.0;

    return (longerLength - editDistance(longer, shorter)) / (double) longerLength;
}

private int editDistance(String s1, String s2) {
	s1 = s1.toLowerCase();
    s2 = s2.toLowerCase();
    int[] costs = new int[s2.length() + 1];
    
    for (int i = 0; i <= s1.length(); i++) {
        int lastValue = i;
        for (int j = 0; j <= s2.length(); j++) {
            if (i == 0) {
            	costs[j] = j;
            } else {
                if (j > 0) {
                    int newValue = costs[j - 1];
                    
                    if (s1.charAt(i - 1) != s2.charAt(j - 1)) {
                    	newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1;
                    }
                    
                    costs[j - 1] = lastValue;
                    lastValue = newValue;
                }
            }
        }
        
        if (i > 0) costs[s2.length()] = lastValue;
    }
    
    return costs[s2.length()];
}

사용은 similarity 함수에 비교할 문자열 2개를 지정하면 비슷한 정도가 0~1 사이의 값으로 반환된다.