1. Swing과 AWT에 대한 아주 짧은 소개
Java가 자동차라면 Swing과 AWT는 그 “계기판”과 “핸들”입니다. AWT (Abstract Window Toolkit)는 Java에서 데스크톱 애플리케이션을 만들기 위한 최초의 라이브러리입니다. 운영체제의 네이티브 컨트롤을 사용하므로 Windows, Linux, Mac에서 보이는 모습이 서로 다릅니다.
Swing은 AWT 위에 구축된, 순수 Java로 작성된 보다 현대적인 라이브러리입니다. Swing은 더 아름답고 유연하며, 부가 기능을 많이 제공하고(완전히 같지는 않지만) 모든 플랫폼에서 거의 동일하게 보입니다. 두 경우 모두 버튼, 텍스트 필드, 체크박스 등 모든 UI 구성 요소가 이벤트를 지원합니다.
이 강의에서는 Swing을 사용할 것입니다. 초보자에게 더 쉽고 학습 예제에서 자주 보이기 때문입니다. 가장 흔한 과제인 버튼 클릭 반응을 예로 들어 이벤트 처리가 어떻게 구성되는지 알아봅시다.
버튼 만들기
Swing에서 버튼은 다음과 같이 생성합니다:
JButton button = new JButton("눌러 주세요!");
여기서 “눌러 주세요!”는 버튼에 표시되는 텍스트입니다.
리스너 추가
클릭에 반응하려면 이벤트에 리스너를 등록해야 합니다:
button.addActionListener(new MyActionListener());
그렇다면 MyActionListener는 무엇일까요? ActionListener 인터페이스를 구현하는 클래스입니다:
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class MyActionListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("버튼이 눌렸습니다!");
}
}
사용자가 버튼을 누르면, Swing은 addActionListener로 등록된 모든 리스너의 actionPerformed 메서드를 호출합니다.
전체 예제: 버튼이 있는 간단한 창
이제 모든 것을 합쳐 실제로 동작하는 애플리케이션을 만들어 봅시다:
import javax.swing.*;
import java.awt.event.*;
public class SimpleButtonApp {
public static void main(String[] args) {
JFrame frame = new JFrame("이벤트 예제");
JButton button = new JButton("눌러 주세요!");
// 버튼에 리스너 추가
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("버튼이 눌렸습니다!");
JOptionPane.showMessageDialog(frame, "야호! 버튼이 눌렸어요!");
}
});
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(button);
frame.setSize(300, 200);
frame.setVisible(true);
}
}
여기서 무슨 일이 일어나나요:
- 창(JFrame)을 만듭니다.
- 버튼(JButton)을 만듭니다.
- 리스너를 추가합니다(별도 파일을 늘리지 않기 위해 익명 클래스를 사용).
- actionPerformed 안에서 콘솔 메시지를 출력하고 팝업 창(JOptionPane)을 띄웁니다.
- 버튼을 창에 추가하고 창을 사용자에게 표시합니다.
직접 해보기: 이 코드를 복사해 실행하고 버튼을 눌러 보세요!
2. 익명 클래스와 람다식
현대의 Java 코드에서 단 하나의 처리기 때문에 별도의 클래스를 매번 만드는 것은 빵을 사러 전차를 몰고 가는 격입니다. 익명 클래스나 람다식을 쓰는 것이 훨씬 편리합니다.
익명 클래스
이미 위에서 본 방식입니다:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// 처리 코드
}
});
여기서는 클래스가 이름 없이 그 자리에서 선언됩니다.
람다식 (Java 8+)
리스너 인터페이스가 함수형(즉, 추상 메서드가 하나뿐)이라면 람다를 사용할 수 있습니다:
button.addActionListener(e -> {
System.out.println("람다로 버튼이 눌렸습니다!");
});
본문이 한 줄이라면 더 짧게 쓸 수 있습니다:
button.addActionListener(e -> System.out.println("람다: 버튼이 눌렸습니다!"));
이는 코드를 줄일 뿐 아니라 더 읽기 쉽게 만듭니다. 알아두면 좋은 사실: ActionListener 인터페이스는 메서드가 하나뿐이므로 함수형 인터페이스입니다. 해당 메서드는 actionPerformed입니다.
4. 기타 이벤트 유형
세상은 버튼만 있는 것이 아닙니다! Swing과 AWT의 거의 모든 UI 구성 요소는 각자의 이벤트를 지원합니다. 대표적인 것들은 다음과 같습니다:
마우스 이벤트: MouseListener와 MouseAdapter
마우스 클릭에 반응하려면 MouseListener를 사용합니다:
button.addMouseListener(new MouseListener() {
@Override
public void mouseClicked(MouseEvent e) {
System.out.println("버튼을 마우스로 클릭!");
}
// 나머지 메서드는 비워 두어도 됩니다
@Override public void mousePressed(MouseEvent e) {}
@Override public void mouseReleased(MouseEvent e) {}
@Override public void mouseEntered(MouseEvent e) {}
@Override public void mouseExited(MouseEvent e) {}
});
하나의 처리기 때문에 다섯 개의 빈 메서드를 작성하는 것은 불편합니다. 이를 위해 MouseAdapter가 있습니다:
button.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
System.out.println("버튼을 마우스로 클릭(어댑터 사용)!");
}
});
MouseAdapter는 인터페이스의 모든 메서드를 기본 구현하므로 필요한 것만 오버라이드하면 됩니다.
키보드 이벤트: KeyListener/KeyAdapter
키 입력을 처리하려면 다음과 같이 합니다:
button.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
System.out.println("키가 눌림: " + e.getKeyChar());
}
});
텍스트 변경 이벤트: DocumentListener
텍스트 필드(JTextField, JTextArea)에는 전용 리스너가 있습니다:
JTextField textField = new JTextField();
textField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
System.out.println("텍스트 변경됨: " + textField.getText());
}
@Override public void removeUpdate(DocumentEvent e) {}
@Override public void changedUpdate(DocumentEvent e) {}
});
참고로, 대부분의 이벤트에는 빈 메서드를 작성하지 않도록 어댑터가 제공됩니다.
5. 실습: 창과 버튼이 있는 미니 예제
버튼을 누를 때마다 카운터가 증가하고 메시지가 출력되는 가장 단순한 GUI 애플리케이션을 만들어 봅시다.
예제: 클릭 카운터
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class ClickCounterApp {
public static void main(String[] args) {
JFrame frame = new JFrame("클릭 카운터");
JButton button = new JButton("눌러 주세요!");
JLabel label = new JLabel("클릭 수: 0");
// 클릭 카운터(반드시 final 또는 effectively final이어야 함)
final int[] count = {0};
button.addActionListener(e -> {
count[0]++;
label.setText("클릭 수: " + count[0]);
});
frame.setLayout(new FlowLayout());
frame.add(button);
frame.add(label);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(250, 100);
frame.setVisible(true);
}
}
설명:
- 카운터를 표시할 창, 버튼, 레이블을 만듭니다.
- 람다의 final/사실상 final 변수 제약을 우회하기 위해 원소가 하나인 배열(final int[] count = {0};)을 사용합니다.
- 버튼을 누를 때마다 카운터를 증가시키고 레이블 텍스트를 갱신합니다.
연습 아이디어: 다섯 번마다 버튼 색을 바꿔 보세요!
6. 이벤트 모델은 내부에서 어떻게 동작할까
잠깐 보닛 아래를 들여다봅시다. 다음을 호출하면:
button.addActionListener(listener);
버튼(JButton 객체)은 내부 리스트에 리스너를 추가합니다. 사용자가 버튼을 누르면 내부적으로 다음이 일어납니다:
- 버튼이 이벤트 객체(ActionEvent)를 생성합니다.
- 버튼이 리스너 목록을 순회하며 각 리스너의 actionPerformed 메서드를 호출합니다.
- 여러분의 처리기가 필요한 동작을 수행합니다(예: 레이블 업데이트).
중요한 점: 각 이벤트에는 리스너를 여러 개 등록할 수 있습니다. 하나의 버튼에 여러 처리기를 추가하면 차례대로 호출됩니다.
7. 시각적 다이어그램: 이벤트 처리
graph TD
A[사용자가 버튼을 눌렀다] --> B{JButton}
B --> C[ActionEvent 생성]
C --> D[모든 리스너의 actionPerformed 호출]
D --> E[핸들러가 동작 수행]
7. Swing과 AWT에서 이벤트를 다룰 때 흔한 실수
오류 1: 리스너 등록을 잊음. addActionListener 호출을 빼먹으면 버튼이 클릭에 반응하지 않습니다.
오류 2: 처리기에서 무거운 작업을 수행. 이벤트 처리기에서 긴 루프를 돌리거나 인터넷에서 다운로드를 하면 UI가 “멈춥니다”. 오래 걸리는 작업은 별도 스레드나 SwingWorker를 사용하세요.
오류 3: final/사실상 final이 아닌 변수를 람다에서 변경하려 함. 람다 내부에서는 final 또는 “사실상 final” 변수만 사용할 수 있습니다. 카운터에는 배열이나 전용 클래스를 사용하세요(좀 더 본격적으로 하려면 AtomicInteger).
오류 4: 리스너 제거를 잊음. 컴포넌트를 제거하면서 리스너를 제거하지 않으면 메모리 누수가 발생할 수 있습니다. 컴포넌트가 더 이상 필요 없을 때는 구독을 해지하세요.
오류 5: 인터페이스의 모든 메서드를 구현하지 않음. 예를 들어 MouseListener를 직접 구현할 때는 모든 메서드를 잊지 말고 구현하세요! 가능하면 어댑터(MouseAdapter, KeyAdapter)를 사용하세요.
GO TO FULL VERSION