자바

week9.java GUI_Swing, Event 실습

aerimi-code 2025. 11. 2. 21:42

1.  자바 GUI와 Swing 개요

자바의 GUI (Graphical User Interface)는 그래픽 요소를 활용하여 사용자에게 정보를 시각적으로 제공하고, 사용자의 입력(마우스 클릭, 키 입력 등)을 시스템에 전달하는 매개체입니다. 이는 소프트웨어 아키텍처에서 MVC (Model-View-Controller) 패턴 중 View 역할을 담당합니다.

 

Swing의 특징: 

 

  • 경량(Lightweight) 컴포넌트: 기존의 AWT(Abstract Window Toolkit)의 한계를 극복하기 위해 등장했습니다. OS의 네이티브(Native) 코드 대신 순수 자바 코드로 그려지기 때문에, 모든 운영체제에서 동일한 모양과 동작을 보장합니다.
  • 컴포넌트 명칭: 모든 Swing 컴포넌트에는 접두사 'J'가 붙습니다. (예: JFrame, JButton, JLabel, JPanel).

 


2. Swing 응용 프로그램 구조 및 계층

 

주요 컨테이너

  • 최상위 컨테이너: 응용 프로그램의 메인 윈도우 역할을 하며, 스스로 표시될 수 있는 영역입니다.
    • JFrame: 응용 프로그램의 기본 창.
    • JDialog: 팝업 창 또는 메시지 창.
  • 중간 컨테이너: 다른 컴포넌트들을 담고 그룹화하며 Layout Manager를 통해 컴포넌트의 위치와 크기를 관리합니다.
    • JPanel: 컴포넌트를 논리적으로 묶는 데 가장 흔히 사용되는 컨테이너.

응용 프로그램 종료 처리 (JFrame)

프레임을 닫는 행위와 응용 프로그램 종료는 다릅니다. 이는 JFrame.setDefaultCloseOperation() 메소드를 통해 설정합니다.


3. 이벤트 처리 메커니즘 (Event Handling)

GUI의 핵심은 사용자의 행동에 응답하는 것입니다.

이 응답 과정을 이벤트 처리라고 합니다.

 

나는 windowbuilder tool을 사용하여 실습을 진행했다. 

이벤트 처리의 3요소

  1. 이벤트 소스 (Event Source): 이벤트가 발생하는 컴포넌트 (예: JButton).
  2. 이벤트 객체 (Event Object): 발생한 이벤트에 대한 정보(어떤 일이 일어났는지)를 담은 객체 (예: 버튼 클릭 시 발생하는 ActionEvent).
  3. 이벤트 리스너 (Event Listener): 이벤트 객체를 전달받아 실제 처리 로직을 수행하는 코드.

 

 

ActionListener 인터페이스

  • 역할: JButton과 같은 컴포넌트가 클릭되는 Action Event가 발생했을 때 처리할 작업을 정의하는 인터페이스입니다.
  • 필수 메소드: 이 인터페이스를 구현하려면 반드시 actionPerformed(ActionEvent e) 메소드를 오버라이딩하여 정의해야 합니다. 이 메소드 안에 버튼 클릭 시 실행될 코드를 작성합니다.

 

addActionListener() 메소드

  • 역할: 이벤트 소스 컴포넌트(예: 버튼)가 가지고 있는 메소드로, "누가 이 이벤트를 처리할 것인지" (이벤트 리스너)를 컴포넌트에게 등록하고 알려줍니다.
  • 인자: ActionListener 인터페이스를 구현한 객체를 인자로 받습니다.

 

Event 리스너 작성 방법: 해당 버튼에 ActionListener를 등록합니다. 

 

리스너를 등록하는 과정을 거쳐야하는데, 해당 방법은 총 4가지 입니다. 

 

1. 익명 클래스 사용

// JButton이 이벤트 소스
JButton myButton = new JButton("클릭");

// myButton이 이벤트를 처리할 리스너 객체를 등록하고 "알려줍니다."
myButton.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        // 버튼이 클릭되었을 때 실행될 코드
        System.out.println("버튼이 눌렸습니다!");
    }
});
  • 컴포넌트 (이벤트 소스): 위의 코드에서는 myButton 객체입니다. 이 버튼은 자신이 클릭되는 순간 (이벤트 발생)을 감지합니다.
  • addActionListener(): 이 메소드를 호출하여 리스너 객체를 myButton 내부에 등록(저장)합니다.
  • 이벤트 발생 시: 실제로 버튼이 클릭되면, myButton은 자신이 저장해 두었던 리스너 객체를 찾아가 그 안에 있는 actionPerformed() 메소드를 실행하도록 요청합니다.

장점: 가독성이 높고, 간결, 여러 버튼에 각기 다른 동작을 줄 때 유용하다.

 

 

2. 람다식 사용

// JButton 이벤트 소스 생성
JButton lambdaButton = new JButton("람다 버튼");

// 람다식을 사용하여 ActionListener 구현 및 등록
lambdaButton.addActionListener(e -> {
    System.out.println("람다식으로 처리됨: 버튼이 눌렸습니다.");
});

장점: 코드가 간결해지고, 가독성이 좋아집니다. 

 

 

3. 별도의 클래스에서 구현

// 1. 별도의 이벤트 처리 클래스 정의
class MyListener implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("별도의 클래스로 처리됨: 버튼이 눌렸습니다.");
        // 참고: 이 경우, GUI 요소(예: 텍스트 필드)에 접근하려면 해당 요소를 생성자 등을 통해 전달받아야 합니다.
    }
}

// 2. GUI 클래스 내에서 등록
JButton separateClassButton = new JButton("별도 클래스 버튼");
separateClassButton.addActionListener(new MyListener());

장점: View와 완전히 불리하여, 코드 재사용성이 높아지고, 캡슐화가 가능합니다.

 

4. 현재 클래스에서 구현 

// 1. GUI 클래스가 ActionListener를 구현한다고 선언
public class MyFrame extends JFrame implements ActionListener {
    JButton currentClassButton = new JButton("현재 클래스 버튼");

    public MyFrame() {
        // 컴포넌트에 현재 클래스(this)를 리스너로 등록
        currentClassButton.addActionListener(this);
    }

    // 2. 인터페이스의 추상 메소드를 오버라이딩
    @Override
    public void actionPerformed(ActionEvent e) {
       System.out.println("내부 클래스 - 클릭됨");
    }
}

 

장점: 현재 클래스 자체가 ActionListener인터페이스를 구현하고, 메소드를 오버라이딩한다. (작은 프로그램에 적)

 

 


마우스 이벤트와 키 이벤트

 

마우스 이벤트 (Mouse Event):

마우스 클릭, 이동, 버튼 누름/뗌 등의 동작을 감지합니다.

MouseMotionListener는 이동과 드래그 이벤트를 처리합니다. 

마우스 이벤트 발생 순서

**clicked는 클릭이 완전히 끝나야 발생됩니다. 

 


 

키 이벤트 (Key Event): 

키 입력 시, key 이벤트가 발생합니다.

키를 누르거나, 뗄 때 발생합니다.

Key 이벤트에는 조건이 존재합니다.

- 모든 컴포넌트가 가능하지만, 현재 포커스 (focus)를 가진 컴포넌트가 이벤트를 받습니다.

 

포커스(Focus):

컴포넌트나 응용프로그램이 키 이벤트를 독점하는 권한입니다.

 

component.setFocusable(true); //포커스 받을 수 있도록 설정
component.requestFocus(); // 포커스 강제 지정

 

실행 환경의 초기화가 다를 수 있기 떄문에 componenet.setFocusable(true)가 필요합니다. 

key 이벤트의 focus는 여러 컴포넌트에 키 이벤트가 등록되어 있을 때, 입력을 받을 하나의 컴포넌트를 지정할 때 사용됩니다. 


리스너 등록 메소드: add000Listener 

 

컴포넌트는 다른 이벤트에 대한 리스너를 동시에 가질 수 있습니다.

예) 버튼 하나에 마우스 클릭, 키보드 입력 등 여러개의 리스너를 등록할 수 있습니다. 

 

컴포넌트는 한 이벤트에 대해 여러개의 리스너를 동시에 가질 수 있습니다.

예) 버튼이 클릭되었을 때, 여러개의 리스너를 동시에 등록할 수 있습니다. (단 실행은 거꾸로 실행됩니다. stack

특징 FILO)


#망나니 실습 

망나니 프로젝트 클래스 다이어그램
PlayerButton클래스 구조

Myview1:

gui화면을 담당하며, JFrame을 상속받습니다.

contentPane: 은 실제 컴포넌트들이 부착되는 영역입니다.

bt1, bt2, bt3, bt4 들은 PlayerButton 타입의 버튼들이고, 각각 플레이어이다. 

플레이어에게 입력받고, 화면에 보여주는 역할을 합니다. 

 

 

start 버튼이 눌리면, 플레이어 버튼들의 thread를 실행한다. 

 

Player: 

Player클래스는 플레이어의 정보와 상태를 담고 있습니다. 

Plaeyr 클래스

 

view 클래스에서 사용

 

player 정보를 담고, 해당 담은 정보를 버튼에 전달, 담는다.

delay: delay 값이 작을 수록 sleep시간이 짧아져, 스레드가 더 자주 꺠어나, 

 

 

PlayerButton: 

PlayerButton 클래스는 버튼으로서의 기능을 가지고, 클릭 이벤트가 발생했을 때 처리할 로직을 정의합니다. 스레드로 실행 될 수 있는 코드가 있고, 버튼 자체는 Player객체를 받아 Player정보를 담고, 움직임 작업을 독립적으로 수행할 수 있습니다. 

하나의 버튼이자 플레이어입니다. 

 

PlayerButton들은 하나의 Player이기 때문에 movenetType이 정해지고, 정해진 type에 맞는 움직임을 실행합니다.

 

 

기본 field들, 생성자 설명: 

플레이어 정보와 movementType정보를 담고, 버튼 속성을 설정합니다. endValue는 결승선에 대한 정보입니다. 

각각의 playerbutton들은 개별적으로 실행되어야 하기 때문에 Thread를 가지고 있습니다. 

정보들은 외부에서 get과 set을 통해 변경이 가능합니다.

 

button클래스 안에서 thread실행:

Runnable을 구현한 해당 클래스를 Thread로 생성하고, start()메소드를 만들어 쓰레드 start메서드를 실행합니다.

 

랜덤하게 색 바꾸기:

색을 랜덤하게 바꾸는 코드 입니다.

색은 rgb값을 통해 설정됩니다. red,green,blue의 정도를 random하게 생성하여 색을 설정합니다.

 

결승선 도착 검사: 

도착한다면 true를 리턴. 검사만 합니다. 

x를 계산하는 과정과 y를 계싼하는 메서드를 분리했습니다. 합쳐서 어떤 movementType인지 검사하고 case에 따라 처리하는 방식도 있겠지만, 그렇다면 위치 정보 말고, 추가적인 정보를 받아야하고, 추가적인 처리도 필요해서 간단하게 나눈 상태로 구성했습니다. 

 

버튼의 event: 

 

 

 

 

 

 

PlayerButton은 movementType이 RANDOM이 아니라면, 이벤트로 색깔이 변경되고, 속도가 빨라집니다. 

 

 

각 버튼들의 움직임: 

오른쪽으로 움직입니다. 결승선 체크를 합니다. 

delay는 해당 쓰레드가 sleep하는 정도입니다. 

 

밑으로 움직이기: 

 

아래쪽으로 움직입니다. 결승선 체크를 합니다. 

delay는 해당 쓰레드가 sleep하는 정도입니다. 

 

랜덤하게 움직이기: 

랜덤하게 움직이며, 색을 랜덤하게 바꿉니다. 결승선에 도착할 경우 멈춥니다. 

 

쓰레드 run 코드: sitch-case이용

쓰레드는 PlayerButton의 movementType을 받아 그에 맞는 움직임을 각각 개별의 스레드로 실행합니다. 

나중에 movement가 다양해진다면.. 전략 패턴을 사용할 수 도 있겠다는 생각이들었습니다. 

 

 

팀 스터디 

팀원 중 다인님의 아이디어를 설명하겠습니다.

enum 대신 전략으로 움직임을 각각 다르게 구현하고, view에서 해당 버튼의 전략을 설정 하고, run코드에서는 해당 전략이 실행되도록 하는 코드를 작성하면 됩니다. 

 

 

 

 

 

 

 


 

 

 

 

 

 

망나니의 독단적인 능력 

bt3는 망나니로 다른 플레이어들과 다르게 랜덤으로 움직입니다. (다른 플레이어들은 한 방향으로 움직입니다.)

다른 객체들과 다르게 클릭이 된다면 속도가 빨라지는 것이 아니라, 더블클릭 시 망나니 종료 메서드를 실행합니다. (endMang())

 

 

해당 구현은 "컴포넌트는 다른 이벤트에 대한 리스너를 동시에 가질 수 있다." 와

"인스턴스의 독단적인 처리를 위해서는 익명 클래스를 사용할 수 있습니다. " 라는 정보를 이용해 구현하였습니다. 

 

 

view클래스 속 메서드 구현

 

 

 

망나니 종료 코드

//망나니 종료
	void endMang() {
		bt3.running=false;
		bt3.setEnabled(false);
		//잡혔음을 알림
		JOptionPane.showMessageDialog(
				bt3, 
				bt3.getName() + "를 잡았습니다.\n망나니 쓰레드를 종료합니다!", 
                "메시지", 
                JOptionPane.INFORMATION_MESSAGE
            );
	}

 

망나니 버튼의 running 상태가 false되어 while문이 실행되지 않고, 버튼을 클릭할 수 없게 setEnabled(false)가 실행됩니다.

잡혔을 때 OptionPane에 메세지를 띄워서 종료 메시지를 보입니다. 

 

 

 

addMouseListener 코드 더블클릭 처리 

bt3.addMouseListener(new MouseListener(){
			@Override
			public void mouseClicked(MouseEvent e) {
				// TODO Auto-generated method stub
				if (bt3.getMovementType() == MovementType.RANDOM) {
					if(e.getClickCount()==2)//더블클릭인지 확인
					{
						endMang();
					}
				}
			}

if문 조건: 해당 버튼이 random하게 움직이는 타입의 버튼이면

더블클릭인지 확인하고, 망나니 종료 메서드를 실행합니다. 

 

addKeyListener 코드 엔터키 입력 처리

엔터 키를 눌렀을 때 망나니 메서드가 종료되도록 설정했습니다. 해당 구현은 망나니를 잡기 어려운 상황에 쓰는 치트키 같은 개념으로 작성했습니다.

addKeyListener를 사용했을 때는 bt3에 포커스가 있어야만 enter키가 작동합니다.

기본적으로 포커스를 가질 수 있지만 실행 될 때 다시 한번 강제로 포커스를 지정해야합니다. 

팀 스터디 

팀원 중 아연님이 진행하신 알낳기 프로그램에 대해서 설명하겠습니다. 

오른쪽 방향키로 닭을 움직이는 이벤트와 스페이스바로 알을 낳는 이벤트가 키 이벤트로 사용이 되었습니다. 

알 낳기 프로그램에서는 키 입력을 닭 버튼 컴포넌트가 받고 오른쪼깅나 스페이스바 키를 누르게 되면 해당 닭 버튼이 입력을 받게됩니다. 

 

 

 

 

Start 버튼: 

 

스타트 버튼을 누르면 view에서 입력한 delay를 망나니에게 반영합니다. 그 후에 플레이어버튼들의 개별적인 스레드를 진행합니다. 실행 환경 초기화가 자바 환경마다 다르기 때문에 bt3에 한번 더 포커스를 강제지정 해줍니다. 

 

 

 



MVC: Model, View, Controller.

Controller: View로부터 입력을 받아, Model을 변경하고, 그 결과를 View에 반영합니다.

 

 내 망나니 실습 코드에서는 책임이 혼재되어있다는 문제점이 있습니다.
player코드가 model이고 playerbutton은 버튼으로서의 플레이어 객체. Myview1이 view와 controller 역할을 가졌습니다. 완벽하게 각 기능을 구분 하지 못해, 구분한다면 어떻게 구분해야할지 생각해보았습니다. 

 

PlayerButton이 너무 많은 책임을 가지고 있고, Myview1이 view와 controller역할을 함께 가지고 있다는 문제가 있습니다.

 

 

cluade에게 구조 개선을 요청했을 때 얻은 답변

claude가 제안한 방식은 mvc를 잘 적용한 구조입니다. 

view에서는 순수 UI만 관리하고, controller에서 전체적인 제어를 책임집니다. controller안에는 game로직을 제어하는 controller와 개별 플레이어를 제어할 수 있도록 playercontroller를 관리합니다. 쓰레드 실행이 이루어집니다.

=> model과 view업데이트를 controller에서 책임집니다. 


해당 실습을 통해서 event에 대해 이해할 수 있었고, MVC 구조에대해 이해할 수 있었습니다. 


++ 엔터를 눌러도 종료가 되기도 하고 안되는 경우가 발생하는것을 확인

 

- 기본 클릭 이벤트만 추가해도, 엔터나 스페이스에 반응을 한다. (윈도우가 열리자마자 엔터를 누르면 bt3가 아닌 start버튼에서 시작이 된다. 자동으로 첫 번째 컴포넌트에 포커스를 주기 때문이다.)

예시로 알낳기에서 닭이 버튼의 기본 클릭 이벤트만 있어도 엔터나 스페이스에 반응을 한다. 

  • Enter 키: 포커스가 버튼에 있을 때 Enter 키를 누르면 ActionEvent가 발생합니다.
  • 스페이스 키: 마찬가지로 JButton에 포커스가 있을 때 Space 키를 누르면 클릭 이벤트가 발생합니다.


해결 방법 : SwingUtilities.invokeLater()사용 
SwingUtilities.invokeLater(() -> bt3.requestFocusInWindow());