Java로 메일 보내기

예전에 Go언어로 메일을 보내는 코드를 작성해 활용한 적이 있었는데, Java로도 가능하다. 당연히! 사용한 라이브러리는 JavaMail이고 이번에 사용한 버전은 1.6.2이다.

전체 코드는 다음과 같다.

package sendMail;

import java.util.Properties;

import javax.mail.Message;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;


public class MainEntry {
    static final String FROM = "GOODMAN@geoservice.co.kr";
    static final String FROMNAME = "지오서비스";
    static final String TO = "heyMan@naver.com";

    static final String SMTP_USERNAME = "GOODMAN@geoservice.co.kr";
    static final String SMTP_PASSWORD = "****";
    
    static final String HOST = "smtp.live.com";
    static final int PORT = 25;
    
    static final String SUBJECT = "메일 제목";
    
    static final String BODY = String.join(
    	System.getProperty("line.separator"),
    	"

메일 내용

", "

이 메일은 아름다운 사람이 보낸 아름다운 메일입니다!

." ); public static void main(String[] args) throws Exception { Properties props = System.getProperties(); props.put("mail.transport.protocol", "smtp"); props.put("mail.smtp.port", PORT); props.put("mail.smtp.starttls.enable", "true"); props.put("mail.smtp.auth", "true"); Session session = Session.getDefaultInstance(props); MimeMessage msg = new MimeMessage(session); msg.setFrom(new InternetAddress(FROM, FROMNAME)); msg.setRecipient(Message.RecipientType.TO, new InternetAddress(TO)); msg.setSubject(SUBJECT); msg.setContent(BODY, "text/html;charset=euc-kr"); Transport transport = session.getTransport(); try { System.out.println("Sending..."); transport.connect(HOST, SMTP_USERNAME, SMTP_PASSWORD); transport.sendMessage(msg, msg.getAllRecipients()); System.out.println("Email sent!"); } catch (Exception ex) { ex.printStackTrace(); } finally { transport.close(); } } }

실행해 보고, 메일이 왔나 가밨더니 아래처럼 와있더라.. 북치고 장구치고~ ^^

mybatis를 통한 Select, Insert, Delete

과거에 mybatis 라이브러리 사용하기라는 제목으로 mybatis를 이용하기 위한 프로젝트 설정과 간단한 SELECT 문을 호출해 그 결과를 살펴보았습니다. 이번에 실제 프로젝트에 mybatis를 사용하기로 결정하여 좀더 구체적인 SQL 문을 호출하는 API를 파악하여 정리해 봅니다.

먼저 사용하는 테이블의 구조는 다음과 같습니다. (헐! 그림 대따 크게 표시되네.. -_-;)

앞서 언급해 드린 mybatis 라이브러리 사용하기를 통해 프로젝트를 구성하고, SQL 문을 정의하는 sql.xml 파일을 아래처럼 구성합니다.

<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="test">
    <select id="getFirstPlayerName" resultType="string">
        SELECT name FROM players LIMIT 1
    </select>
    
    <select id="getFirstPlayer" resultType="map">
        SELECT name, age FROM players LIMIT 1
    </select>
    
    <select id="getPlayers" resultType="map">
        SELECT name, age FROM players
    </select>
    
    <select id="getPlayersWhere" resultType="map">
        SELECT name, age FROM players WHERE age > #{age}
    </select>
    
    <select id="getPlayersWhere2" resultType="map">
        SELECT name, age FROM players WHERE age > #{age} AND name LIKE #{name}
    </select>
    
    <insert id="insertPlayer"> 
    	INSERT INTO players (name, age) VALUES (#{name}, #{age}) 
    </insert>
    
    <delete id="deletePlayerWhere"> 
    	DELETE FROM players WHERE name = #{name} 
    </insert>    
</mapper>

보시는 것처럼 SELECT 문에 대한 getFirstPlayerName, getFirstPlayer, getPlayers, getPlayersWhere, getPlayersWhere2과 INSERT 문에 대한 insertPlayer, DELETE 문에 대한 deletePlayerWhere가 정의되어 있습니다. 이 7개에 대한 SQL문을 실행하는 API의 설명을 통해 mybatis에서 SELECT, INSERT, DELETE 문의 호출을 정리해 보겠습니다.

먼저 getFirstPlayerName은 테이블의 첫번째 Row에 대한 name 필드값을 가져오는 것인데, 관련된 API는 다음과 같습니다. 반환값이 string이고 반환결과는 1건 이하이므로 String과 SqlSession 객체의 selectOne 함수를 사용합니다.

String result = session.selectOne("test.getFirstPlayerName");
// 홍길동

다음은 getFirstPlayer인데, 반환결과는 역시 1건 이하인데 반환 필드가 name과 age로 2개입니다. API는 다음과 같습니다.

Map result = session.selectOne("test.getFirstPlayer");
 // {name=홍길동, age=18}

다음은 getPlayers로 반환 결과는 0건 이상이며 API는 다음과 같습니다.

List<Map<String, Object>> result = session.selectList("test.getPlayers");
// [{name=홍길동, age=18}, {name=임꺽정, age=32}, {name=일지매, age=21}]

getPlayersWhere는 조건 결과가 지정된 것으로 사용하는 API는 다음과 같습니다.

List<Map<String, Object>> result = session.selectList("test.getPlayersWhere", 20);
// [{name=임꺽정, age=32}, {name=일지매, age=21}]

getPlayersWhere2는 getPlayersWhere처럼 조건이 지정되어 있지만 비교 조건으로 사용되는 변수가 2개입니다. 사용하는 API는 다음과 같습니다.

Map param = new HashMap<>(); 
param.put("age", 20); 
param.put("name", "일%");

List<Map<String, Object>> result = session.selectList("test.getPlayersWhere2", param);
// [{name=일지매, age=21}]

insertPlayer는 데이터를 추가하는 것으로 API는 다음과 같습니다.

Map<String, Object> param = new HashMap<>(); 
param.put("age", 41); 
param.put("name", "황장군");

int result = session.insert("test.insertPlayer", param); // 1
session.commit();

deletePlayerWhere는 지정된 조건과 매칭되는 데이터를 삭제하는 것으로 사용하는 API는 다음과 같습니다.

Map<String, Object> param = new HashMap<>(); 
param.put("name", "황장군");

int result = session.delete("test.deletePlayerWhere", param);  // 1
session.commit();

mybatis는 별도의 xml 파일을 통해 SQL 문의 정의 부분을 독립적으로 유지할 수 있으며, SQL문을 보다 유연하게 처리할 수 있다는 문법 구문을 제공합니다. 또한 mybatis는 데이터베이스 연결시 발생하는 부하를 줄이기 위한 Connection Pool을 자체적으로 제공하고 있습니다. 이외에 JDBC를 직접 사용함에 따라 발생하는 복잡한 try .. catch .. finally 코드 부분이 제거되어 코드의 가독성이 향상됩니다.

[Java] 고유값 만들기

고유한 값으로 사용할 만한 것은 UUID이다. UUID는 그 의미가 가지는 것 그대로 고유한 값이다. 그러나 저장소의 공간은 128 bits이며 문자열로 표현될 때 550e8400-e29b-41d4-a716-446655440000 처럼 그 길이가 상당히 길다. 물론 UUID는 이 기종의 하드웨어나, 시간 또는 공간적으로 다른 환경 모두에서 고유한 값을 제공해 주는 장점이 있다. 그런데, 만약 하나의 서버 내부에서 고유한 값이 필요하다면 어떨까? 한계는 있겠으나, 필자에게는 적합한 방법 중에 하나는 서버 측의 시간을 활용하는 것이다. Java에서 서버의 시간을 얻는 함수는 System.currentTimeMillis, 서버가 기동 된 후의 경과된 시간은 System.nanoTime를 통해 얻을 수 있다. 그 결과는 long형이므로 64 Bits이며, 문자열로 표현하면 대부분 21220887433392와 같이 14개면 표현이 가능하다. 그러나 나에게 고유값이 필요한 목적은 저장소에 저장할 필요는 없고 문자열로 표현했을때, 최대한 짧아야 한다는 것이다. 그런데 이 long 타입을 문자열로 표현하면 14개의 자리를 차지하는 것은 너무 길다. 짧게 표현하는 방법은 없을까? 이 long 값은 10진수이다. 이를 좀 더 높은 진수로 표현한다면 그 길이는 줄어들 것이다. 16진수, 32진수, 64진수, 128진수 등이 가능한데.. 표현 가능한 문자여야 하므로 64진수가 최적이다.

이러한 상황에서 아래의 코드 중 longToBase64라는 함수는 long 타입의 값을 받아 64진수로 진수 변환 방식을 통해 문자열로 변환해 주는 함수이다.

public static String longToBase64(long v) {
    final char[] digits = {
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 
        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 
        'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 
        'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D',
        'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 
        'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 
        'Y', 'Z', '#', '$'
    };

    int shift = 6;
    char[] buf = new char[64];
    int charPos = 64;
    int radix = 1 << shift;
    long mask = radix - 1;
    long number = v;

    do {
        buf[--charPos] = digits[(int) (number & mask)];
        number >>>= shift;
    } while (number != 0);

    return new String(buf, charPos, (64 - charPos));
}

아래의 코드는 위의 함수를 테스트한 것으로, 1000개의 고유한 값을 얻는 기능을 수행한다.

for(int i=0; i<1000; i++) {
    long time =  System.nanoTime();
    System.out.println(time + " -> " + longToBase64(time));
}

위의 코드의 결과는 아래와 같은데, 실행하는 매 순간마다 다르게 표시될 것이다.

...

21767297606161 -> 4YMo2G8h
21767297618803 -> 4YMo2JdP
21767297627494 -> 4YMo2LlC
21767297634210 -> 4YMo2M#y
21767297647642 -> 4YMo2Qgq
21767297654358 -> 4YMo2RVm
21767297661469 -> 4YMo2TEt
21767297668185 -> 4YMo2Vhp
21767297676877 -> 4YMo2Xpd
21767297683593 -> 4YMo2Z29
21767297690704 -> 4YMo2#Ng
21767297697420 -> 4YMo30qc
21767297704531 -> 4YMo329j
21767297718753 -> 4YMo35Dx
21767297727445 -> 4YMo37Ll
21767297741272 -> 4YMo3b7o
21767297748383 -> 4YMo3cSv
21767297755099 -> 4YMo3evr
21767297762210 -> 4YMo3gey
21767297776037 -> 4YMo3jCB
21767297783148 -> 4YMo3llI

...

[Java] Thumbnail Image 만드는 코드

코드 정리하다가 블로그에 정리되지 않은 코드가 있어 정리해 봅니다. 일반적인 이미지를 작은 이미지, 즉 썸네일 이미지로 만들어 주는 코드입니다. 이미지의 종횡비 크기를 유지하며 썸네일 이미지를 만들어 줍니다. 썸네일 이미지 만드는 코드를 실제 프로젝트에 붙이기 위해 만들다 보니, 부가적인 내용이 덧붙여 있으니 참고하시기 바랍니다.

사용은 아래처럼 하면 됩니다. 즉, d:/a.jpg 파일을 썸네일 이미지를 만드는데, 가로 또는 세로 크기 중 하나를 256을 만듭니다.

reateThumbnail("d:/a.jpg", 256);

아래는 d:/a.jpg의 실제 이미지입니다. 이미지 크기는 920×600입니다.

위의 이미지가 위의 코드를 통해 아래와 같이 d:/a.thumbnail.jpg 파일로 만들어지고 크기는 256×166이 됩니다. 원본 이미지의 가로 크기가 세로보다 길다보니 가로를 256으로 만들어지게 되는 것입니다.

createThumbnail 함수는 아래와 같습니다.

private static boolean createThumbnail(String fileName, int maxSize) {
    try {
        int thumbnail_width = maxSize;
        int thumbnail_height = maxSize;

        File origin_file_name = new File(fileName);
		    
        String ext = getFileExt(fileName);
        String newFileName = fileName.replace("." + ext, ".thumbnail." + ext);
		    
        BufferedImage buffer_original_image = ImageIO.read(origin_file_name);
		    
        double imgWidth = buffer_original_image.getWidth();
        double imgHeight = buffer_original_image.getHeight();
		    
        if(imgWidth < imgHeight) {
            thumbnail_width = (int)((imgWidth / imgHeight) * maxSize);
        } else {
            thumbnail_height = (int)((imgHeight / imgWidth) * maxSize);
        }
		    
        int imgType = (buffer_original_image.getTransparency() == Transparency.OPAQUE) ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
        BufferedImage buffer_thumbnail_image = new BufferedImage(thumbnail_width, thumbnail_height, imgType);
        Graphics2D graphic = buffer_thumbnail_image.createGraphics();
		    
        graphic.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
        graphic.drawImage(buffer_original_image, 0, 0, thumbnail_width, thumbnail_height, null);
		    
        if(ext.equalsIgnoreCase("jpg")) {
            writeJpeg(buffer_thumbnail_image, newFileName, 1.0f);
        } else {
            File thumb_file_name = new File(newFileName);
            ImageIO.write(buffer_thumbnail_image, ext.toLowerCase(), thumb_file_name);
        }
		    
        graphic.dispose();
    } catch (Exception e) {
        e.printStackTrace(System.err);
        return false;
    }
		
    return true;
}

위의 함수는 2개의 내부 함수를 호출하고 있는데, 해당되는 함수들은 아래와 같습니다.

private static String getFileExt(String fileName) { // "abc.txt" -> "txt", not ".txt"
    int i = fileName.lastIndexOf('.');
    int p = Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\'));

    if (i > p) {
        return fileName.substring(i+1);
    }

    return null;
}
	 
private static void writeJpeg(BufferedImage image, String destFile, float quality) throws IOException {
    ImageWriter writer = null;
    FileImageOutputStream output = null;
		  
    try {
        writer = ImageIO.getImageWritersByFormatName("jpeg").next();
	
        ImageWriteParam param = writer.getDefaultWriteParam();
		    
        param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
        param.setCompressionQuality(quality);
		    
        output = new FileImageOutputStream(new File(destFile));
        writer.setOutput(output);
		    
        IIOImage iioImage = new IIOImage(image, null, null);
        writer.write(null, iioImage, param);
    } catch (IOException ex) {
        throw ex;
    } finally {
        if (writer != null) {
            writer.dispose();
        }
		    
        if (output != null) {
            output.close();
        }
    }
}

단위 테스트로 만들어 놓은 코드인지라 최적화가 되어 있지 않으니, 살펴보시고 최적화 해 사용하시기 바랍니다.