개발자입니다
[비트캠프] 62일차(13주차4일) - Java(Client, Server 아키텍처), myapp-32-1~2 본문
[비트캠프] 62일차(13주차4일) - Java(Client, Server 아키텍처), myapp-32-1~2
끈기JK 2023. 2. 2. 17:33
문자 처리할 때 아래 사용하니 용어 참고한다.
regex
lex yacc
com.eomcs.basic.ex11
Java 17 API 의 Regex > Pattern 에 나와있다.
package com.eomcs.basic.ex11;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Exam0100 {
public static void main(String[] args) {
// 1) 패턴 정의
// Pattern pattern = Pattern.compile("...", Pattern.CASE_INSENSITIVE);
// Pattern pattern = Pattern.compile("\\*", Pattern.CASE_INSENSITIVE);
// Pattern pattern = Pattern.compile("[0123456789]", Pattern.CASE_INSENSITIVE);
// Pattern pattern = Pattern.compile("[0-9]+", Pattern.CASE_INSENSITIVE);
Pattern pattern = Pattern.compile("[0-9]+|[\\+\\-\\*/]", Pattern.CASE_INSENSITIVE);
// Pattern pattern = Pattern.compile("\\d+|\\D", Pattern.CASE_INSENSITIVE);
// 2) 패턴에 따라 콘텐트를 검사할 도구 준비
Matcher matcher = pattern.matcher("123 + 2* 98-24/19");
// 3) 패턴의 결과를 담을 컬렉션 준비
ArrayList<String> items = new ArrayList<>();
// 4) 패턴 검사
while (matcher.find()) {
// 5) 패턴과 일치하는 항목을 추출하여 컬렉션에 담기
items.add(matcher.group());
}
System.out.println("-----------------------------");
for (String item : items) {
System.out.println(item);
}
}
}
Client / Server 프로젝트 분리
myapp 을 app-client 와 app-server 로 분리한다. 공통 코드(vo 객체)는 app-common 으로 추출하고 포함하도록 한다.
app-common 의 build.gradle (build script file) 에 다음 코드 추가한다.
plugins {
id 'java-library'
}
app-client, app-server 의 build.gradle 파일에 plugins 를 다음과 같이 설정하고 dependencies 에 아래 추가한다.
plugins {
id 'application'
}
dependencies {
implementation project(':app-common')
}
myapp > settings.gradle 을 이렇게 수정한다.
rootProject.name = 'myapp'
// include('app')
include('app-client')
include('app-server')
이후 myapp 에서 $ gradle eclipse 하면 위 두군데를 빌드한다.
데이터처리 아키텍처
Client 가 BoardDao 를 ① call 한다. BoardDao 에서 Server 측의 ServerApp 으로 ② 요청 한다. ③ 응답을 하면 BoardDao 가 ④ return 한다.
BoardDao ← 기존의 BoardDao 를 네트워킹을 통해 데이터를 처리하는 방식으로 변경해야 한다.
강결합의 문제점
BoardHandler 가 BoardDao 를 직접 지정해 사용하는 강결합 관계이다.
BoardDao 에서 네트워크가 필요해 기능 변경한다. BoardDao 가 사용하는 List 와 파일 I/O 를 ServerApp 으로 별도의 Application 으로 분리한다. 그러면 BoardDao 에서 데이터 처리 작업 요청하고 처리 결과를 받는 통신을 할 수 있다.
이때 BoardDao 변경에 맞춰 Handler도 변경해야 한다. 왜? Handler 에서 DAO를 생성했기 때문이다. → 강결합 되어 있다.
강결합의 문제점 해결책?
low coupling 구조로 변경
① 의존 객체를 직접 생성하는 대신 주입 받기
Dependency Injection (DI)
② 인터페이스를 통해 관계를 맺는다.
직접 클래스를 언급하지 않기
BoardHandler 에서 생성자 아규먼트로 BoardDao 를 주입 받는다. 그러면 객체 생성방식이 변경되더라도 핸들러에게 영향을 끼치지 않는다.
BoardDao boardDao = new BoardDao(in, out); ← 외부에서 의존 객체 생성
BoardHandler boardHandler = new BoardHandler(boardDao); ← 의존 객체 주입
low coupling 구조로 변경
① 의존 객체를 직접 생성하는 대신 주입 받기
Dependency Injection (DI)
② 인터페이스를 통해 관계를 맺는다.
직접 클래스를 언급하지 않기
BoardDao 를 interface 로 변경하고 NetworkBoardDao 로 교체한다면?
문제! NetworkBoardDao를 받을 수 있도록 변경해야 한다.
BoardHandler 의 BoardDao 필드 및 생성자 아규먼트 변경해야 한다.
해결책은 인터페이스를 통해 관계를 맺는다.
NetworkBoardDao boardDao = new NetworkBoardDao(in, out);
BoardHandler boardHandler = new BoardHandler(BoardDao);
BoardHandler 의 BoardDao 필드 및 생성자 아규먼트가 인터페이스로 변경되어 의존 객체를 교체하기 쉽다.
concrete 클래스인 LocalBoardDao 또는 NetworkBoardDao 로 변경 가능하다.
Client / Server Protocol
요청: 데이터명, 액션, 파라미터(선택) 으로 한다. 예컨대 "board", "findAll" 각각 요청한다.
응답 : 상태코드(문자열), 데이터(JSON 텍스트) 로 한다.
Server Architecture
Server Application + let (작은 조각) = Servlet 이라 한다.
ServerApp 으로 요청이 들어올때, "board" 일 경우 BoardServlet 을 호출해 BoardDao 로 List 를 사용한다.
"student" 일 경우 StudentServlet 을 호출해 StudentDao 로 List 를 사용한다.
"teacher" 일 경우 TeacherServlet 을 호출해 TeacherDao 로 List 를 사용한다.
RPC → RMI → CORBA → EJB → Web Service → RESTful Service (REST API)
RPC (Remote Procedure Call), RMI (Remote Method Invocation)
예전 방식은 BoardHandler 가 BoardDao 를 call 하는 방식이었다.
BoardHandler 가 NetworkBoardDao 를 call 한다. BoardServlet 으로 요청하고 BoardServlet 은 BoardDao (Remote Object) 를 call 해 파일을 이용하거나 List 를 사용한다. 결과를 BoardServlet 가 응답한다.
BoardHandler 가 BoardDao 를 바로 call 하는 것처럼 사용한다.
*Remote : 다른 프로세스(실행중인 프로그램)
이처럼 중간에서 데이터를 주고 받는 객체를 Object Request Broker (ORB) 라 한다. 특히 Client ORB 는 "Stub" (클라이언트 측 중개인(ORB)) 라 하고 Server ORB 는 "Skeleton" (서버측 중개인) 이라 한다.
Common ORB Architecture 를 줄여서 "CORBA" 라 한다.
RMI : Client 와 Server 가 동일 언어여야 한다.
CORBA : Stub 을 Server 언어에 맞게 자동으로 만들어 준다.
EJB : 2000년 초반에 은행권에서 대세 기술이었다.
Web Service : 통신에 HTTP 기술을 사용한다.
RESTful Service : HTTP 로 통신한다.
app-common 추출
app-client 와 app-server 의 공통 코드를 추출하기 위해 복사해서 이름을 app-common 으로 설정하고 vo 패키지 외에 모두 삭제한다.
server 측 외에 csv, json 파일 삭제한다.
myapp-server, myapp-client 속성을 보면 myapp-common 이 포함되어 있어야 한다.
Classpath 에 myapp-common 포함되어 있어야 한다.
### 32-1. DAO 객체를 교체하기 쉽게 만들기
- 인터페이스와 의존 객체 주입
- Handler에서 DAO 객체를 교체하기 쉽게 만들기
### 32-2. 네트워킹을 이용한 파일 공유: client/server app. 아키텍처로 전환
- 네트워크를 통해 파일을 공유하고 데이터 입출력을 처리하는 방법
- 데이터를 파일에 저장하고 꺼내는 기능을 별도의 애플리케이션으로 분리한다.
- 기존의 프로그램은 네트워크를 통해 파일 서버에 접속하여 데이터 입출력을 처리한다.
myapp-client
public class ClientApp {
public static void main(String[] args) {
new ClientApp().execute("localhost", 8888);
}
void execute(String ip, int port) {
try (Socket socket = new Socket(ip, port);
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
DataInputStream in = new DataInputStream(socket.getInputStream())) {
NetworkBoardDao boardDao = new NetworkBoardDao(in, out);
NetworkStudentDao studentDao = new NetworkStudentDao(in, out);
NetworkTeacherDao teacherDao = new NetworkTeacherDao(in, out);
StudentHandler studentHandler = new StudentHandler("학생", studentDao);
TeacherHandler teacherHandler = new TeacherHandler("강사", teacherDao);
BoardHandler boardHandler = new BoardHandler("게시판", boardDao);
loop: while (true) {
System.out.println("1. 학생관리");
System.out.println("2. 강사관리");
System.out.println("3. 게시판");
System.out.println("9. 종료");
int menuNo;
try {
menuNo = Prompt.inputInt("메뉴> ");
} catch (Exception e) {
System.out.println("메뉴 번호가 옳지 않습니다!");
continue;
}
try {
switch (menuNo) {
case 1:
studentHandler.service();
break;
case 2:
teacherHandler.service();
break;
case 3:
boardHandler.service();
break;
case 9:
out.writeUTF("quit");
break loop; // loop 라벨이 붙은 while 문을 나간다.
default:
System.out.println("잘못된 메뉴 번호 입니다.");
}
} catch (Exception e) {
System.out.printf("명령 실행 중 오류 발생! - %s : %s\n",
e.getMessage(),
e.getClass().getSimpleName());
}
}
System.out.println("안녕히 가세요!");
Prompt.close();
} catch (Exception e) {
System.out.println("네트워킹 오류!");
e.printStackTrace();
}
}
}
BoardDao 를 인터페이스로 변경한다.
public interface BoardDao {
void insert(Board board);
Board[] findAll();
Board findByNo(int no);
void update(Board b);
boolean delete(Board b);
}
StudentDao, TeacherDao 도 인터페이스로 바꾼다.
public interface StudentDao {
void insert(Student s);
Student[] findAll();
Student findByNo(int no);
void update(Student s);
boolean delete(Student s);
}
public interface TeacherDao {
void insert(Teacher t);
Teacher[] findAll();
Teacher findByNo(int no);
void update(Teacher t);
boolean delete(Teacher t);
}
기존 BoardDao 였던 파일은 LocalBoardDao 로 이름을 바꾼다. StudentDao, TeacherDao 도 동일하게 바꾼다.
NetworkBoardDao 생성한다. NetworkTeacherDao, NetworkStudentDao 는 변수 이름만 다를 뿐 동일하게 동작한다.
public class NetworkBoardDao implements BoardDao {
List<Board> list;
int lastNo;
DataInputStream in;
DataOutputStream out;
public NetworkBoardDao(DataInputStream in, DataOutputStream out) {
this.in = in;
this.out = out;
}
@Override
public void insert(Board b) {
fetch("board", "insert", b);
}
@Override
public Board[] findAll() {
return new Gson().fromJson(fetch("board", "findAll"), Board[].class);
}
@Override
public Board findByNo(int no) {
return new Gson().fromJson(fetch("board", "findByNo", no), Board.class);
}
@Override
public void update(Board b) {
fetch("board", "update", b);
}
@Override
public boolean delete(Board b) {
fetch("board", "delete", b);
return true;
}
public void save(String filename) {
try (FileWriter out = new FileWriter(filename)) {
out.write(new Gson().toJson(list));
} catch (Exception e) {
e.printStackTrace();
}
}
public void load(String filename) {
if (list.size() > 0) { // 중복 로딩 방지!
return;
}
try (BufferedReader in = new BufferedReader(new FileReader(filename))) {
// 1) JSON 데이터를 어떤 타입의 객체로 변환할 것인지 그 타입 정보를 준비한다.
TypeToken<List<Board>> collectionType = new TypeToken<>() {};
// 2) 입력 스트림에서 JSON 데이터를 읽고, 지정한 타입의 객체로 변환하여 리턴한다.
list = new Gson().fromJson(in, collectionType);
if (list.size() > 0) {
lastNo = list.get(list.size() - 1).getNo();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public String fetch(String dataName, String action, Object... data) throws DaoException {
try {
out.writeUTF(dataName);
out.writeUTF(action);
if (data.length > 0) {
out.writeUTF(new Gson().toJson(data[0]));
}
// 응답
String status = in.readUTF();
if (status.equals("400")) {
throw new DaoException("클라이언트 요청 오류!");
} else if (status.equals("500")) {
throw new DaoException("서버 실행 오류!");
}
return in.readUTF();
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException("오류 발생!", e);
}
}
}
Handler 들의 생성자에 BoardDao 객체 주입받도록 한다.
public class BoardHandler {
private BoardDao boardDao;
private String title;
public BoardHandler(String title, BoardDao boardDao) {
this.title = title;
this.boardDao = boardDao;
}
public class TeacherHandler {
private TeacherDao teacherDao;
private String title;
public TeacherHandler(String title, TeacherDao teacherDao) {
this.title = title;
this.teacherDao = teacherDao;
}
public class StudentHandler {
private StudentDao memberDao;
private String title;
public StudentHandler(String title, StudentDao memberDao) {
this.title = title;
this.memberDao = memberDao;
}
myapp-server
public class ServerApp {
public static void main(String[] args) {
new ServerApp().service(8888);
System.out.println("서버 종료!");
}
void service(int port) {
System.out.println("서버 실행 중...");
try (ServerSocket serverSocket = new ServerSocket(port);
Socket socket = serverSocket.accept();
DataInputStream in = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
BoardDao boardDao = new BoardDao(new LinkedList<Board>());
boardDao.load("board.json");
StudentDao studentDao = new StudentDao(new ArrayList<Student>());
studentDao.load("student.json");
TeacherDao teacherDao = new TeacherDao(new ArrayList<Teacher>());
teacherDao.load("teacher.json");
StudentServlet studentServlet = new StudentServlet(studentDao);
TeacherServlet teacherServlet = new TeacherServlet(teacherDao);
BoardServlet boardServlet = new BoardServlet(boardDao);
while (true) {
String dataName = in.readUTF();
switch (dataName) {
case "board":
boardServlet.service(in, out);
boardDao.save("board.json");
break;
case "student":
studentServlet.service(in, out);
studentDao.save("student.json");
break;
case "teacher":
teacherServlet.service(in, out);
teacherDao.save("teacher.json");
break;
case "quit":
return;
}
}
} catch (Exception e) {
System.out.println("서버 오류 발생!");
e.printStackTrace();
}
}
}
server 측 Handler 는 Servlet 으로 이름 변경하고 다음과 같이 수정한다.
student, teacher 도 변수명만 다를 뿐 동일하게 동작한다.
public class BoardServlet {
private BoardDao boardDao;
public BoardServlet(BoardDao boardDao) {
this.boardDao = boardDao;
}
private void onInsert(DataInputStream in, DataOutputStream out) throws Exception {
// 클라이언트가 보낸 JSON 데이터를 읽어서 Board 객체로 만든다.
Board b = new Gson().fromJson(in.readUTF(), Board.class);
this.boardDao.insert(b);
out.writeUTF("200");
out.writeUTF("success");
}
private void onFindAll(DataInputStream in, DataOutputStream out) throws Exception {
out.writeUTF("200");
out.writeUTF(new Gson().toJson(this.boardDao.findAll()));
}
private void onFindByNo(DataInputStream in, DataOutputStream out) throws Exception {
int boardNo = new Gson().fromJson(in.readUTF(), int.class);
Board b = this.boardDao.findByNo(boardNo);
if (b == null) {
out.writeUTF("400");
return;
}
out.writeUTF("200");
out.writeUTF(new Gson().toJson(b));
}
private void onUpdate(DataInputStream in, DataOutputStream out) throws Exception {
Board board = new Gson().fromJson(in.readUTF(), Board.class);
Board old = this.boardDao.findByNo(board.getNo());
if (old == null) {
out.writeUTF("400");
return;
}
this.boardDao.update(board);
out.writeUTF("200");
out.writeUTF("success");
}
private void onDelete(DataInputStream in, DataOutputStream out) throws Exception {
Board board = new Gson().fromJson(in.readUTF(), Board.class);
Board b = this.boardDao.findByNo(board.getNo());
if (b == null) {
out.writeUTF("400");
return;
}
this.boardDao.delete(b);
out.writeUTF("200");
out.writeUTF("success");
}
public void service(DataInputStream in, DataOutputStream out) throws Exception {
try {
// 클라이언트가 요구하는 액션을 읽는다.
String action = in.readUTF();
switch (action) {
case "insert": this.onInsert(in, out); break;
case "findAll": this.onFindAll(in, out); break;
case "findByNo": this.onFindByNo(in, out); break;
case "update": this.onUpdate(in, out); break;
case "delete": this.onDelete(in, out); break;
default:
System.out.println("잘못된 메뉴 번호 입니다.");
}
} catch (Exception e) {
out.writeUTF("500");
}
}
}
조언
*초급자가 처음부터 끝까지 다 짤 수 없다. 따라 치면서 방법을 익히고 점차 할 수 있는 능력을 넓혀서 나중에는 스스로 할 수 있게 된다.
*네카라쿠배에서 네트워킹 관련 나온 질문 : stateful 방식과 stateless 방식 차이. 상담은 stateful, 114는 stateless
과제
StudentDao, TeacherDao 네트워킹 버전으로 변경
'네이버클라우드 AIaaS 개발자 양성과정 1기 > Java' 카테고리의 다른 글
[비트캠프] 63일차(13주차5일) - Java(네트워킹 연결방식, 스레드), myapp-33~35 (0) | 2023.02.03 |
---|---|
[Java] 예제 소스 정리 - 네트워킹(Client/Server, 연결 방식) (0) | 2023.02.02 |
[비트캠프] 61일차(13주차3일) - Java(네트워킹), myapp-29~32 (0) | 2023.02.01 |
[비트캠프] 60일차(13주차2일) - Java(데코레이터, I/O stream), myapp-27~28 (0) | 2023.01.31 |
[Java] 예제 소스 정리 - 파일 입출력 (0) | 2023.01.30 |