윈도우 탐색기에서 마우스로 파일을 선택하여 프로그램으로 드래그하는 것으로 파일을 읽는 것을 Drag and Drop 라고 한다. 간단하게 구현 방법을 알아보자.

 

Drag and Drop

 

DropTarget

public DropTarget(Component c, DropTargetListener dtl)

 드래그 앤 드롭을 적용할 Component와 드롭되었을 때 적절한 처리를 해줄 DropTargetListener를 생성자 파라미터로 받는다.

 

DropTargetListener

Method 설명
public void drop(DropTargetDropEvent dtde) 파일이 드래그 된 후 타겟 위에서 마우스 동작을 종료했을 때 호출
public void dragEnter(DropTargetDragEvent dtde) 파일이 타겟 위에 드래그 된 시점에서 호출
public void dragOver(DropTargetDragEvent dtde) 파일이 드래그 된 후 타겟 위에서 마우스 포인터가 존재할 때 호출
public void dropActionChanged(DropTargetDragEvent dtde) 파일이 드래그 된 후 제스쳐가 변경되었을 때 호출
public void dragExit(DropTargetEvent dte) 파일이 드래그 된 상태에서 포인터가 타겟에서 벗어났을 때 호출

 드래그 앤 드롭 과정에서 발생하는 콜백 메서드들이다. 다른 메서드는 그다지 쓰이지 않고 drop을 구현하는 것이 일반적이다.

 

 

Code

import java.awt.Color;
import java.awt.datatransfer.DataFlavor;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.io.File;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JTextArea;

public class GUI extends JFrame implements DropTargetListener{
	JTextArea jta_log;
	
	public GUI () {
		setTitle("Swing");
		setBounds(10, 10, 400, 300);
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		getContentPane().setBackground(Color.gray);
		setLayout(null);
		
		DropTarget dropTarget = new DropTarget(this, this);
		setDropTarget(dropTarget);
		
		jta_log = new JTextArea();
		jta_log.setBounds(10, 10, 360, 200);
		add(jta_log);
	}
	
	@Override
	public void drop(DropTargetDropEvent dtde) {
		jta_log.append("파일이 드롭되었습니다.\n\n");
		try {
			dtde.acceptDrop(DnDConstants.ACTION_MOVE);
			List<File> files = (List<File>)dtde.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
			for(File file : files) {
				jta_log.append(file.getPath() + "\n");
			}
		} catch(Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public void dragEnter(DropTargetDragEvent dtde) { }

	@Override
	public void dragOver(DropTargetDragEvent dtde) { }

	@Override
	public void dropActionChanged(DropTargetDragEvent dtde) { }

	@Override
	public void dragExit(DropTargetEvent dte) { }
}

 

 예제는 JTextArea를 배치하여 드롭된 파일 목록을 출력한다. DropTargetListener는 이름에서 짐작할 수 있듯이 파일을 드래그해 올 때 이벤트를 감지하고 처리하는 인터페이스이고, DropTarget은 파일을 드롭시킬 Target Component와 이벤트를 설정할 수 있다. JFrame을 Target으로 하고 이벤트도 현재 클래스에서 implements 했으므로 (this, this)를 넘겨주었다.

 

 파일을 Drop하게 되면 Drop 메서드에서 File 타입 객체들을 List로 읽어올 수 있다. 바로 처리할 것이 아니라면 File 타입의 리스트를 멤버 변수로 생성해두고 파일이 드래그될 때마다 누적시켜서 처리하는 것이 좋다. 

문제가 발생한 예
클릭을 해야만 정상적으로 화면이 표시된다.

 Swing으로 화면을 구성했을 때 Frame에 배치한 컴포넌트들이 보이지 않고 마우스로 해당 자리를 클릭해야만 컴포넌트가 표시되거나 클릭해도 표시되지 않을 때가 있다.

 

 

public class Main {
	public static void main(String args[]) {
		GUI gui = new GUI();
	}
}

class GUI extends JFrame {
	public GUI () {
		..
		this.setVisible(true);
		..
    }
}

 JFrame을 상속한 GUI 클래스가 있을 때 해당 클래스 내부에서 setVisible을 호출할 경우 위와 같은 현상이 발생한다.

 

 

public class Main {
	public static void main(String args[]) {
		GUI gui = new GUI();
		gui.setVisible(true);
	}
}

 내부에서 setVisible을 호출하지 않고 객체화한 후 호출하도록 하자.

 자바에서 입출력이 이루어지는 과정에 대해 알아보자. 주로 Stream, Buffer에 대하여 다룬다. 입출력에 대해 이해하기 위해서는 먼저 Stream을 알아야 하고, 입출력 과정에서 Buffer를 사용하는 것과 사용하지 않는 것의 차이를 아는 것이 중요하다. 자바에서 입출력은 Stream을 이용하는 단방향 통신의 IO와 Channel을 이용하는 양방향 통신의 NIO가 있다. Stream의 경우 단방향이기 때문에 입력 혹은 출력을 하기 위해서는 용도에 맞는 스트림을 각각 열어야만 하고 입력 스트림은 입력으로만, 출력 스트림은 출력으로만 사용가능하다. Channel의 경우 Stream과 달리 하나의 채널로 양방향으로 통신이 가능하다. 본 포스팅에서는 Stream에서 출력이 이루어지는 과정에 대해 알아본다.

 

 

 

 Stream

 Stream 이란 입력 장치와 프로그램 간에 데이터가 이동하는 가상의 통로로, 데이터를 입력받는 InputStream과 데이터를 출력하는 OutputStream으로 나뉜다. Consol 입출력, 파일 입출력, 네트워크 통신 등 스트림은 다양하게 사용되며 자바에서는 각 상황에 맞는 다양한 Stream 클래스를 제공하고 있다. Stream은 FIFO 특성을 가지며 데이터를 입력하는 순서대로 들어오고 들어온 순서대로 도착한다. 자바에서 모든 IO는 Stream을 통해서 이루어진다. 

 

 

출처 : https://story.stevenlab.io/38

 

 자세히 살펴보면 알 수 있듯이 IO Stream은 항상 Read(input), Write(Output) 두 가지가 존재한다. 즉 단방향 통신이라는 이야기다. 입력을 받지 않고 출력만을 한다면 OutputStream에 관한 클래스만 사용해도 되고, 입력받아서 처리한 후 출력할 내용이 없다면 InputStream만 써도 된다는 뜻이다. 

 

 입출력에 있어서 Stream 개념이 중요한 이유는 우리가 입력하고 출력하는 데이터들이 화면에 표시되기 전에 이 Stream이라는 통로를 거치기 때문이다. 이 개념을 이해하지 못하면 입출력이 이루어지는 과정을 이해하는데 어려움이 있기 때문에 잘 숙지해야 한다.

 

 

 

 Buffer

 Stream은 입력 장치와 프로그램 간의 데이터 통로라고 했다. 사용자가 키보드에서 'a'를 입력하면 그 즉시 프로그램에 'a'가 전달된다. 사용자의 입력이 프로그램에 즉시 전달되는 것은 성능에 나쁜 영향을 준다. 하지만 실제로 처리되는 과정에서 대량의 데이터가 입출력되는 와중에 하나의 문자를 계속해서 전달하는 것은 굉장히 비효율적이다. n 개의 문자가 있을 때 n번 데이터 전송이 이루어진다는 것이다.

 이 문제를 해결하기 위해서 buffer라는 개념이 등장한다. 데이터를 계속해서 보내지 말고 어딘가에 쌓아두다가 일정량만큼 쌓이면 데이터를 전송하자는 것이다. 

 

 

 버퍼를 사용하지 않는 예와 버퍼를 사용하는 예를 보자. 중간에 화살표가 Stream이라고 생각하자.

 

 

버퍼를 사용하지 않는 예) 문자열 "Hello World" 를 입력했을 경우

 

 버퍼를 사용하지 않고 "Hello world"를 입력하는 경우 위와 같은 일이 일어난다. "Hello world"라는 타이핑하는데 3초도 걸리지 않음에도 문자가 입력될 때마다 프로그램에 전송하고 있는 것을 볼 수 있다.

 

 

 

버퍼를 사용한 예) 문자열 "Hello world"를 입력했을 경우

 

 버퍼를 사용하여 "Hello world"를 입력할 때 버퍼를 두게 되면 버퍼에 입력하는 데이터가 쌓이게 된다. 그리고 버퍼가 다 찼거나 사용자가 임의적으로 데이터를 출력하면 버퍼에 있는 데이터가 Program으로 전송된다. 단순히 생각해봐도 버퍼의 크기가 1,024char(2,048byte)일 때 버퍼를 사용하지 않는 경우 1024번 데이터가 전송되는 것을 버퍼를 사용함으로써 1번으로 줄일 수 있다는 것을 알 수 있다.

 

 위는 입력받는 과정을 나타냈지만 출력도 동일하다. 화살표 방향만 바꾸어 보면 감이 올 것이다.

 

 


 

 

 Buffer는 반드시 사용해야 할까?

 그렇다면 "버퍼를 사용하지 않고 입력을 받는 것은 잘못된 것인가?" 라고 묻는다면 그렇지는 않다. 다만 대부분의 경우 효율성이 떨어진다는 이야기다.

 문자 1개만을 입력받는 상황을 예로 들어보자. 이러한 상황에서 버퍼는 도움이 될까? BufferedReader 클래스를 사용할 때 기본 Buffer 크기는 8192char(16,384byte)다. 문자 1개만을 입력받고자 할 때 굳이 8,192의 크기를 할당받는 것이 맞는 것일까? 출력할 때에는 BufferedWriter를 사용할 수 있다. 마찬가지로 기본 버퍼 크기가 8,192인데 문자 하나를 출력한다고 하면 버퍼가 무슨 소용이 있겠는가?

 물론 실제 상황에서는 문자 하나, 숫자 하나를 입력받고 끝나는 케이스보다는 긴 문자열을 입력받는 경우가 커먼 케이스일 것이다. 다만 알고리즘처럼 어떤 상황에서든 최고의 성능을 내는 알고리즘은 없듯이 이 또한 필요한 상황이 있고 필요없는 상황이 있다. 필요에 따라서 적절하게 구현해서 사용해야 한다. 물론 그러기 위해선 제대로 된 이해가 동반되어야 할 것이다.

 

 마치며

 알고리즘 문제를 해결할 때 문제에서 요구하는 결과를 얻어도 실행시간이 초과되어 실패하는 경우가 종종 있다. 알고리즘이 비효율적이기 때문일 수도 있지만 입출력 속도에 문제가 있을 때도 있다. 알고리즘을 해결하는 것도 중요하지만 해결했을 때 다른 개발자 분들이 해결한 알고리즘에 비해서 속도가 현저히 떨어진다면 어느 부분에서 속도가 차이나는 것인지 또한 생각해보는 것이 좋겠다. 

 코딩 문제 사이트에서 문제를 풀다가 이 클래스에 대해 알게 되었다. 단계별로 준비된 문제를 풀다가 입출력 문제를 풀게 되었는데 내 코드의 실행 시간과 상위권 유저의 실행 시간이 너무나도 크게 차이났다. 도대체 뭐가 얼마나 차이나는 것인지 궁금해서 공개된 코드를 보게 되었는데 나는 학교에서 배운대로 Scanner를 사용한 반면 상위권 유저들은 모두 System.in으로 입력을 받고 있었다. 출력 시간을 줄이는 노력은 해본적 있지만 입력 시간을 줄인다는 것은 생각해본 적 없었기에 이 클래스가 궁금해졌다.

 

System.in

 System.in은 자바에서 데이터를 입력받는 표준 클래스지만 일반적인 상황에서는 단독으로 사용할 일이 잘 없다. 자바에서 입력을 받아야 하는 상황이라면 대부분 Scanner 혹은 BufferedReader를 사용하기 때문인데 그 이유는 단순히 System.in 클래스를 사용하는 것보다 다른 두 클래스가 사용하기 편하기 때문이다.

 

System.in.read(byte)

 System.in을 사용할 경우 가장 많이 사용되는 메서드로 1byte 크기의 데이터 하나를 입력받을 수 있다. 바꿔 말하면 영문 대소문자, 숫자, 특수문자 등을 입력받을 수 있고 한글은 입력받을 수 없다. 

 

int c = System.in.read();
System.out.println(c);

ex) [ 숫자(문자) 0~9 : 48~57, 대문자 A~Z : 65~90, 소문자 a~z : 97~122 ]

 

 read 메서드는 위와 같이 사용할 수 있다. 위 코드를 실행해서 문자를 입력받으면 어떤 값(숫자, 영문자 등)을 입력해도 정수형으로 반환되는데 이를 이해하려면 아스키 코드를 알아야 한다. 콘솔에서 입력한 값은 아스키 코드표에 해당하는 값으로 저장되기 때문이다. 숫자 0을 입력했다면 변수 c에 48이, 문자 'a'를 입력했다면 97이 초기화될 것이다.

 

 Scanner를 사용하면 숫자, 문자, 문자열 등 원하는 값을 즉시 입력받을 수 있는 메서드들이 존재하지만 System.in을 사용하면 이 메서드 하나로 숫자, 문자를 모두 입력받아야 한다. 이 메서드 자체로는 아스키 코드값밖에 얻을 수 없으니 숫자나 문자를 저장하고 싶으면 그에 맞는 가공 작업이 필요하다.

 

 

문자로 입력받기

char c = (char)System.in.read();
System.out.println(c);

 입력받은 문자를 그대로 출력하기 위한 방법 중 하나는 형 변환(Type Casting)이다. 입력을 받을 때 강제로 형 변환을 시키면 아스키 코드값에 맞는 문자가 변수에 초기화된다.

 

 

숫자로 입력받기

int num = System.in.read() - 48;
System.out.println(num);

 숫자 0을 입력받았다고 가정할 때 숫자 0은 아스키 코드표에서 48에 해당하기 때문에 num에는 48이 저장된다. 이 값을 숫자로 사용하려면 입력받은 값에 -48 연산을 해주면 된다.

 

 

10이상의 수 입력받기

int num = System.in.read();
System.out.println(num);

 위 코드를 실행하고 "12"를 입력하면 "49"가 출력된다. 앞서 말한 것처럼 read()는 1바이트 단위로 입력을 받기 때문에 입력한 값 중 먼저 입력한 '1'의 아스키 코드값인 49만 저장된 것이다.

 

 

public class Main {
	public static void main(String args[]) throws Exception {
		int num = readInt();
		System.out.println(num);
	}
	
	public static int readInt() throws Exception {
		InputStream is = System.in;
		int sum = 0;
		
		while(true) {
			int n = is.read();
			
			if(n == '\n')
				return sum;
			else if (n >= 48 && n <= 57)
				sum = (sum*10) + (n-48);
		}
	}
}

 두 자리 이상의 수를 저장하려면 키보드에서 입력이 들어올때 마다 입력된 값이 숫자인지를 판별하여 값을 누적시켜야 한다. 입력되는 수가 늘어날수록 자릿수가 하나씩 증가하기 때문에 누적된 값에 *10을 하고 입력되는 값을 더해주는 것으로 10 이상의 숫자를 입력받을 수 있다.

 

 


 

 

 System.in은 Scanner, BufferedReader와 비교하면 입력하는 값에 따라 그에 맞는 구현을 해줘야한다는 것, 입력에서 발생할 수 있는 예외를 처리해줘야 한다는 단점이 있다. 이런 이유로 System.in으로 입력받는 방법을 알게 된 후에도 코딩 문제를 풀 때를 제외하면 사용할 일이 크게 없는 것 같다.

 자바에서는 디렉터리나 파일을 다룰 수 있도록 java.io.File 클래스를 제공하고 있다. File 클래스에서 제공하는 renameTo( ) 메소드는 디렉터리, 파일의 이름을 변경할 수 있다. 

 

 

 주요 메서드

public boolean renameTo(File dest)

 

 renameTo 메소드는 변경하고자 하는 파일에 대한 참조를 가진 File 객체를 파라미터로 받아 이름 변경 성공 여부를 리턴한다. 이름 변경에 성공했을 경우 true, 실패했을 경우 false를 리턴하는데 실패한 이유까지는 제공하지 않는다. 주로 변경하고자 하는 디렉터리(혹은 파일)이 같은 경로에 있을 경우 혹은 변경하려고 하는 디렉터리가 다른 프로세스에 의해서 참조되고 있을 경우 false를 반환한다. 모든 상황에 대해서 false를 반환하기 때문에 제어문을 통해서 먼저 검증을 하고 renameTo 메서드를 호출하는 것이 좋다.

 

 또한 변경하려고 하는 파일, File 객체에 의해서 참조되고 있는 파일이 존재하지 않을 경우에 FileNotFoundException을 발생시킨다. 굉장히 간단하게 쓸 수 있는 메서드지만 실패했을 경우 그 상세 이유를 알 수 없기 때문에 예외 처리에 있어서 조금 까다로울 수 있다.

 

public String getParent()

 

 객체가 가리키는 파일의 상위 디렉터리 Path를 String 형태로 반환한다. 

 

public String getName()

 

 객체가 가리키는 파일을 경로없이 이름만 반환한다.

 

 


 

 

디렉터리 이름 변경

example) "C:\Users\guest\Pictures\images" → "C:\Users\USER\Pictures\icon" 으로 변경

public class Example {
	public static void main (String args[]) {
		File file = new File("C:\\Users\\USER\\Pictures\\images");
		File newFile = new File(file.getParent() + "\\" + "icons");
		if(file.renameTo(newFile))
			System.out.println("성공");
		else
			System.out.println("실패");
	}
}

 

 "사진" 디렉터리에 있는 images 디렉터리의 이름을 icons으로 변경하는 예제다. images 디렉터리의 경로를 File의 생성자 파라미터로 가지는 File 객체를 생성하고 마찬가지로 변경하고자하는 디렉터리의 경로를 File 객체를 생성한다. 이 경우 같은 위치에서 디렉터리 이름만 바뀌는 것이기 때문에 기존 파일 객체에서 getParent() 메서드를 통해 상위 경로를 문자열 객체로 얻어올 수 있다. 

 

 이름을 변경하고자 하는 객체에서 renameTo(File) 메서드를 호출한 뒤 파라미터로 바꾸고자 하는 파일 객체를 전달하면 이름을 변경할 수 있다.

 

 

 

파일 이름 변경

 디렉터리가 아닌 파일의 이름을 변경하고자 할 때 같은 메소드를 사용하지만 확장자를 고려해야 한다. 이미지 파일인 경우 JPG, PNG 등이 있고 동영상 파일의 경우 AVI, MP4 등의 확장자가 붙는데 파일의 확장자를 고려하지 않고 파일의 이름을 변경하면 파일 탐색기 상에서 파일 형식이 "파일"로 표시되어 어떤 파일인지 알아볼 수 없게 되기 때문에 파일 이름을 변경한다면 반드시 확장자를 고려해야 한다.

 

 

 

example) "C:\Users\guest\Pictures\images"에 있는 icon1.jpg 파일을 icon2.jpg로 변경

public class Example {
	public static void main (String args[]) {
		File file = new File("C:\\Users\\USER\\Pictures\\images\\icon1.jpg");
		String fileName = file.getName();
		int idx = fileName.lastIndexOf(".");
		String ext = fileName.substring(idx + 1);
		String newFilePath = file.getParent() + "\\" + "icon2." + ext;
		File newFile = new File(newFilePath);

		if(file.renameTo(newFile))
			System.out.println("성공");
		else
			System.out.println("실패");
	}
}

 

 File 클래스에서 getName() 메소드를 호출하면 해당 객체가 가진 파일 경로에서 파일 이름(icon1.jpg)만을 얻을 수 있다. 이렇게 얻은 파일 이름의 끝에서부터 "."을 찾아 해당 위치를 저장하고 해당 위치의 다음에 오는 문자열을 저장하는 것으로 확장자를 얻을 수 있다. 그 후 getParent()로 상위 경로를 얻고 바꿀 파일 이름과 추출한 확장자를 연결하는 것으로 파일 이름 변경을 할 수 있다.

+ Recent posts