JAVA

JavaFX GUI활용한 프로그램 개발

동빈나님의 유투브를 활용하여 개발을 진행하였습니다.

https://www.youtube.com/watch?v=0gMYlUppe-c&list=PLRx0vPvlEmdBtvcSqVkjeL1MwSfRLENYc 

 

Client와 Server를 나누어서 코딩을 진행하였기에, 두 개의 Java FX Project가 필요했다. 

1. ChatServer Project

1) Main.java

하나의 서버 프로그램은 하나의 서버 모듈로 동작하도록 개발하려 한다. 

ExecutorsService 쓰레드 풀을 활용하여, 다양한 client가 접속했을 때, Thread들을 효과적으로 관리할 수 있도록 하였다. 다양한 client가 동시에 접속하려 할 때 마다 Thread들을 만들어야 한다면 성능에 영향을 끼칠 수 있기에 미리 쓰레드들을 만들어 놓고 이에 대비하기 위함이었다. (ExecutorService는 여러 개의 쓰레드들을 효율적으로 관리하기 위해 사용하는 대표적인 Library이다.) 

 

접속한 클라이언트를 관리하기 위해 동적 배열 vector로 client 객체를 담아서 관리하였고, server단이기에 serverSocket을 이용하여 클라이언트 연결을 기다리는 소켓을 만들었다.

	public static ExecutorService threadPool;
	public static Vector<Client> clients = new Vector<Client>();
	ServerSocket serverSocket;

startServer() 함수는 서버를 구동시켜서 클라이언트 연결을 기다리는 메소드이다. 서버가 실행된다면 serverSocket 객체를 활성화하여, 자신의 IP주소와 port번호로 특정 client 접속을 기다릴 수 있게끔, 소켓에 bind 해준 InetSocketAddress를 활용하였다. Exception이 발생하는 경우, 서버 소켓을 닫음으로서 발생할 수 있는 문제들을 막아주었다. 

 

서버가 소켓을 문제 없이 열어서 기다릴 수 있는 상태가 되면, 쓰레드를 만들어서 Client가 접속할떄까지 기다리게 하였다. Runnable을 활용한 thread 객체를 선언 후 while문으로  계속 새로운 client가 접속할 수 있도록 serverSocket.accept()함수로 구현해 주었다. Client가 접속했다면, Client배열에 새롭게 접속한 client를 추가 시켰고, 오류가 발생한 경우, 서버를 빠져나오도록 구현하였다. 이후 ThreadPool에 client를 기다리는 thread를 담아주는 함수를 만들었다.

	public void startServer(String IP, int port) {
		try {
			//서버가 실행이 된다면
			//소켓 통신에서는 socket에 대한 객체를 활성화 해야 한다.
			serverSocket=new ServerSocket();
			//서버의 경우 자신의 IP주소와 port 번호로 특정한 client 접속을 기다릴 수 있다.
			serverSocket.bind(new InetSocketAddress(IP,port));
		}catch(Exception e) {
			e.printStackTrace();
			//서버 소켓이 닫혀있는 경우가 아니면 멈추어 주어야 한다.
			if(!serverSocket.isClosed()) {
				stopServer();
			}
			return;
		}
		//서버가 소켓을 잘 열어서 접속을 기다릴 수 있는 상태가 되었다면
		//하나의 쓰레드를 만들어서 클라이언트가 접속할 때 까지 계속 기다리게 한다.
		Runnable thread= new Runnable() {
			@Override
			public void run() {
				while(true) {
					try {
						//계속 새로운 client 접속 가능하도록
						Socket socket=serverSocket.accept();
						//클라이언트가 접속했다면, 클라이언트 배열에 새롭게 접속한 클라이언트 추가
						clients.add(new Client(socket));
						System.out.println("[클라이언트 접속]"
								+socket.getRemoteSocketAddress()
								+": "+Thread.currentThread().getName());
					}catch(Exception e) {
						//오류 발생 시 serverSocket에 오류가 발생한 것
						if(!serverSocket.isClosed()) {
							stopServer();
						}
						break;//빠져나오기
					}
				}
			}
		};
		//threadPool 초기화
		threadPool=Executors.newCachedThreadPool();
		//client를 기다리는 thread를 담아준다. 그 안에 첫번째 thread로 클라이언트 접속 기다리는 thread
		threadPool.submit(thread);
	}

서버의 동작을 중지시키는 메소드로는 stopServer()함수를 구현하였다. Iterator를 활용하여 현재 동적 배열에 담겨 있는 것들을 하나씩 접근하여 소켓을 닫았고, 동적 배열에서 제거하였다.  모든 client에 대한 연결이 끊긴 이후, 서버 객체 소켓 또한 닫고 threadPool을 종료함으로서 자원을 모두 해제 시켜주었다.

	public void stopServer() {
		try {
			//현재 작동 중인 모든 소켓 닫기
			Iterator<Client> iterator=clients.iterator();
			while(iterator.hasNext()) {
				//하나씩 모든 client 에 접근한다.
				Client client=iterator.next();
				client.socket.close();//클라이언트 소켓 닫기
				//iterator에서도 연결이 끊긴 것 제거해 준다.
				iterator.remove();
			}
			//모든 client에 대한 연결이 끊겼으니, 서버 객체 소켓 또한 닫아준다.
			if(serverSocket!=null && !serverSocket.isClosed()) {
				serverSocket.close();
			}
			//쓰레드 풀 종료하기
			if(threadPool!=null && !threadPool.isShutdown()) {
				threadPool.shutdown();//자원 할당 해제
			}
		}catch(Exception e) {
			e.printStackTrace();
		}
	}

 

UI를 생성하고 실질적으로 프로그램을 동작시키는 method로 start()함수를 구현하였다. argument로 들어오는 stage는 최상위 컨테이너로 GUI의 토대가 되는 컨테이너를 뜻한다. 전체 디자인 틀을 담을 수 있는 하나의 레이아웃인 BorderPane을 선언하고, padding을 주었으며, 출력만 하고 수정이 불가능한 textArea를 BorderPane의 중앙에 위치 시켰다. 시작하기 버튼을 활용하여, 버튼을 BorderPane 하단에 배치시켰으며, IP와 port를 설정하였다.

 

시작하기라는 toggleButton이 눌리게 되는 경우 시작하기라는 버튼이 눌리는 이벤트가 발생하면, 서버를 시작시켰고, 작업 스레드가 직접 UI를 변경 불가하므로 UI 변경이 필요하면 작업 스레드는 UI 변경 코드를 Runnable로 생성하고 이 것을 매개값으로 Platform의 runLater() 정적 메서드를 호출하였다. 이 runLater() 함수를 활용하여 if-else문으로 서버가 시작 된 경우 종료하기로, 서버가 돌아가고 있는 경우 시작하기로 바꿔주었다. 

	//UI를 생성하고, 실질적으로 프로그램을 동작시키는 메소드
	//stage는 최상위 컨테이너로 GUI 기본 토대가 되는 컨데이너이다.
	@Override
	public void start(Stage primaryStage) {
		BorderPane root = new BorderPane(); //한개의 전체 디자인 틀을 담을 수 있는 것을 만듬 일종의 레이아웃
		root.setPadding(new Insets(5));
		
		TextArea textArea=new TextArea();
		textArea.setEditable(false);//출력만 하고 수정은 불가능
		textArea.setFont(new Font("나눔고딕",15));
		root.setCenter(textArea);//중간에 textArea 담는다.
		
		Button toggleButton = new Button("시작하기");
		toggleButton.setMaxWidth(Double.MAX_VALUE);//toggleButton은 일종의 스위치다
		BorderPane.setMargin(toggleButton, new Insets(1,0,0,0));
		root.setBottom(toggleButton);
		
		String IP="127.0.0.1";//자기 자신의 주소
		int port=9876;
		
		//사용자 toggle 눌렀을 떄 event 발생
		toggleButton.setOnAction(event->{
			if(toggleButton.getText().equals("시작하기"))
			{
				startServer(IP,port);
				//바로 쓰지 않고 어떤 GUI 메세지를 출력할 수 있게끔 한다.
				Platform.runLater(()->{
					String message= String.format("[서버 시작]\n", IP, port);
					textArea.appendText(message);
					toggleButton.setText("종료하기");//버튼에 쓰인 정보 바꿔주기
				});
			}
			else{//종료하기 눌린경우
				stopServer();
				//바로 쓰지 않고 어떤 GUI 메세지를 출력할 수 있게끔 한다.
				Platform.runLater(()->{
					String message= String.format("[서버 종료]\n", IP, port);
					textArea.appendText(message);
					toggleButton.setText("시작하기");//버튼에 쓰인 정보 바꿔주기
				});
			}
		});
		
		//윈도우 내에 표시되는 내용들을 구축하는 것.
		Scene scene= new Scene(root,400,400);
		primaryStage.setTitle("[채팅 서버]");//제목 설정
		primaryStage.setOnCloseRequest(event->stopServer());
		primaryStage.setScene(scene);
		primaryStage.show();//Stage에서 구축된 window를 화면에 표시하는 것
	}
	// 프로그램의 진입점
	public static void main(String[] args) {
		launch(args);//javaFX 응용 프로그램 시작
	}

 

2) Client.java

chat server가 한 명의 클라이언트와 통신하도록 해주기 위해 만든 class이다.

멤버 변수로 Socket을 선언하였고, 생성자 안에는 멤버 변수를 초기화 할 수 있게끔 했으며, receive()함수를 통해 Client로부터 message를 전달 받을 수 있도록 만들었다.

Socket socket;
	
//생성자
public Client(Socket socket) {
	this.socket=socket;
	receive();//반복적으로 client로 부터 message 전달 받을 수 있도록 만든다.
}

클라이언트에게 메세지를 전송하는 메소드로는 send()를 활용하였다. 메세지를 전달 하기 위해서 Thread를 활용하였다. 하나의 Thread를 만들 때 Runnable을 사용하여 내부 클래스로 선언하였다.

 

Java에서는 Thread를 작성하기 위해서는 1) Thread클래스를 상속받거나, 2) Runnable 인터페이스를 implements하여 구현하는 방법이 존재한다. 이 중 사용할 Runnable의 경우 무조건적으로 run()이라는 함수를 구현해야 하기에 이를 내부 클래스를 활용하여 구현하였다.

 

메세지를 보내기 위해 OutputStream을 활용하여, String으로 들어왔던 message를 버퍼에 담고 OutputStream으로 write해주었다. 성공적으로 메세지를 보낸 이후에는 flush를 해줌으로서 버퍼를 비워주었다.

 

예외가 발생했을 경우, client 정보를 담고 있는 main함수의 clients 배열에서 현재 존재하는 client를 지워준 후 소켓을 닫음으로서 문제를 대비하였다.

 

예외 없이 성공한 경우, 메세지를 전송하기 위한 thread를 Main의 threadPool에 submit함으로서 추가하였다. 

	//클라이언트에게 메세지를 전송하는 메소드
	public void send(String message) {
		//Runnable library 이용해서 thread 정의 해주고
		Runnable thread= new Runnable(){
			@Override
			public void run() {
				try {
					//OutputStream 사용 이유, 메세지를 보내주고자 할 때는 outputStream으로
					OutputStream out= socket.getOutputStream();
					byte[] buffer = message.getBytes("UTF-8");
					//오류 미 발생 시 서버에서 client로 전송하기 위해서
					//out에서 write 해준다.
					out.write(buffer);
					//성공적으로 여기까지 전송했다는 것을 알리기 위해 반드시 flush 해주어야 한다.
					out.flush();
				}catch(Exception e) {
					try {
						System.out.println("[메세지 송신 오류]"
								+socket.getRemoteSocketAddress()
								+": "+Thread.currentThread().getName());
						//예외 발생 시 메인 함수의 client의 정보를 담든 clients배열에서
						//현재 존재하는 Client를 지워준다.
						Main.clients.remove(Client.this);
						//오류가 생긴 client의 socket을 닫는다.
						socket.close();
					}catch(Exception e2) {
						e2.printStackTrace();
					}
				}
			}
		};
		//Main threadPool에 추가 한다.
		Main.threadPool.submit(thread);
	}

 

소켓을 활용하여, end to end communication을 시키기 위해서는 Exception이 발생할 수 있기에, try~catch 문을 활용하여 주었다. 어떠한 내용을 전달 받을 수 있게끔 InputStream 객체를 활용하였고, 메세지를 정상적으로 받지 않은 경우 IOException을 발생 시켰다.

 

전달 받은 메세지의 경우, String 변수 안에 담되 한글이 가능하도록 UTF-8을 이용하였고, Server에서 전달 받은 메세지이기에 다른 Client에도 보낼 수 있도록하였다.

 

만들어진 Thread는 main함수에 만들어진 ThreadPool에 등록하므로써, 생성된 thread를 안정적으로 관리하도록 등록하였다.

	public void receive() {
		Runnable thread= new Runnable() {
			//내부 클래스 활용, 하나의 쓰레드 만들 때 runnable 사용, 반드시 run 함수 필요
			@Override
			public void run() {
				//예외 발생 가능하기에 try~catch로 구문 잡음
				try {
					//반복적으로 client로 부터 어떠한 내용을 전달 받을 수 있도록 한다.
					while(true) {
						//어떠한 내용을 전달 받을 수 있게끔 InputStream 객체 활용
						InputStream in=socket.getInputStream();
						byte[] buffer= new byte[512];//버퍼 이용 한번에 512바이트 만큼
						//실제로 client로 부터 어떠한 내용을 전달 받아서 buffer에 담아주고 그 길이
						int length=in.read(buffer);
						//만약 메세지의 길이가 -1이라면, 오류가 발생했다면 IOException으로 알림
						while(length==-1) throw new IOException();
						//메세지를 정상적으로 받은 경우
						System.out.println("[메세지 수신 성공]"
								+socket.getRemoteSocketAddress()//client의 IP주소 출력
								+": "+Thread.currentThread().getName());
								//쓰레드 고유 정보 또한 출력
						//전달받은 값을 message에 담되, 한글 가능하도록 UTF-8 이용
						String message= new String(buffer,0,length,"UTF-8");
						//전달 받은 메세지를 다른 client에게도 보낼 수 있도록
						for(Client client: Main.clients) {
							client.send(message);
						}
							
					}
				}catch(Exception e) {
					try {
						System.out.println("[메세지 수신 오류] "
								+socket.getRemoteSocketAddress()
								+": "+Thread.currentThread().getName());
					}catch(Exception e2) {
						e2.printStackTrace();
					}
				}
			}
		};
		//만들어진 thread를 main 함수에 만들어진 threadPool에  등록해준다.
		//만들어진 thread를 안정적으로 관리하기 위해 등록
		Main.threadPool.submit(thread);
	}

2. ChatClient

client단이기에 serverSocket이 아닌 socket과 Text Area를 멤버 변수로 선언하였다. client의 경우 여러개의 thread가 필요하지 않다. startClient()를 활용하여 IP와 port번호를 받아 클라이언트 프로그램 동작 메소드를 구현하였다. 여러개의 thread가 존재하지 않기에, runnable 대신 thread 객첼르 활용하였으며, 내부 클래스로 만든 함수 안에 run()함수를 구현하여 소캣을 생성하고 서버로 부터 메세지를 전달 받을 수 있도록 하였다. 문제가 생기지 않은 경우 thread를 start하므로써 클라이언트 프로그램이 동작할 수 있도록 하였다.

//클라이언트 프로그램 동작 메소드입니다.
	public void startClient(String IP, int port) {
		//여러개의 thread 필요 없기에 runnable 대신 thread 객채 사용
		Thread thread= new Thread() {
			public void run() {
				try {
					socket=new Socket(IP, port);//소켓 새로 생성
					receive();//서버로 부터 메세지 전달 받음
					
				}catch(Exception e) {
					if(!socket.isClosed()) {
						stopClient();//오류 시
						System.out.println("[서버 접솔 실패]");
						Platform.exit();//프로그램 종료
					}
				}
			}
		};
		thread.start();
	}

client 종료를 위해 구현한 stopClient()의 경우 소켓이 열려있는 경우 닫아주는 것을, 서버로 부터 메세지를 전달 받는 메소드인 receive()의 경우 while문을 통해 계속 전달 받을 수 있게 구현 후 socket.getInputStream()을 활용하여 서버로 부터 전달 받은 메세지를 buffer에 입력 받았다. 이후 Platform.runLater()함수를 이용하여 UI에 함수를 적용 시켰다.

//클라이언트 프로그램 종료 메소드
	public void stopClient() {
		try {
			if(socket!=null && !socket.isClosed()) {
				socket.close();
			}
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
	//서버로부터 메세지를 전달 받는 메소드
	public void receive() {
		while(true) {
			//계속 전달 받음
			try {
				InputStream in = socket.getInputStream();//서버로부터 전달 받음
				byte[] buffer= new byte[512];
				int length=in.read(buffer);//read 함수로 실제 입력 받는다.
				if(length==-1) throw new IOException();
				String message= new String(buffer,0,length,"UTF-8");
				Platform.runLater(()->{
					textArea.appendText(message);
				});
			}catch(Exception e) {
				stopClient();
				break;
			}
		}
	}

서버에 메세지를 전달하는 메소드로는 send를 활용하였으며 thread에 run함수를 구현하여, OutputStream을 이용하여 버퍼에 받은 메세지를 담고 적을 수 있도록 하였다. 이후 flush()를 통해 버퍼를 비우므로써 버퍼에서 문제가 생기지 않도록 하였으며, thread에서 문제가 발생하지 않는 경우 start()시켰다.

//서버로 메세지를 전송하는 메소드
	public void send(String message) {
		//서버로 전달하기 위해서도 thread 필요, receive thread와 다름
		Thread thread=new Thread() {
			public void run() {
				try {
					OutputStream out=socket.getOutputStream();
					byte[] buffer = message.getBytes("UTF-8");
					out.write(buffer);
					out.flush();
				}catch(Exception e) {
					stopClient();
				}
			}
		};
		thread.start();
	}

실제로 프로그램을 동작시키는 메소드인 start의 경우, BorderPane위에 수평으로 컨트롤을 배치하는 컨테이너인 HBox을 이용하여 layout을 만들었다. 이 안에 userName, IPText, portText를 적음으로서 연결을 할 수 있도록 도왔다. 입력 textField인 input란을 만들었고, setOnAction 이벤트를 만들어서 send해준 후 칸을 비우고 다시 보낼 수 있도록 focus 설정을 해주었다.

 

접속하기 버튼을 누르는 경우, setOnAction란을 통해 누를 경우, port를 잡을 수 있도록 더 나아가 requestFocus를 통해 다시 버튼을 누를 수 있도록 해 주었다.

	@Override
	public void start(Stage primaryStage) {
		BorderPane root= new BorderPane();
		root.setPadding(new Insets(5));
		
		HBox hbox=new HBox(); //BorderPane 위에 하나 더 layout을 만든다.
		hbox.setSpacing(5);
		
		TextField userName=new TextField();
		userName.setPrefWidth(150);
		userName.setPromptText("닉네임을 입력하세요.");
		HBox.setHgrow(userName, Priority.ALWAYS);
		
		TextField IPText= new TextField("127.0.0.1");
		TextField portText = new TextField("9876");
		portText.setPrefWidth(80);
		
		//hbox 내에 실제로 들어갈 수 있도록
		hbox.getChildren().addAll(userName,IPText,portText);
		root.setTop(hbox);//border padding 위에 넣어줌
		
		textArea=new TextArea();
		textArea.setEditable(false);//수정할 수 없도록
		root.setCenter(textArea);
				
		TextField input= new TextField();
		input.setPrefWidth(Double.MAX_VALUE);
		input.setDisable(true);//접속 전에는 안되도록
		
		input.setOnAction(event->{
			send(userName.getText()+":"+input.getText()+"\n");
			input.setText("");//전송 했으니 메세지 전송 칸 비운다.
			input.requestFocus();//다시 보낼 수 있도록 Focus 설정해준다.
		});
		
		Button sendButton=new Button("보내기");
		sendButton.setDisable(true);
		
		sendButton.setOnAction(event->{
			send(userName.getText()+": "+input.getText()+"\n");
			input.setText("");
			input.requestFocus();
		});
		Button connectionButton = new Button("접속하기");
		connectionButton.setOnAction(event->{
			if(connectionButton.getText().equals("접속하기")) {
				int port=9876;
				try {
					port=Integer.parseInt(portText.getText());
				}catch(Exception e) {
					e.printStackTrace();
				}
				startClient(IPText.getText(),port);
				Platform.runLater(()->{
					textArea.appendText("[ 채팅방 접속]\n");
				});
				connectionButton.setText("종료하기");//접속이 이루어 졌으니
				input.setDisable(false);
				sendButton.setDisable(false);
				input.requestFocus();
			}else{
				//종료하기였다면
				stopClient();
				Platform.runLater(()->{
					textArea.appendText("[ 채팅방 퇴장]\n");
				});
				connectionButton.setText("접속하기");
				input.setDisable(true);
				sendButton.setDisable(true);
			}
		});
		BorderPane pane= new BorderPane();
		pane.setLeft(connectionButton);
		pane.setCenter(input);
		pane.setRight(sendButton);
		root.setBottom(pane);
		Scene scene=new Scene(root, 400,400);
		primaryStage.setTitle("[ 채팅 클라이언트]");
		primaryStage.setScene(scene);
		primaryStage.setOnCloseRequest(event->stopClient());
		primaryStage.show();
		
		connectionButton.requestFocus();
	}
	
	//프로그램의 진입점이다.
	public static void main(String[] args) {
		launch(args);
	}