[OZpc] 프로젝트 3-10일차, 갑자기 회고

·2023년 10월 17일
0

project

목록 보기
1/1

3일차

중요한 건 꺾였지만 계속 하는 마음...

우와...

3일차 작성했던 게 다 날라가 버렸다...

간략하게 요약하자면 멀티 thread로 전환하는 과정에서 오류가 났고, 계속 gui 연결이 안 됐었는데 server 구동을 하면서 gui 가 켜지는 것으로 순서를 바꿨더니 간단하게 해결됐음. 애초에 thread라는 개념을 모르고 코드를 만드는 것에 집착했던 것 같다.

4일차

내가 해야 하는 것

DB 연결

로그인이 되었다? -> 클라이언트에서 값 받기

if (client == 로그인 할 경우){
1. 클라이언트에게 id 받아오기 o
2. DB한테 id 값 주고 남은 time 클라이언트에게 보내주기 o
3. textArea : "48번 @id 님이 로그인하셨습니다." o
}else if (client == 로그아웃 할 경우) {
1. "48번 @id 님이 사용 종료하셨습니다."
2. 마지막 값 DB로 보내기
}

if (client == 시간 추가) {
1. client 에게 값 받아오기
2. server에서 시간 추가해 주기
}else if (server == 시간 추가) {
1. server 에서 값 추가 후 client 로 보내 주기
}

서버와 클라이언트 1분마다 소통, client한테 시간 보내 주기

4일차에 마친 일

if (client == 로그인 할 경우){
1. 클라이언트에게 id 받아오기 (o)
2. DB한테 id 값 주고 남은 time 클라이언트에게 보내주기 (o)
3. textArea : "48번 @id 님이 로그인하셨습니다." (o)
}else if (client == 로그아웃 할 경우) {
1. "48번 @id 님이 사용 종료하셨습니다." (0)
2. 마지막 값 DB로 보내기
}

if (client == 시간 추가) {
1. client 에게 값 받아오기
2. server에서 시간 추가해 주기
}else if (server == 시간 추가) {
1. server 에서 값 추가 후 client 로 보내 주기
}

클라이언트에게 값이 넘어온다면, 그 클라이언트가 로그인 할 건지, 로그아웃 할 건지, 시간 추가를 한 건지, 메뉴를 주문할 건지 구분이 안 되는 문제점이 있었다.

이 문제는 client가 소통을 할 때마다 객체를 넘겨 주는 것으로 수정하여 해결하였다.
client가 객체를 넘기면 server가 그 객체를 까보고 메서드대로 행동하는 것이다.

우선 client를 기다리는 동안... 객체를 넘기면 어떻게 처리할 건지 고민하는 게 4일까지의 일이었고,

기다리는 동안 db 를 수정했다.
member 값을 다 넣어서 테스트용 내 member를 넣었고, DB 구축까지 마쳤다.

5일차

내가 해야 하는 일

우선 tcp 서버를 구축할 때는 hashtable로 값을 저장하는 게 동시성 문제가 일어나지 않아 편하다고 해서 hashtable로 넘어온 객체와 소켓을 저장하려고 한다.

  1. 문제점 1

그런데 내가 고민하는 문제는... hashtable로 <객체, 클라이언트의 각 소켓> 을 저장하고 나면 객체의 값이 바뀌어 버렸을 때 어떻게 구분하지?

id같은 경우는 string 값으로 변하지 않으니까 바뀌어도 상관없지만 객체의 값은 계속 클라이언트에게 받고 있는데 그걸 어떻게 특정해서 값을 수정해 주지?

이걸 고민해 봐야 한다.

그리고 server 구성을 정리해 보아야 한다.

지금은 로그인, 로그아웃, 메뉴추가, 시간 추가 메서드가 서버에 한꺼번에 구현이 되어 있는데 가독성이 좋지 않고 코드를 짠 나조차도 알아먹기 번거롭다.

그리고 thread와 시간 흐름 메서드도 한꺼번에 구현되어 있기 때문에 더 번거롭다.

  1. 문제점 2

그리고 두 번째 문제점은 스레드가 멈추지 않는다는 것이다.

 public class SenderThread extends Thread {
    	private boolean stop = false, add = false;
    	private int time, money;
    	private String id;
    	private int no = 0;
    	
    	public SenderThread(int time, String id) {
    		this.time = time;
    		this.id = id;
    	}
    	
    	public void setStop(boolean stop) {
    		this.stop = stop;
    	}
    	
    	public void setAdd(boolean add) {
    		this.add = add;
    	}
    	
    	public void setMoney(int money) {
    		this.money = money;
    	}
    	
    	public int getTime() {
    		return stop_time;
    	}
    	
    	public void run() {
       	 while (stop == false) {
  	   		if (stop == true) {
		       	 stop_time = time;
		       	 serverGUI.ititial_seat(3);
		       	 break;
	        }else if (time <= 0) {
                // 시간이 0 이하이면 스레드를 종료
                break;
                // 그리고 시간 종료되었습니다 뜨게
            }else if (add == true) {
           	 time += 60*money;
           	 serverGUI.addMessageToAlarm(id + "님이 " + money*60 + "분 추가했습니다.\n");
           	 add = false;
            }
       		
             pw.println(time);
             pw.flush();
             
             System.out.println("클라이언트로 시간 전달 성공! : " + time/60);
             serverGUI.set_start_seat(3, time/60);	// 시간을 분으로 전송


             try {
                 Thread.sleep(60000);	// 60초동안 대기 했다가 시간을 업데이트 하고 전송한다
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }

             time -= 60; // 시간을 60초씩 감소
             
         }
       	 
       	 System.out.println("break 하였음");
    	}
    	
    }

이런 스레드로 만들었는데 다른 곳에서 setstop(true)로 바꾸어 줘도 멈추지 않고 스레드가 계속된다. 이건 뭐가 문제지?

while 문 제일 앞에 if 문을 추가해서 stop 이 true인 것을 확인해서 바뀌도록 만들었는데 다른 곳에서 setstop문을 바꾸어 줘도 인식하지 못한다. 제일 밑에 stop = true 을 바꾸면 되는데

내 생각에는 이게 객체를 구분 못해서 그런 것 같다. 어떤 client의 객체인지 제대로 모르는 것이다!!!

그런데 그걸 어떻게 구분해 주냐... ht로 구분해 주는 거겠지.
일단 해 보자.

해결 과정

그래서 hashTable에 key값을 id, 해당하는 객체와 thread를 values 값에 저장해 객체를 구분할 수 있도록 만들었다.

프로젝트를 마치며...

우선 프로젝트 완성은 깃허브에 올려둘 것이다.
오즈 피시를 만들며 가장 많이 느꼈던 점은 내가 자바 gui코드를 공부하며 선생님의 코드를 지속적으로 베끼고, 그대로 사용하기만 하는 공부를 하고 있었다는 거.
능동적으로 공부하고 제대로 기능 구현을 해 보니까 확실히 알 수 있었던 게 나는 애초에 객체 개념과 thread 를 이해하지 못하고 있었던 것을 깨달았다.
이번 기회로 "객체 지향 언어" 가 무엇을 말하는 건지 많이 공부하고 이해할 수 있게 되었고,
서버를 맡아 구현하게 되어 데이터 이동도 알 수 있게 되었다.

또한 팀프로젝트에 대해서도 많이 배우게 되었다. pc방이라는 아이디어 자체를 내가 짜갔고 준비도 많이 해갔다고 생각했는데 프로젝트를 진행하면서 느낀 것은, 미리 계획하고 방향성을 수립하지 않으면 산으로 간다는 거였다.

pc방 프로젝트의 시점은 서버에 맞추어져 있어야 정상이다. 클라이언트의 요청을 받으면 서버가 디비를 수정하고, 값을 가져와 클라이언트에게 주어야 정상인데 Ozpc방은 클라이언트에서 DB를 처리하는 등 서버가 할 일을 클라이언트가 하고 있다.

또한 client 객체 하나에 모든 기능을 구현해서 id 별로 객체를 관리했으면 훨씬 더 client 관리를 쉽게 할 수 있었는데 지금은 서버가 client를 받으면 시간과 채팅 thread를 따로 관리해서 오류가 났을 때 대체 어디서 난 거지? 하고 처음부터 코드를 되짚는 일이 잦았다.

이러한 일들은 uml 과 usecase를 배우고 적용해서 먼저 내가 구현하고 싶은 것을 명확하게 구분한 다음 코딩을 시작하지 않아서 벌어진 것 같다.

그리고... 나한테 프로젝트를 시작하기 앞서 내가 가장 걱정하고, 부족하다고 생각하는 팀원들과의 관계.

너무 운이 좋게도 이번에는 책임감이 강해 맡은 바를 열심히 하고, 소통이 원할하게 이루어지는 팀원들을 만나 팀원들과의 불화나 소통 불능으로 인해 프로젝트에 해가 되는 일은 전혀 전혀 전혀 없었다.

이는 우리의 정리되진 않았어도 pc방 프로그램이라는 가시적이고 명확한, 그리고 일상생활에서 쉽게 접근할 수 있는 pc방 프로그램이 우리의 주제여서 그랬던 것 같다. 주제를 잘 잡는 것이 얼마나 중요한가라는 걸 더 잘 알 수 있었다.

package pj;
import java.io.*;
import java.util.*;

import view.*;

import java.net.*;
import client.*;
import pj.*;

// 오즈 서버 실행
public class OzServer{
	private ServerSocket ss;
	private Socket soc;
	private ObjectOutputStream oos;
	private ObjectInputStream ois;
	private Scanner sc;
	private ClientMessage cm;
	
	// 객체를 구분하기 위해 hashtable 자료 구조 사용
	// hashtable 자료 구조 선택 이유 :
	// hashtable은 동기화된 메서드로 구성되어 있기 때문에 멀티 스레드가 동시에 hashtable의 메소드들을
	// 실행할 수 없고, 따라서 멀티 스레드 상황에서도 안전하게 객체를 추가하고 삭제할 수 있다
	
	private Hashtable<String, ClientMessage> clientMHT  = new Hashtable<>();	// 클라이언트를 구분하는 hashtable
	private Hashtable<String, Integer> seatNum = new Hashtable<>();	// 자리를 구분하는 hashtable
	private Hashtable<String, TimeSetThread> timeSetHt = new Hashtable<>();	
	private Hashtable<String, ChatServerGUI>chatHt = new Hashtable<>();	// 채팅 프로그램 켜져 있는지 꺼져 있는지 확인
	private Hashtable<String, ServerChatThread>serChatHt = new Hashtable<>();
	
	UserTimePro userTime = new UserTimeProImpl();
	OzServerGui oz = new OzServerGui("관리자 화면");
	
	// 오즈 서버 구동
	public OzServer() {
		try {
			ss = new ServerSocket(24511);
			System.out.println("서버 대기 중.....");
			
			//sc = new Scanner(System.in);
			
			//serverGUI = new view.OzServerGui("관리자 서버");
			
			while (true) {
				soc = ss.accept();	
				System.out.println(soc.toString());// 소켓이 서버소켓을 받을 때마다
				OzClient ozc = new OzClient(soc);	// 오즈 클라이언트 객체 스레드 실행
				ozc.start();						// 클라이언트 스레드 스타트
			}
			
		}catch(Exception e) {
			e.printStackTrace();
		}
	}

	// 로그인 하면 => DB에 있는 시간 확인 후 클라이언트에게 시간 알려주기
    public void login(Socket soc, String id) {
    	cm = clientMHT.get(id);	
        System.out.println("server sendTime id : " + id);
        oz.addMessageToAlarm("@" + id + "님이 로그인 하였습니다.\n");
        
        // 자리 랜덤 배정 : 이미 선택된 자리는 배정 X
        boolean duplication;
        while (true) {
            Enumeration<String> keys = seatNum.keys();
            int seat = (int) ((Math.random() * 30) );
            duplication = false; // 루프 시작 시 duplication을 초기화

            while (keys.hasMoreElements()) {
                String key = keys.nextElement(); // 현재 반복 중인 키 가져오기
                if (seatNum.get(key).equals(seat)) {
                    System.out.println("자리 " + seat + "에 이미 다른 사용자가 배정되어 있습니다.");
                    duplication = true;
                    break;
                }
            }
            if (duplication) continue;
            seatNum.put(id, seat);
            System.out.println("id :" + seatNum.get(id));
            break;
        }
		System.out.println("자리 " + (seatNum.get(id)+1) + "번 에 " + id + " 배정 완료.");
    }
    
	
    // 로그아웃 하면 => 아이디에 남은 시간 값 DB에 저장하기
    public void logout(Socket soc, String id) throws Exception{
    	try {
    		// 서버가 로그아웃 한다고 받은 id                  
            System.out.println("server get logout id : " + cm.getSendId());
            // 서버에 메시지 보내기
            oz.addMessageToAlarm("@" + cm.getSendId() + "님이 로그아웃 하였습니다.\n");
            int num = seatNum.get(id);
            System.out.println("자리 값 : " + num);
            oz.ititial_seat(num+1);
            seatNum.remove(id);
            
        } catch (Exception e) {
            e.printStackTrace();
        }	
    }
  
	// 클라이언트 객체 스레드
	class OzClient extends Thread {
		private boolean stop = false;
		Socket soc;
		ClientMessage cm;
		private ObjectOutputStream oos;
		private ObjectInputStream ois;

		OzClient (Socket soc) {	// 클라이언트가 보낸 신호 읽기
			this.soc = soc;
		}

		public void run() {
			while (true) {
				try{
					try {
		        	oos = new ObjectOutputStream(new BufferedOutputStream((soc.getOutputStream())));
		        	ois = new ObjectInputStream(new BufferedInputStream((soc.getInputStream())));
		            cm = (ClientMessage) ois.readObject();
		            cm.disp();
					}catch(Exception e) {}
					
					String type = cm.getSendType();
					String id = cm.getSendId();
					System.out.println(id);
					System.out.println(type);
					if ("login".equals(type)) {
						clientMHT.put(id, cm);	// id 값이랑 clientMessage 객체 저장
						int time = userTime.UserLogin(id);		// DB에서 id 값에 저장된 time 불러오기
						//System.out.println(time);
						login(soc, id);		// 로그인 메서드 시작
						int addtime = 0;
						TimeSetThread tst = new TimeSetThread(soc, id, time, addtime); // time thread 시작
						timeSetHt.put(cm.getSendId(), tst);	// timethread 객체 저장
						tst.start();	// 스레드 시작
						//oos.close();
					}else if("logout".equals(type)){
						try {
							logout(soc, id);	// 여기를 못 넘어가고 뺑뺑도는 거임
							System.out.println("로그아웃 된 거임");
							//로그아웃 하는 timeSetThread 가져오기
							System.out.println(timeSetHt.get(cm.getSendId()));
							TimeSetThread tst_logout = timeSetHt.get(cm.getSendId());
							tst_logout.interrupt();	// interrupt 발생시켜 스레드 중지
							ClientMessage oldcm = clientMHT.get(cm.getSendId());

							try {
								chatHt.remove(id);	// 채팅 로그인 기록 삭제
								ServerChatThread sct = serChatHt.get(id);	// 채팅 스레드 중지
								sct.interrupt();
								serChatHt.remove(cm.getSendId());		// 채팅 스레드 중지
								
							}catch(Exception e) {
								System.out.println("채팅을 실행하지 않은 손님");
							}
							
							// 클라이언트 객체 가져오기
							userTime.UserLogout(id, timeSetHt.get(cm.getSendId()).getTime());
							System.out.println("DB에 남은 시간 저장 : " + timeSetHt.get(cm.getSendId()).getTime());
							clientMHT.remove(cm.getSendId(), oldcm);	// 객체 삭제
							//timeSetHt.remove(cm.getSendId(), tst_logout);	// 스레드 삭제
							
							//oos.close();
							this.interrupt();
							break;
						}catch(EOFException e){
							e.printStackTrace();
						}
					}else if("Time".equals(type)) {
						TimeSetThread tst_logout = timeSetHt.get(cm.getSendId());
						// 기존 id 에 저장되어 있는 time set thread 가져옴
						int time = tst_logout.getTime();	// 기존 객체에 있는 시간 가져옴
						tst_logout.interrupt();	// interrupt 발생시켜 기존 스레드 중지
						int addtime = cm.getSendAddTime(); // 충전된 시간
						TimeSetThread tst = new TimeSetThread(soc, id, time, addtime);
						// 새로운 time thread 시작
						timeSetHt.put(cm.getSendId(), tst);
						tst.start();
						oz.addMessageToAlarm(seatNum.get(id) +" 번 자리 "+ cm.getSendId() + "님이 " + (addtime/60) + " 분 추가하셨습니다\n");
						//oos.close();
					}else if("Food".equals(type)) {
						oz.addMessageToAlarm("@" + cm.getSendId() + "님이 " + seatNum.get(id) + "번 자리에서" + cm.getSendMenuList() + " 주문하셨습니다\n");
						
					//oos.close();
					}else if("chat".equals(type)) {
						ServerChatThread sct = new ServerChatThread(soc, id, cm);
						serChatHt.put(id, sct);
						sct.start();
						// 만약 클라이언트에서 채팅을 나간다면?
						// 채팅 스레드가 닫혀야 하므로 클라이언트에서 chatStop 신호도 받기
					}
	
				}catch(Exception e){
					e.printStackTrace();
				}
			}
		
		}	
	}
	
	// 시간 측정 스레드
	public class TimeSetThread extends Thread {
		Socket soc;
		String id;
		int time;
		int addtime;
		int sum;
		
		public TimeSetThread(Socket soc, String id, int time, int addtime) {
			this.soc = soc;
			this.id = id;
			this.time = time;
			this.addtime = addtime;
			sum = time+addtime;
		}
		
		public int getTime() {
			return sum;
		}
		
		public void run() {
            while (!interrupted()) {

            	// System.out.println("보낸 시간" + sum);
                oz.set_start_seat((seatNum.get(cm.getSendId())+1), sum);	// 시간을 분으로 전송
                sendTime(id, sum);
                
                
                try {
                    Thread.sleep(60000);	// 60초동안 대기
                }catch (Exception e) {
                    //e.printStackTrace();
                    break;
                }
                
                sum -= 60; // 시간을 60초씩 감소
                
            }
            System.out.println("리소스 정리");
            System.out.println("타임 스레드 종료");
		}
	}

	// 클라이언트에게 시간 보내는 스레드
	public void sendTime(String id, int time) {
    	try {
    		oos = new ObjectOutputStream(new BufferedOutputStream((soc.getOutputStream())));
			ClientMessage cm = new ClientMessage();
			cm.setSendAddTime(time);
			cm.setSendType("Time");
			cm.disp();
			
			try {
				oos.writeObject(cm);
				oos.flush();
			}catch(IOException e) {
				e.printStackTrace();
			}
			System.out.println("클라이언트로 잔여시간 전달 성공!" + time);

		}catch(Exception e) {
			e.printStackTrace();
		}
    }
	
	// 메시지 받는 스레드 : 메시지 전송은 ChatServerGUI 에서 함
	public class ServerChatThread extends Thread implements Serializable {
		Socket soc;
		String id;
		boolean duplication;
		ClientMessage cm;
		
		public ServerChatThread(Socket soc, String id, ClientMessage cm) {
			this.soc = soc;
			this.id = id;
			this.cm = cm;

			// 만약 채팅이 켜져 있지 않다면? 
	        while (true) {
	        	
	            Enumeration<String> keys = chatHt.keys();
	            duplication = false; // 루프 시작 시 duplication을 초기화

	            while (keys.hasMoreElements()) {
	            	try {
	            		if (!chatHt.get(id).isVisible()) break;
	            	}catch(Exception e) {}
	                String key = keys.nextElement(); // 현재 반복 중인 키 가져오기
	                if (key.equals(id)) {
	                    System.out.println(id + " 님과의 채팅이 켜져 있습니다.");
	                    duplication = true;
	                    break;
	                }
	            }
	            if (duplication) break;
	            oz.addMessageToAlarm("@" + cm.getSendId() + "님이 채팅을 시작하셨습니다.\n");
	            ChatServerGUI csg = new ChatServerGUI(id + "님 과의 채팅입니다", soc, id);
	            chatHt.put(id, csg);
	            System.out.println(id + " 님과의 채팅이 켜졌습니다.");
	            break;
	        }
			
		}
		
		public void run() {
			try{
				String idcm = cm.getSendId();
				String message = cm.getSendChat();
				
				
				chatHt.get(id).clientMessage(id, message);
				// 클라이언트 메시지를 chatServerGUI에게 보내주기-
				System.out.println(cm.getSendId());
				System.out.println(cm.getSendChat());
				// csg에 id값과 message 값을 줘서 클라이언트 메시지 받기
				
			}catch(Exception e) {
				e.printStackTrace();
			}
		}
		
	}
	

    
	
	public static void main(String[] args) {
		new OzServer();
	}
}

애증의 oz 서버 코드...
진짜 열심히 했다 약 10일 동안 밥 먹고 화장실 가고 집에 있는 시간을 제외하면 코딩만 하고 코딩만 생각했음...

지금 생각하면 너무 정립되지 않은 상태로 무작정 몸통 박치기만 했던 10일이지만, 다음 프로젝트를 위해 많이 도움될 것이라는 생각이 든다. 지금 하는 jsp 가 이해 쏙쏙 되는 것만 봐도 진심으로 도움 많이 됨.

그리고 다른 분들의 발표를 들으며 프로젝트 할 때 주의할 점을 정리해 보았는데...

구성하기

  • 내 것만 생각하지 않고 사이트의 기능 고민해서 세세하게 추가하기
  • 아웃 라인 구성 : 필요한 것들의 목록 나열 → 세세한 것 하나씩 구현

관리자 기능

  • 데이터 베이스 관리 (매출, 전년도 비교, 각 회원별 니즈 등등)
  • 메뉴 추가하기 등을 관리자가 할 수 있도록

클라이언트 기능

  • 멤버십 : 멤버별 등급 매겨서 혜택을 다르게 준다
  • 닉네임/ID 변경할 수 있는 기능

회원 관리

  • 로그인 할 때 중복 아이디 주의
  • pattern 클래스 : 그 패턴 말고 다른 값이 들어가면 안 됨
  • 자신만 자신의 회원 정보에 접근할 수 있도록 (클라이언트에서 값 주지 않기)
  • 중복 로그인은 불가능 : 만약 로그인이 되어 있으면 기존 로그인 아이디 로그아웃시키고 아이디로 로그인 할 수 있게

네트워크 관리

  • ⭐“서버를 여러 개 열어두고 / 포트 번호로 클라이언트의 접근을 나눌 수 있다”
  • tcp :

개선할 점

  • 시간 충전이 되어 있지 않으면 gui 안 되게 : 시간 충전을 먼저 해 주세요
  • 시간이 5-10분 남았을 때 dialog 띄우기
  • 객체 지향 언어
profile
자바 백엔드 개발자 개인 위키

0개의 댓글