교재 실습/자바 웹 개발 워크북

3. 클라이언트·서버 애플리케이션

Jint 2021. 12. 28. 20:39

클라이언트와 서버간의 통신을 통한 애플리케이션의 기능 분리를 살펴봤다.

package lesson01.exam02.server;

import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class CalculatorServer {
	private int port;
	
	public CalculatorServer(int port) {//8888
		this.port = port;
	}
	
	@SuppressWarnings("resource")
	public void service() throws Exception {
		ServerSocket serverSocket = new ServerSocket(port);
		System.out.println("CalculatorServer startup:");
		
		Socket socket = null;
		
		while(true) {
			try {
				System.out.println("waiting client...");
				
				//클라이언트와 연결되면 클라이언트의 요청처리 → 무한반복
				socket = serverSocket.accept();
				System.out.println("connected to client.");
				
				processRequest(socket);
				System.out.println("closed client.");
				
			} catch (Throwable e) {
				System.out.println("connection error!");
			}
		}
	}
	
	private void processRequest(Socket socket) throws Exception {
		//클라이언트 소켓으로부터 입출력 위한 스트림 객체 생성
		Scanner in = new Scanner(socket.getInputStream());
		PrintStream out = new PrintStream(socket.getOutputStream());
			
		String operator = null;
		double a, b, r;
		
		while(true) {
			try {
				operator = in.nextLine();
				
				if (operator.equals("goodbye")) {
					out.println("goodbye");
					break;
					
				} else {
					a = Double.parseDouble(in.nextLine());
					b = Double.parseDouble(in.nextLine());
					r = 0;
				
					switch (operator) {
					case "+": r = a + b; break;
					case "-": r = a - b; break;
					case "*": r = a * b; break;
					case "/": 
						if (b == 0) throw new Exception("0 으로 나눌 수 없습니다!");
						r = a / b; 
						break;
					default:
						throw new Exception("해당 연산을 지원하지 않습니다!");
					}
					out.println("success");
					out.println(r);
				}
				
			} catch (Exception err) {
				out.println("failure");
				out.println(err.getMessage());
			}
		}
		
		try {out.close();} catch (Exception e) {}
		try {in.close();} catch (Exception e) {}
		try {socket.close();} catch (Exception e) {}
	}
	
	public static void main(String[] args) throws Exception {
		CalculatorServer app = new CalculatorServer(8888);
		app.service();
	}
}

CalculatorServer에서 계산기 서버를 실행한다. 클라이언트가 연결되면 클라이언트가 보낸 요청을 processRequest(Socket socket)에서 처리한다.

 

package lesson01.exam02.client;

import java.awt.Container;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTextField;

@SuppressWarnings("serial")
public class CalculatorFrame extends JFrame implements ActionListener {
	
	//선언
	CalculatorAgent calcAgent;
	JTextField operand1 = new JTextField(4);
	JTextField operator = new JTextField(2);
	JTextField operand2 = new JTextField(4);
	JButton equal = new JButton("=");
	JTextField result = new JTextField(6);
	JButton clear = new JButton("Clear");
	
	//서버 연결하며 계산기 생성
	public CalculatorFrame() {
		try {
			calcAgent = new CalculatorAgent("localhost", 8888);
		} catch (Exception err) {
			JOptionPane.showMessageDialog(
					null, err.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
			System.exit(0);
		}
		
		this.setTitle("Lesson01-Exam02");
		
		Container contentPane = this.getContentPane();
		contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.Y_AXIS));
		
		contentPane.add(Box.createVerticalGlue());
		contentPane.add(this.createInputForm());
		contentPane.add(this.createToolBar());
		contentPane.add(Box.createVerticalGlue());
		
		this.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				calcAgent.close();
				System.exit(0);
			}
		});
		
		this.pack();
		this.setLocationRelativeTo(null);
	}
	
	@Override
	public void actionPerformed(ActionEvent event) {
		if (event.getSource() == equal) {
			compute();
		} else {
			clearForm();
		}
	}
	
	private void compute() {
		double a = Double.parseDouble(operand1.getText()); 
		double b = Double.parseDouble(operand2.getText());
		double r = 0;
		
		try {
			r = calcAgent.compute(operator.getText(), a, b);
			result.setText(Double.toString(r));
			
		} catch (Exception err) {
			JOptionPane.showMessageDialog(
				null, err.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
		}
	}
	
	private void clearForm() {
		this.operand1.setText("");
		this.operand2.setText("");
		this.result.setText("");
	}
	
	
	private Box createInputForm() {
		Box box = Box.createHorizontalBox();
		box.setMaximumSize(new Dimension(300, 30));
		box.setAlignmentY(Box.CENTER_ALIGNMENT);
		box.add(operand1);
		box.add(operator);
		box.add(operand2);
		box.add(equal);
		box.add(result);
		equal.addActionListener(this);
		return box;
	}
	
	private Box createToolBar() {
		Box box = Box.createHorizontalBox();
		box.add(clear);
		clear.addActionListener(this);
		return box;
	}
	
	public static void main(String[] args) {
		CalculatorFrame app = new CalculatorFrame();
		app.setVisible(true);
	}
}

CalculatorServer에서 서버가 실행되어 있다면 CalculatorFrame에서 main()메소드를 호출하게 되면 이벤트가 걸린 계산기를 생성한다. 이 계산기의 =나 clear 버튼을 누르면 이벤트가 실행되어 compute()메소드가 실행되어 CalculatorAgent에 사용자가 입력한 숫자와 연산자를 가져간다.

 

package lesson01.exam02.client;

import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

public class CalculatorAgent {
	Socket socket = null;
	PrintStream out = null;
	Scanner in = null;
	
	//소켓을 통해 입출력할 수 있도록 스트림 객체 준비
	public CalculatorAgent(String ip, int port) throws Exception {
		socket = new Socket(ip, port);
		out = new PrintStream(socket.getOutputStream());
		in = new Scanner(socket.getInputStream());
	}
	
	public double compute(String operator, double a, double b) throws Exception {
		try {
			out.println(operator);
			out.println(a);
			out.println(b);
			out.flush();
			
			String state = in.nextLine();
			if (state.equals("success")) {
				return Double.parseDouble(in.nextLine());
			} else {
				throw new Exception(in.nextLine());
			}
		} catch (Exception e) {
			throw e;
		} 
	}
	
	public void close() {
		try {
			out.println("goodbye");
			System.out.println(in.nextLine());
		} catch (Exception e) {}
		
		try {out.close();} catch(Exception e) {}
		try {in.close();} catch(Exception e) {}
		try {socket.close();} catch(Exception e) {}
	}
}

CalculatorAgent에서는 입출력 스트림을 통해 서버로 요청을 전달하고 요청 결과를 받아 반환하여 CalculatorFrame로 전달한다.

 

이 예제를 통해 클라이언트·서버 구조를 살펴봤다. 서버에서는 계산을 수행하고 그 결과를 클라이언트에 보내주는 방식으로 신규 연산자가 추가되더라도 서버쪽만 변경하면 된다. 클라이언트는 바뀌지 않기에 다시 설치할 필요가 없다. 이런 구조는 기능 변경이나 추가에 유연하게 대처할 수 있다.

 

하지만 한 번에 하나의 클라이언트하고만 연결이 가능하여 현재 연결된 클라이언트와 연결이 끊어질 때까지 다른 클라이언트는 기다려야 하는 단점이 있다. 이 문제를 해결하기 위해 멀티 프로세스와 멀티 스레드와 같은 병행처리 방식이 도입된다.

 

- 멀티 프로세스 : 클라이언트가 연결 요청 시 서버 프로그램을 복제하여 클라이언트에 대응. 원본 프로세스의 메모리를 모두 복제하기 때문에 자원 낭비가 심함.

- 멀티 스레드 : 클라이언트 요청을 처리하는 일부 코드만 별도로 분리하여 실행. 메모리 낭비 적음. 독립적으로 실행되기 때문에 다른 클라이언트 요청을 받아 병행 처리 가능.

 

참고도서 : https://freelec.co.kr/book/1674/

 

[열혈강의] 자바 웹 개발 워크북

[열혈강의] 자바 웹 개발 워크북

freelec.co.kr