일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
- DispatcherServlet
- 스프링요청반응
- 불변객체
- 검증 실패 예외처리
- equals
- rest api
- biblecash
- 동기비동기블로킹논블로킹
- multiparfile데이터
- 옵티마이저
- fcm데이터구조
- 옵티마저
- 래퍼클래스
- 중첩클래스
- 프로세스 생성
- 왜불변객체인가
- HttpServlet
- fcm성능비교
- fcmv1
- 클라이언트요청반응
- 데이터베이스파서
- httpservlet기술
- 공유기작동방식
- java enum
- Wrapper class
- 동등성동일성
- HTTP프로토콜
- multipart바인딩
- 디스패처서블릿
- rest api 검증
- Today
- Total
개발은 아름다워
[ Java ] socket을 이용한 실시간 채팅 프로그램을 만들어보자 (ver1) 본문
설계가 중요
내 생각에 객체지향 프로그래밍에서 가장 첫번째로 되어야할 것은 바로 설계인것 같다.
왜냐하면 설계를 통해 시스템 요구사항의 맥락을 파악하고, 그 맥락의 책임을 담당할 객체를 만들어 프로그래밍을 할 수 있기 때문이다.
책임을 담당하는 객체를 만드는 것은 추후 유지,보수에도 매우 중요하다.
변경사항 또는 문제가 생겼을 때, 그 책임을 담당하는 객체를 찾으면 되는 것이기 때문이다.
글의 순서는 이렇게 된다.
채팅프로그램 요구사항 이해 -> 요구사항을 바탕으로 객체 및 책임 설정 -> 코드 구성
채팅 프로그램 요구사항
- name을 입력하면 채팅방에 입장됨
- client가 입장하면 다른 client들에게 입장한 client의 name과 함께 입장 메세지가 전달됨
- 메세지를 입력하면 client들에게 메세지가 전달됨
- 채팅방을 나가면 client들에게 퇴장했음이 메세지가 전달됨
위와 같은 요구사항이 있다고 한다.
이제 요구사항의 처리할 수 있도록 객체와 책임을 만들면 된다.
요구사항을 바탕으로 객체 및 책임을 설정
"누가? 어떻게 할 수 있을까?"
난 이 두가지 질문으로 요구사항을 해결하기 위한 객체 만들고 책임을 설정할 것이다.
1. 실시간 채팅을 하기 위해서는 무엇이 필요할까?
- 누가?
일단 채팅을 하기 위해서는 client와 server가 필요하다.
-> clietnt와 server가 필요함
- 어떻게 채팅을 할 수 있을까?
client는 네트워크를 통해 server에 메세지를 보내야하고 server는 메세지를 받아야한다.
server는 받은 메세지를 server와 연결된 client들에게 전달해야한다.
이를 위해서는 client 와 server가 연결되어야한다.
어떻게 client와 server를 연결할 것인가?
-> client와 server를 연결하고 관리하는 socket을 이용하면 된다. 따라서 연결을 위해 socket을 사용할 것이다.
이 떄, TCP 소켓을 쓸 것이다. 왜냐하면 실시간 채팅은 client와 server 연결되어 있을때만 채팅을 보내는 것이고, 연결이 끊길 경우 채팅을 보낼 수 없게 만들어야하기 때문이다.
- 그렇다면 TCP socket은 어떻게 데이터를 주고 받는가?
TCP socket의 경우 byte stream 사용하여 데이터를 송수신한다.
socket은 데이터를 byte[] 로 주고 받기 때문에, 데이터를 byte[] 인코딩 또는 디코딩해야하는 문제가 발생한다.
이 문제를 해결하기 위해 socket으로 데이터를 송신할때는 DtataOutputStream , 데이터를 수신할때는 DataInputStream을 사용할 것이다.
2. 접속한 유저들에게 어떻게 메세지들을 전달할 수 있을까?
요구사항에 접속하게 되면 접속했음을 다른 유저들에게 알린다.
채팅과 퇴장시에도 다른 유저들에게 알린다.
다른 유저들에게 메세지를 어떻게 전달할 수 있을까?
- 누가 메세지를 받고 메세지를 전달하는가?
client들과 연결된 server의 socket들이 메세지를 받는다.
받은 메세지들을 server와 socket으로 연결된 다른 client들에게 전달해야한다.
-> 클라이언트들와 각각 연결된 서버의 소켓이 메세지를 전달한다.
--> 서버의 소켓이 클라이언트마다 생겨나야 한다. 또한 클라이언트마다 연결된 서버의 socket들이 각각 따로 동작해야 하므로, socket을 받아 멀티 스레드로 동작하는 session을 만들 것이다.
- 어떻게 클라이언트들에게 메세지를 전달할 수 있을까?
server에는 client들과 socket으로 연결된 session들이 존재한다.
session들에게 메세지를 전달하면 된다.
-> session을 모아서 관리하는 sessionManager 를 만들어서 session들에게 메세지들 전달할 수 있도록 한다.
또한 sessionManager에서는 client들 각각 socket으로 연결된 session들에게 메세지를 전달한다.
client는 session이 보내는 데이터를 실시간으로 받을 수 있는 상태가 되어야한다.
--> client는 데이터를 보낼때와 받을때의 thread를 분리하도록 한다.
객체를 설정하고 큰 틀은 완성이 되었다.
이제 요구사항을 세밀하게 분석하여 각 객체에게 책임을 설정한다.
요구사항 면밀 분석
- name을 입력하면 채팅방에 입장됨
- client가 입장하면 다른 client들에게 입장한 client의 name과 함께 입장 메세지가 전달됨
- message를 입력하면 client들에게 메세지가 전달됨
- exit를 입력하면 client들에게 퇴장했다는 메세지가 전달됨
위의 요구사항을 보면 공통적으로 client가 server에 데이터를 보내는 것이다.
-> client는 외부로부터 IP주소와 port번호를 받아 socket 생성하고 server와 연결한다.
-> socket을 이용하여 데이터를 주고 받기 위해 DataInputStream과 DataOutputStream을 생성한다.
-> 생성된 DataInputStream과 DataOutputStream을 WriteHandler와 ReadHandler에 넘겨준다.
-> WriteHandler와 ReadHandler을 스레드로 실행한다.
--> WriteHandler는 데이터를 server에 전달하고, ReadHandler는 server에서 데이터를 받는다.
CRC 카드로 보면 다음과 같다.
서버에 데이터를 보낸다라는 책임이 세분화 되고, 각각의 객체에게 세분화 된 책임이 설정되었음을 알 수 있다.
코드에서 보겠지만 writeHandler에서 server로 데이터를 보낼 때, name,message,exit 입력에 따라 분기처리해서 보낼 것이다.
다음은 server쪽이다.
client가 분기처리하여 보낸 데이터를 받아서 처리 한 후 다시 각 session들에게 전달한다.
Server는 외부로부터 port를 받아 serverSocket을 생성한다.
-> serverSocket의 accept()가 호출되면 backlog queue에서 TCP 연결 정보를 조회한다.
-> 해당 정보를 기반으로 client와 연결된 socket 객체가 생성된다.(TCP 연결 정보가 없다면 생성될때까지 대기함)
-> 생성된 socket으로 session이 만들어지고, session은 socket으로 DataOutputStream와 DataInputStream을 만들어 client로부터 데이터를 주고 받는다.
-> session은 받은 데이터를 sessionManager에게 전달하고, sessionManager는 각 session들에게 메세지를 전달한다.
CRC카드로 보자면 다음과 같다.
client에서 분기 처리하여 보낸 데이터는 마찬가지로 server의 session에서 분기처리 할 것이다.
코드!
import java.io.*;
import java.net.Socket;
/**
* clinet의 책임
* - server와 socket으로 연결
* - server에 데이터를 송신하기 위한 DataOutputStream과 WriteHandler 생성
* - server로부터 데이터를 수신하기 위한 DataInputStream과 ReadHandler 생성
* - Socket,DataInputStream, DataOutputStream은 프로세스 외부의 자원이므로 종료시 반드시 자원 정리를 해야함
*/
public class Client {
private final Socket socket;
private final DataOutputStream output;
private final DataInputStream input;
private boolean closed = false;
public Client(String ipAddress, int port) throws IOException {
this.socket = new Socket(ipAddress,port);
this.input = new DataInputStream(socket.getInputStream());
this.output = new DataOutputStream(socket.getOutputStream());
}
public void start() throws IOException {
WriteHandler writeHandler = new WriteHandler(output,this);
ReadHandler readHandler = new ReadHandler(input,this);
Thread writeThread = new Thread(writeHandler);
Thread readThread = new Thread(readHandler);
writeThread.start();
readThread.start();
}
public synchronized void closeAll() {
if(closed){
return;
}
closed = true;
close(input,output,socket);
}
private static void close(InputStream input,OutputStream output,Socket socket){
if(input != null){
try {
input.close();
} catch (IOException e){
e.printStackTrace();
}
}
if(output != null){
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Scanner;
/**
* WriteHandler의 책임
* - scanner로 데이터를 입력 받은 후, 입력 받은 데이터를 server로 보내야함
* - 입력에 따라 분기 처리 만듦
* -- name 입력시 server로 "/join" + name 이 전송 또한 name을 입력하지 않으면 다음 단계로 넘어가지 않음
* -- /exit 입력시 "/exit" 를 server로 전송 후 반복문 종료
* -- /message + 내용 입력시 "/message내용" 이 server로 전송
* - scanner 자원을 정리해야한다.
* - client에게 외부자원 정리를 요청한다.
* */
public class WriteHandler implements Runnable {
private final DataOutputStream output;
private final Client client;
private boolean closed = false;
public WriteHandler(DataOutputStream output,Client client) {
this.output = output;
this.client = client;
}
@Override
public void run() {
try {
Scanner scanner = new Scanner(System.in);
String name;
do{
System.out.println("이름을 입력하세요.");
name = scanner.nextLine();
}while (name.isEmpty());
output.writeUTF("/join" + name);
while (true){
String received = scanner.nextLine();
if(received.equals("/exit")){
output.writeUTF("/exit");
break;
}
if(received.contains("/message")) {
output.writeUTF(received);
} else {
System.out.println("명령어를 다시 입력하세요");
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
close();
client.closeAll();
}
}
// scanner 자원 종료를 위한 close
public synchronized void close(){
if(closed){
return;
}
try {
System.in.close(); // scanner 입력 중지 - 사용자의 입력을 닫음
} catch (IOException e) {
e.printStackTrace();
}
closed = true;
}
}
import java.io.DataInputStream;
import java.io.IOException;
/** ReadHandler의 책임
* - server로부터 데이터를 받는다.
* - 연결 종료시 client에게 외부자원 정리를 요청한다.
* */
public class ReadHandler implements Runnable {
private final DataInputStream input;
private final Client client;
public ReadHandler(DataInputStream input,Client client) throws IOException {
this.input = input;
this.client = client;
}
@Override
public void run() {
try {
while (true){
String inputFromServer = input.readUTF();
System.out.println(inputFromServer);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
client.closeAll();
}
}
}
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/** Server의 책임
* - serverSocket을 통해 client의 socket과 연결된 socket을 생성한다.
* - 생성된 socket을 session에 넘긴 후 Thread로 session을 실행한다.
* */
public class Server {
private final ServerSocket serverSocket;
private final SessionManager sessionManager;
public Server(ServerSocket serverSocket, SessionManager sessionManager) {
this.serverSocket = serverSocket;
this.sessionManager = sessionManager;
}
public void running(){
try {
while (true){
Socket socket = serverSocket.accept();
Session session = new Session(socket,sessionManager);
Thread thread = new Thread(session);
thread.start();
}
} catch (IOException e){
e.printStackTrace();
}
}
}
import java.io.*;
import java.net.Socket;
/** Session의 책임
* - socket으로 DataInputStream과 DataOutputStream 을 생성한다.
* - client로부터 받은 데이터에 따라 분기 처리한다.
* - Socket,DataInputStream, DataOutputStream은 프로세스 외부의 자원이므로 종료시 반드시 자원 정리를 해야함
* */
public class Session implements Runnable{
private String name ="";
private final Socket socket;
private final DataInputStream input;
private final DataOutputStream output;
private final SessionManager sessionManager;
private boolean closed = false;
public Session(Socket socket,SessionManager sessionManager) throws IOException {
this.socket = socket;
this.input = new DataInputStream(socket.getInputStream());
this.output = new DataOutputStream(socket.getOutputStream());
this.sessionManager = sessionManager;
}
@Override
public void run() {
try {
while (true) {
String received = input.readUTF();
if(received.startsWith("/join")){
String receivedName = received.replace("/join", "").trim();
setName(receivedName);
sessionManager.add(this);
sessionManager.sendMessages(getName() + "님이 채팅방에 입장하셨습니다.");
} else if (received.startsWith("/exit")) {
sessionManager.sendMessages(getName() + "님이 채팅방을 나가셨습니다.");
break;
} else if (received.startsWith("/message")) {
String message = received.replace("/message","").trim();
sessionManager.sendMessages(getName() + " " +message);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
sessionManager.remove(this);
} catch (IOException e) {
e.printStackTrace();
}
closeAll();
}
}
public synchronized void closeAll() {
if(closed){
return;
}
closed = true;
close(input,output,socket);
}
private static void close(InputStream input, OutputStream output, Socket socket){
if(input != null){
try {
input.close();
} catch (IOException e){
e.printStackTrace();
}
}
if(output != null){
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void sendMessage(String messages) throws IOException {
output.writeUTF(messages);
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public DataOutputStream getOutput() {
return output;
}
}
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class SessionManager {
private List<Session> sessions = new ArrayList<>();
public synchronized void add(Session session) throws IOException {
sessions.add(session);
}
public synchronized void remove(Session session) throws IOException {
sessions.remove(session);
}
// 연결 된 각 세션들에게 메세지를 전달하는 책임
public synchronized void sendMessages(String message) throws IOException {
for(Session session:sessions){
session.sendMessage(message);
}
}
}
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/** SessionManager의 책임
* - List에 session들을 추가한다.
* - List에서 session을 삭제한다.
* - List에 있는 각 session들에게 메세지를 전달한다.
* */
public class SessionManager {
private List<Session> sessions = new ArrayList<>();
public synchronized void add(Session session) throws IOException {
sessions.add(session);
}
public synchronized void remove(Session session) throws IOException {
sessions.remove(session);
}
public synchronized void sendMessages(String message) throws IOException {
for(Session session:sessions){
session.sendMessage(message);
}
}
}
정리
채팅 프로그램의 요구사항의 문맥들을 파악한 후 문맥들을 책임질 객체를 만들었다. 요구사항의 문맥을 세분화 시킬 수 록 그 문맥을 책임 객체가 구체적으로 정해진다. 특히 요구사항을 객체의 책임과 협력으로 만들어가는 과정이 무척이나 오래걸렸다. 하지만 객체의 책임을 명확하게 정함으로써 어떤 객체가 어떻게 동작하는지 알게 되고, 어떻게 협력을 하면 되는지 조금 알게 된 것 같다. 다음에는 이 코드들을 리펙토링 해볼 예정이다.
참고
김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션 강의 | 김영한 - 인프런
김영한 | I/O, 네트워크, 리플렉션, 애노테이션을 기초부터 실무 레벨까지 깊이있게 학습합니다. 웹 애플리케이션 서버(WAS)를 자바로 직접 만들어봅니다., 국내 개발 분야 누적 수강생 1위, 제대로
www.inflearn.com
오브젝트 - 기초편 강의 | 조영호 - 인프런
조영호 | 책임 주도 설계 방법으로 대표되는 객체지향 설계 방법을 학습하고 응집도, 결합도, 캡슐화 관점에서 설계를 트레이드오프하는 방법을 살펴봅니다., 객체지향 설계의 핵심을 담았다!이
www.inflearn.com
'자바' 카테고리의 다른 글
[ Java ] 불변객체는 왜 필요할까? (1) | 2024.10.24 |
---|---|
[ Java ] Java는 왜 callbyValue만 되는걸까? (1) | 2024.10.24 |
[ Java-OOP(1) ] 객체를 쓴다고 객체지향 프로그래밍이 아니다! - 절차지향과 객치향의 차이 (0) | 2024.10.18 |
[ Spring ] 고대의 서블릿을 찾아서(4) 수문장인 FrontController의 등장 (0) | 2024.10.12 |
[ Java ] 제네릭은 무엇이며 왜 쓰는걸까? (1) | 2024.10.12 |