## 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
ㄴ 해당 코드는 더 이상 필요 없으므로 제거