본문 바로가기
네이버클라우드/JAVA 웹 프로그래밍

JAVA 45일차 (2023-07-25) 자바 프로그래밍_50. Application Server 아키텍처로 전환하기_개인프로젝트 - 마트 관리 시스템

by prometedor 2023. 7. 25.
## 50. Application Server 아키텍터로 전환하기

- 애플리케이션 서버 아키텍처의 특징과 구현
- Executor를 이용하여 스레드를 풀링하기

 

애플리케이션 서버 아키텍처의 특징과 구현

ㄴ 폴더에 남아있던 app-server 삭제하기

=>

=>

ㄴ app-client 를 복사해서 붙여넣은 후 app-server 라는 이름으로 변경해주기

 

ㄴ build.gradle 스크립트 파일에서 application 의 mainClass 를 ClientApp -> ServerApp 으로 변경해주기

 

ㄴ eclipse 프로젝트 이름 설정하는 부분에서 project의 name 을 report-client -> report-server 로 변경해주기

 

ㄴ gradle 재설정

 

=>

=>

=>

ㄴ report-server 프로젝트 import 해주기

 

=>

=>

ㄴ Refactor 를 이용하여 ClientApp -> ServerApp 으로 Rename 하기

 

ServerApp.java

ㄴ 해당 코드 필요 없으므로 삭제

 

ServerApp.java

ㄴ args[0], Integer.parseInt(args[1]) 대신 8888 포트 이용

 

ServerApp.java

ㄴ 생성자에 port 번호만 넘겨주도록 해줌

 

ServerApp.java

ㄴ LoginListener 를 제거

 

ServerApp.java

ㄴ printTitle() 과 mainMenu.execute(prompt) 는 주석처리하고 서버 실행 관련 예외처리 추가

 

ServerApp.java

ㄴ ServerSocket 추가

ㄴ 생성자에 port 번호 넘겨주기

 

ServerApp.java

ㄴ 클라이언트 통신 관련 예외 처리 추가

 

ServerApp.java

ㄴ accept() 메서드는 ServerSocket 클래스의 메서드로, 호출되면 클라이언트가 지정된 포트로 서버에 연결할 때까지 기다림

ㄴ 한 번 클라이언트 연결이 확립되면, accept() 메서드는 서버와 클라이언트 사이의 통신 채널을 나타내는 Socket 클래스의 새 인스턴스를 반환

 

ServerApp.java

ㄴ DataInputStream 과 DataOutputStream 이용하기위해 추가

 

ServerApp.java

ㄴ 입출력을 위한 readUTF, writeUTF 이용

 

ServerApp.java

ㄴ 클라이언트 통신 부분을 while (true) 를 이용해 무한루프로 만들기

 

ClientApp.java

ㄴ 해당 코드들 필요 없으므로 제거

 

ClientApp.java

ㄴ 생성자에서 ip 와 port 를 받으므로 필드에도 ip, port 추가해주기

 

ClientApp.java

ㄴ close 해줄 필요 없으므로 제거

 

ClientApp.java

ㄴ 해당 코드도 필요 없으므로 제거

=>

ClientApp.java

ㄴ ClientApp 클래스는 이렇게 간단해짐

 

ㄴ 해당 패키지 및 클래스는 client 쪽에서 없어도 되므로 제거

 

ClientApp.java

ㄴ BreadcrumbPrompt 를 사용하는 곳이 없으므로 제거

 

ClientApp.java

ㄴ execute 메서드에 주어진 IP 주소와 포트 번호를 사용하여 서버에 소켓 연결을 시도하는 코드를 추가

ㄴ 예외가 발생할 경우 "서버 통신 오류!" 라는 문구를 출력하도록 함

 

ClientApp.java

ㄴ DataInputStream 과 DataOutputStream 을 이용하기 위해 추가

 

ClientApp.java

ㄴ 서버와 통신이 연결되면 DataOutputStream을 사용하여 서버로 UTF-8 형식으로 "Hello!"라는 문자열을 보내도록 함

 

ClientApp.java

ㄴ DataInputStream을 사용하여 UTF-8 형식으로 서버에서 보내온 문자열을 읽을 수 있도록 함

 

ServerApp.java

ㄴ 서버가 실행중이라면 "서버 실행 중..." 이라는 문구를 출력하도록 함

 

ServerApp.java

ㄴ InetSocketAddress: SocketAddress의 하위 클래스로, IP 주소와 포트 번호를 포함하는 소켓 주소를 나타냄

ㄴ socket.getRemoteSocketAddress(): socket 객체에서 원격 (Remote) 측 소켓 주소를 얻어옴

    ㄴ 이 메서드는 SocketAddress를 반환함

ㄴ getHostString() 메서드는 InetSocketAddress의 메서드로, 해당 주소의 호스트 이름을 문자열로 반환함

     ㄴ 이 메서드는 호스트 이름을 IP 주소 대신에 반환하며, IP 주소가 사용 불가능한 경우 빈 문자열을 반환할 수도 있음

 

ServerApp.java 실행

=>

ClientApp.java 실행

ㄴ 서버에는 ip 주소가 출력됨

ㄴ 클라이언트에는 "응답: Hello!" 가 출력됨

 

ClientApp.java 한 번 더 실행

ㄴ 2 개의 클라이언트가 접속 상태임을 확인할 수 있음

 

ClientApp.java

ㄴ while(true) 를 이용하여 무한루프를 생성

ㄴ 읽어온 값이 <!--end--> 와 같다면 무한루프를 나가도록 함

ㄴ 아니라면 읽어온 response 값을 출력하도록 함

 

ㄴ report-common 프로젝트에 있는 net 패키지에 NetProtocol 클래스를 생성

 

NetProtocol.java

ㄴ 이렇게 생성하여 사용할 경우 오타를 방지할 수 있다는 장점이 있음

=>

ClientApp.java

=>

ClientApp.java

ㄴ 생성한 NetProtocol 클래스에 있는 RESPONSE_END 를 이용함

 

ServerApp.java

ㄴ while (true) 무한루프를 추가하고 클라이언트가 서버에 접속할 경우 클라이언트에서 받은 값을 클라이언트에 출력되도록 함

ㄴ request 값이 "exit" 과 같다면 무한루프를 나가도록 함

 

ServerApp.java 실행 후 ClientApp.java 실행

ㄴ 서버에서 클라이언트로부터 받은 값을 클라이언트에 출력하도록 하여 클라이언트 측에 해당 값이 출력됨

ㄴ 서버측에서는 위와 같이 출력됨

 

ㄴ report-server 프로젝트의 util 패키지 안에 있는 Prompt.java 파일 복사해오기

 

Prompt.java

ㄴ  Java에서 클래스가 AutoCloseable 인터페이스를 구현한다는 것은 이 클래스의 인스턴스가 try-with-resources 문과 함께 사용될 수 있다는 의미임

 

Prompt.java

ㄴ @Override 애노테이션을 입력하였을 때  AutoCloseable 인터페이스는 close()라는 하나의 추상 메서드를 가지고 있으므로 빨간 줄이 나타나지 않으므로 잘 구현됨을 알 수 있음

 

ClientApp.java

ㄴ Prompt 를 이용하도록 함

    =>  try-with-resources 문과 함께 사용하기 위해 AutoCloseable 을 implement 한 것임

 

ClientApp.java

ㄴ "> " 다음에 값을 입력받기위한 코드

 

ClientApp.java

ㄴ "exit" 이라는 문구를 입력 받을 경우 while (true) 무한루프를 나가도록 함

 

ClientApp.java

ㄴ while (true) 무한루프를 추가하기

ㄴ 서버에서 받은 값이 NetProtocol.RESPONSE_END 에 해당하는 값이라면 무한루프를 나가고 아니라면 받은 값을 출력하도록 함

 

ServerApp.java 실행 후 ClientApp.java 실행

ㄴ 클라이언트에서 보낸 값을 서버에서 다시 보내서 클라이언트에 출력되도록 구현됨을 확인

 

ClientApp.java

ㄴ 서버에서 읽어온 값을 출력하도록 함

 

ServerApp.java

ㄴ 해당 코드 제거한 후 while 문 안에 클라이언트 접속되면 "[마트 관리 시스템]\n" 을 출력하도록 코드를 작성

 

ServerApp.java

 

ServerApp.java 실행 후 ClientApp.java 실행

ㄴ 클라이언트 측에서 서버 측으로 보낸 값을 클라이언트 측에서 다시 출력하도록 함

 

report-server 

Prompt.java

ㄴ 서버 측 Prompt 에는 Prompt 생성자에 DataInputStream 과 DataOutputStream 을 넘겨주도록 함

=>

생성자로 넘어온 값 받기 위해 필드 추가

=>

report-server

Prompt.java

ㄴ Scanner 대신 DataInputStream 과 DataOutputStream 이용하도록 함

 

report-server

Prompt.java

ㄴ 해당 코드 필요 없으므로 제거

 

report-server

Prompt.java

ㄴ printf -> writeUTF 이용하도록 함

 

NetProtocol.java

ㄴ report-server 의 Prompt 클래스에서 사용하기 위해 해당 코드 추가

 

report-server

Prompt.java

ㄴ NetProtocol 클래스에 정의한 PROMPT 를 이용

 

report-server

Prompt.java

ㄴ return 값에 Scanner 대신 DataInputStream 이용함

=>

report-server

Prompt.java

ㄴ 예외가 발생할 수 있으므로 예외처리를 해줌

 

report-server

Prompt.java

ㄴ 해당 코드 필요 없으므로 제거 (이제 Scanner 를 사용하지 않으므로)

 

report-server

Prompt.java

ㄴ StringBuffer 생성하기

 

report-server

Prompt.java

ㄴ print 메서드와 println 메서드 생성


report-server

Prompt.java

ㄴ end 메서드는 buf라는 변수에서 문자열을 가져와서 문자열 형태로 변환한 값과 NetProtocol.RESPONSE_END라는 상수를 UTF-8 형식으로 서버에 보내기 위한 위한 메서드임

 

 

serverApp.java

ㄴ 해당 코드 필요 없으므로 제거

 

serverApp.java

ㄴ Connection 을 바깥쪽에 선언해주기

 

serverApp.java

ㄴ Connection 을 닫아주기 위한 close 메서드 생성

 

serverApp.java

ㄴ 해당 코드 필요 없으므로 제거

 

serverApp.java

ㄴ 아래쪽에 주석처리 되어있던 해당 코드 위쪽으로 가져와서 while (true) 이용하여 무한루프 생성

 

BreadcrumbPrompt.java

ㄴ 예외가 발생할 수 있으므로 예외를 던져줘야 함

 

BreadcrumbPrompt.java

=>

=>

ㄴ Generate Constructors from Superclass... 선택하여 수퍼클래스의 기본 생성자를 생성하도록 함

 

MenuGroup.java

ㄴ 예외가 발생할 수 있으므로 예외를 던져줘야 함

=>

Menu.java

예외가 발생할 수 있으므로 예외를 던져줘야 함

ㄴ Menu 관련 execute 메서드를 모두 찾아 예외 처리 해주기

 

=>

이렇게 하는 것 보다 아래처럼 하는 것이 나음

=>

Menu.java

ㄴ throw Exception 제거

 

MemuGroup.java

 ㄴ 전체를 try ~ catch 로 묶고 catch 문에서 RuntimeException 을 이용하여 예외처리 해주기

 

MenuGroup.java

ㄴ printMenu 메서드에 BreadcrumbPrompt 객체 넣어주기

 

MenuGroup.java

ㄴ printMenu 메서드가 BreadcrumbPrompt 객체를 매개변수로 받도록 해줌

 

Prompt.java

ㄴ String.format(format, args): format 문자열에 가변 인자로 전달된 args를 대입하여 형식화된 문자열을 반환

     => %s는 문자열로 대체되고, %d는 정수로 대체됨

 

MenuGroup.java

ㄴ System.out.printf 대신 prompt 에 있는 printf 이용

 

MenuGroup.java

=>

MenuGroup.java

ㄴ prompt 에 있는 end 메서드는 네트워크 통신에서 데이터 전송의 종료를 의미하는 마지막 신호를 보내는 역할을 함

 

MenuGroup.java

ㄴ prompt.end() 에서 예외가 발생할 수 있으므로 예외처리 해주기

 

MenuGroup.java

ㄴ System.out 대신 BreadcrummbPrompt 의 prompt 이용

 

MenuGroup.java

=>

MenuGroup.java

ㄴ prompt 에 있는 end 메서드는 네트워크 통신에서 데이터 전송의 종료를 의미하는 마지막 신호를 보내는 역할을 함

 

MenuGroup.java

=>

MenuGroup.java

ㄴ prompt 에 있는 end 메서드는 네트워크 통신에서 데이터 전송의 종료를 의미하는 마지막 신호를 보내는 역할을 함

 

Menu.java

=>

Menu.java

ㄴ prompt 에 있는 end 메서드는 네트워크 통신에서 데이터 전송의 종료를 의미하는 마지막 신호를 보내는 역할을 함

=>

Menu.java

ㄴ 예외처리 시 try ~ catch ~ finally 문 이용

 

Menu.java

ㄴ prompt.end() 에서 예외가 발생할 수 있으므로 try ~ catch 로 예외처리를 해줌

ㄴ RuntimeException 을 이용해 예외를 던지도록 함

 

Menu.java

ㄴ try ~ catch 문을 for 문 바깥으로 빼주기

 

Menu.java

ㄴ Buffer 를 초기화하기 위한 clear 메서드 생성

 

Menu.java

ㄴ 작업을 수행하다가 예외가 발생할 경우 Buffer 를 초기화시켜주기 위해 prompt.clear() 이용

 

HeaderListener.java

ㄴ System.out 대신 prompt 를 이용하도록 함

 

HelloListener.java

=>

HelloListener.java

ㄴ 예외가 발생할 수 있으므로 try ~ catch 와 RuntimeException 을 이용해 예외처리를 해줌

 

FooterListener.java

ㄴ System.out 대신 prompt 이용

 

ServerApp.java

ㄴ 해당 코드 잘라내서 아래 위치에 두기

=>

ServerApp.java

 

ServerApp.java

ㄴ mainMenu.execute(prompt); 는 밖으로 빼고 while 문은 제거함

 

ClientApp.java

ㄴ 해당 코드 제거

 

ClientApp.java

ㄴ 해당 while 문은 필요 없으므로 제거

 

ClientApp.java

ㄴ else if (response.equals(NetProtocol.PROMPT)) { ... }: 읽어온 메시지가 NetProtocol.PROMPT 상수와 같으면 사용자로부터 입력을 받아 서버로 전송하도록 함

=> 이를 통해 서버에게 클라이언트의 입력을 전달

 

NetProtocol.java

ㄴ <!--stop--> 이라는 문자열을 NET_END 라는 이름으로 상수 추가

 

ServerApp.java

ㄴ NetProtocol.NET_END라는 상수를 클라이언트로 전송하도록 함

    => 이를 통해 클라이언트에게 통신이 종료되었음을 알림

 

ServerApp.java

ㄴ 읽어온 메시지가 NetProtocol.NET_END 상수와 같으면 루프를 종료하도록 함

    => 이는 서버가 클라이언트의 통신을 종료해야 하는 신호를 보낸 경우임

 

ClientApp.java

ㄴ continue 추가

 

ServerApp.java 실행 후 ClientApp.java 실행

ㄴ 입력 받을 때 한 줄 띄어짐

=>

ClientApp.java

ㄴ println -> print 로 변경

 

ServerApp.java 실행 후 ClientApp.java 실행

ㄴ 잘 출력됨을 확인

 

MemberListListener.java

ㄴ System.out 대신 prompt 의 println 이용하기

 

ServerApp.java 실행 후 ClientApp.java 실행

ㄴ 잘 출력됨을 확인

 

MemberAddListener.java

ㄴ 예외 발생할 수 있으므로 try ~ catch 문과 RuntimeException 을 이용하여 예외처리 해줌

 

MemberActionListener.java

ㄴ 예외 발생할 수 있으므로 try ~ catch 문과 RuntimeException 을 이용하여 예외처리 해줌

 

MemberActionListener.java

ㄴ prompt 의 println 을 이용하도록 함

 

ㄴ insert 메서드가 제대로 실행됨을 확인

 

MemberDetailList.java

ㄴ try ~ catch 와 RuntimeException 을 이용하여 예외처리

 

ActioinListener.java

ㄴ IOException : 입출력(IO) 작업 중에 발생할 수 있는 예외를 처리하기 위한 클래스

 

report-server

Prompt.java

ㄴ Exception -> IOExceptioin 으로 변경

=> IOException : 입출력(IO) 작업 중에 발생할 수 있는 예외를 처리하기 위한 클래스

 

ㄴ report-client 프로젝트의 util 패키지는 필요 없으므로 제거

=> client 측에서는  Prompt 사용하지 않기 때문

 

ClientApp.java

ㄴ Prompt 대신 Scanner 선언해주기

=>

ClientApp.java

ㄴ keyscan 이용하기

 

MemberActionListener.java

ㄴ IOExceptioin 이용하여 예외 처리 해주도록 함

 

MemberAddListener.java

ㄴ IOExceptioin 이용하여 예외 처리 해주도록 함

 

MemberAddListener.java

ㄴ IOExceptioin 이용하여 예외 처리 해주도록 함

 

report-server

Prompt.java

ㄴ IOExceptioin 이용하여 예외 처리 해주도록 함

 

MemberDetailListener.java

ㄴ Replace with: 부분에 prompt 을 입력한 후 [Replace All] 선택하여 System.out 을 prompt 로 모두 대체함

 

MemberDetailListener.java

ㄴ IOException 으로 예외를 던져주므로 try ~ catch 문 제거해도 됨

=>

ServerApp.java 실행 후 ClientApp.java 실행

ㄴ 잘 실행됨을 확인

 

MemberUpdateListener.java

ㄴ IOException 을 이용하여 예외를 던져줌

=> 이제 try ~ catch 문을 이용하여 예외처리를 하지 않아도 됨

 

Prompt.java

ㄴ title과 args를 사용하여 포맷 문자열을 만들고, 해당 포맷 문자열을 서버로 전송하도록 함

ㄴ title은 사용자에게 입력을 요청하는 메시지를 나타내는 포맷 문자열임

ㄴ %s, %d, %f 등의 포맷 지정자와 args에 해당하는 값들을 사용하여 메시지를 구성함

 

ServerApp.java 실행 후 ClientApp.java 실행

ㄴ update 가 제대로 실행됨을 확인

 

MemberDeleteListener.java

ㄴIOException 을 이용하여 예외를 던져주도록 함

 

MemberDeleteListener.java

ㄴ System.out 대신 prompt 의 Println 을 이용하도록 함

 

ServerApp.java 실행 후 ClientApp.java 실행

ㄴ 5번 직원을 삭제하려고 했지만 해당 직원이 작성한 게시글이 있어서 삭제 불가

ㄴ 2번 직원은 삭제 가능

 

MemberDeleteListener.java

ㄴ 삭제가 완료되면 "삭제했습니다!" 라는 문구를 출력하도록 함

=>

ServerApp.java 실행 후 ClientApp.java 실행

ㄴ 삭제 후 직원 등록까지 제대로 실행됨을 확인

 

LoginListener.java

ㄴ IOException 을 이용해 예외를 던져주도록 함

 

LoginListener.java

ㄴ System.out 대신 prompt 이용

 

ServerApp.java

ㄴ LoginListener 클래스의 생성자를 호출하고, memberDao를 인자로 전달하여 LoginListener의 인스턴스를 생성
ㄴ 생성된 LoginListener 인스턴스의 service 메소드를 호출함

ㄴ prompt 객체는 클라이언트로부터 입력을 받고 출력하는 역할을 수행함

 

ServerApp.java 실행 후 ClientApp.java 실행

=>

LoginListener.java

ㄴ prompt.end 메소드는 클라이언트와의 상호작용을 끝내는 역할을 함

ㄴ 이를 통해 로그인 기능의 루프가 종료되고, 프로그램이 다음 단계로 진행될 수 있도록 함

 

ServerApp.java 실행 후 ClientApp.java 실행

ㄴ 로그인이 잘 실행됨을 확인

=>

BoardXxxListener 들도 모두 같은 방식으로 예외처리 및 변경작업 해주기

=>

ServerApp.java 실행 후 ClientApp.java 실행

ㄴ 잘 실행됨을 확인할 수 있음

 

Executor를 이용하여 스레드를 풀링하기

app-45-server

ServerApp.java

ㄴ app-45-server 에 있는 자바 스레드풀 준비하는 코드 복사해오기

 

ServerApp.java

ㄴ 자바 스레드풀 준비하는 코드 작성

 

app-45-server

ServerApp.java

ㄴ 해당 코드 참고

 

ServerApp.java

ㄴ Socket 을 try 문 바깥으로 빼줌

 

ServerApp.java

ㄴ threadPool에서 새로운 스레드를 실행하고, 해당 스레드가 run() 메소드를 실행하도록 함

 

ServerApp.java

ㄴ 해당 코드 잘라내기

 

ServerApp.java

ㄴ run 메서드 안에 붙여넣기

 

ServerApp.java

ㄴ Socket s = socket;는 새로운 참조 변수 s를 생성하고, socket에 할당된 Socket 객체를 s에 할당하는 역할을 함

    => try 블록 내에서 Socket 객체에 접근할 수 있도록 하고, 더 이상 필요하지 않을 때 정상적으로 닫을 수 있음

 

ServerApp.java

ㄴ try ~ catch 문 잘라내서 processRequest 라는 메서드로 만들기

 

ServerApp.java

ㄴ 람다식으로 변경

=>

ServerApp.java

ㄴ 더 간결하게 표현

 

Prompt.java

ㄴ HashMap 생성

 

Prompt.java

ㄴ setAttribute, getAttribute 메서드 추가

 

LoginListener.java

ㄴ ServerApp.loginUser = loginUser; => prompt.setAttribute("loginUser", loginUser); 로 변경

ㄴ 변경 전의 코드인 ServerApp.loginUser = loginUser;은 ServerApp 클래스의 정적(static) 변수 loginUser에 로그인한 사용자 정보(loginUser)를 저장하는 것이므로 로그인한 사용자 정보가 ServerApp 클래스 전체에서 공유되며, 어디서든 접근하여 사용할 수 있게 됨
ㄴ 변경 후의 코드인 prompt.setAttribute("loginUser", loginUser);은 Prompt 클래스의 인스턴스인 prompt의 context 맵에 로그인한 사용자 정보(loginUser)를 저장하는 것이므로 로그인한 사용자 정보가 해당 Prompt 인스턴스 내에서만 관리되고, 다른 Prompt 인스턴스와는 독립적으로 처리됨
=> 이렇게 변경하는 이유는 여러 클라이언트가 동시에 서버에 접속하여 각자의 로그인을 수행할 수 있기 때문

=> 서버 애플리케이션이 다중 클라이언트를 동시에 처리할 때, 각 클라이언트마다 별도의 Prompt 인스턴스를 생성하여 클라이언트와의 상호작용을 담당하므로, 각 Prompt 인스턴스 내에서 로그인한 사용자 정보를 저장해야 서로 다른 클라이언트의 정보가 서로 영향을 미치지 않음

 

BoardAddListener.java

ㄴ context 맵은 Map<String, Object> 타입으로 선언되어 있으며, 키(Key)는 문자열(String)이고 값(Value)은 Object 타입으로 저장되므로 해당 코드에서 Member 타입으로 형변환해줘야 함

 

BoardUpdateListener.java

ㄴ update 도 add 와 마찬가지로 해당 코드로 변경해줌

 

BoardDeleteListener.java

ㄴ delete 도 update, add 와 마찬가지로 해당 코드로 변경해줌

 

ServerApp.java

ㄴ 해당 코드는 더 이상 필요 없으므로 제거