3. 클라이언트·서버 애플리케이션
클라이언트와 서버간의 통신을 통한 애플리케이션의 기능 분리를 살펴봤다.
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