5 - 6주차가 마무리 되면서 자바의 전반적인 언어적 특성을 다시 한번 복습할 수 있었고 또한 오랜만에 프레임워크에 의존하지 않고 쿼리를 작성해 볼 수 있었다. (엄청난 반복...)
입출력 및 네트워크 부분은 자바를 계속 다뤄 왔음에도 대략적으로 이런것이 있구나 하고 넘어가곤 했었는데(자바의 언어적 특성을 이해하기도 전에 스프링 프레임워크에만 몰빵한 학습의 폐해) 오프라인으로 기계적인 반복학습으로 입출력이나 네트워크를 이해하는데 큰 도움을 얻었다...
이번 6주차 포스팅에는 지금까지 배워왔던 모든 지식을 담아 과제를 리펙터링 해보았다.
git repository : https://github.com/zezeg2/java-practice/tree/main/src/ch16/assign
├── GlobalScanner.java // 전역에서 사용되는 커스텀 Scanner
├── Product.java // 상품 클래스
├── client // 클라이언트측 프로그램
│ ├── ClientRequest.java
│ └── ProductClient.java
└── server // 서버측 프로그램
├── ProductServerMain.java
├── ProductServerThread.java
└── ProductService.java
GlobalScanner.java
메모리상에 하나의 인스턴스만 존재한다(싱글톤)
try-resource-with
를 통해 Scanner
인스턴스를 회수하도록 하기 위해 Closeable
를 구현한다
사용자가 올바르게 값을 입력 하도록 하는 여러 메서드를 정의한다
comment
를 통해 입력에 대한 힌트를 주고 사용자가 int 타입의 입력을 받을 수 있도록 문자열 입력시 재입력 하도록 하고 최종 입력값을 리턴한다comment
를 통해 입력에 대한 힌트를 주고 사용자 입력을 받고 입력값을 리턴한다comment
를 통해 입력에 대한 힌트를 주고 사용자가 문자열 입력시 check
와 비교하여 일치한다면 replace
에 해당하는 값을 리턴하도록 한다. 그렇지 않다면 재입력 하도록 하고 최종 입력값을 리턴한다comment
를 통해 입력에 대한 힌트를 주고 입력값을 check
와 비교하여 일치한다면 replace에 해당하는 값을 리턴하도록 한다. 그렇지 않으면 사용자의 입력값을 리턴한다.public class GlobalScanner implements Closeable {
private static GlobalScanner instance = null;
private final Scanner scanner;
private GlobalScanner() {
this.scanner = new Scanner(System.in);
}
public static GlobalScanner getInstance() {
if (instance == null) instance = new GlobalScanner();
return instance;
}
public Scanner getScanner() {
return scanner;
}
public int nextNum(String comment) {
System.out.print(comment);
while (!scanner.hasNextInt()) {
scanner.next();
System.err.print("올바른 값을 입력해주세요. 재 선택 > ");
}
return scanner.nextInt();
}
public String nextString(String comment) {
System.out.print(comment);
return scanner.next();
}
public int nextNumOrCheckReplace(String comment, String check, int replace) {
System.out.print(comment);
while (!scanner.hasNextInt()) {
if (scanner.next().equalsIgnoreCase(check)) return replace;
System.err.printf("올바른 값을 입력해주세요 (종료 : '%s') 재 선택 > ", check);
}
return scanner.nextInt();
}
public String nextStringOrReplace(String comment, String check, String replace){
String input = scanner.next();
return input.equals(check) ? replace : input;
}
@Override
public void close() {
scanner.close();
}
}
Product.class
GlobalScanner
를 통해 사용자 입력을 받아 입력에 대한 정보를 가지는 인스턴스를 생성한다.toString()
을 재정의 하여 콘솔에서 출력가능하도록 한다public class Product implements Serializable {
public String name;
public int price, stock;
public Product(String name, int price, int stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", price=" + price +
", stock=" + stock +
'}';
}
public Product() {
GlobalScanner sc = GlobalScanner.getInstance();
name = sc.nextString("name : ");
price = sc.nextNum("price : ");
stock = sc.nextNum("stock : ");
}
}
ProductClient.java
GlobalScanner
, ClientRequest
를 지역변수로 선언DataOutputStream
) 생성try-resource-with
구문을 통해 자원반납을 의무화 한다.GlobalScanner
를 통해 사용자로부터 입력을 받고 입력값에 따라 프로그램 종료 및 ClientRequest
의 post()
(상품 등록) , get()
(상품 조회) 로직을 수행하도록 한다. 이 때 서버와 연결 된 상태의 socket을 인자로 넘겨 ClientReauset 인스턴스가 서버와 통신하며enroll()
, 혹은 search()
메서드를 실행하게 된다public class ProductClient {
public static void main(String[] args) throws IOException {
ClientRequest request = ClientRequest.getInstance();
GlobalScanner sc = GlobalScanner.getInstance();
loop:
while (true) {
try (Socket socket = new Socket("localhost", 19999);
DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
while (true) {
int key = sc.nextNumOrCheckReplace("\nEnroll Product : '1', Search Product : '2', Quit : 'q'\n=> ", "q", 0);
if (key == 0) break loop;
if (key == 1) {
out.writeInt(key);
request.post(socket);
break;
}
if (key == 2) {
out.writeInt(key);
request.get(socket);
break;
}
System.out.println("Enter correct command...\n");
}
}
}
}
}
ClientRequest.java
out.writeUTF()
)public class ClientRequest {
private static ClientRequest instance;
private static final GlobalScanner sc = GlobalScanner.getInstance();
public static ClientRequest getInstance() {
if (instance == null) {
instance = new ClientRequest();
}
return instance;
}
public ClientRequest() { }
public void post(Socket socket) throws IOException {
try (DataInputStream in = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
List<Product> products = new ArrayList<>();
while (true) {
products.add(new Product());
if (!sc.nextStringEqualsWith("enter the 'y' to add another product", "y")) break;
}
out.writeUTF(products.stream()
.map(product -> String.format("%20s%8s%8s", product.name, product.price, product.stock))
.reduce("", (s1, s2) -> s1 + s2 + "\n"));
System.out.println(in.readUTF());
}
socket.close();
}
public void get(Socket socket) throws IOException {
try (DataInputStream in = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
out.writeUTF(sc.nextString("Enter the Product name : "));
System.out.println("Result : " + in.readUTF());
}
socket.close();
}
}
ProductServerMain.class
ProductServerThread
) 실행public class ProductServerMain {
public static void main(String[] args) throws IOException {
ExecutorService pool = Executors.newFixedThreadPool(4);
try (ServerSocket server = new ServerSocket(19999)) {
while (true) {
pool.execute(new ProductServerThread(server.accept()));
}
}
}
}
ProductServiceThread.class
ProductService
를 멤버로 가진다.ProductClient
프로그램에서 입력된 사용자 입력 (key
)이 전달되며 이 값에 따라 ProductService
의 enroll
혹은 search
메서드가 실행된다public class ProductServerThread extends Thread {
private final Socket socket;
ProductService service = ProductService.getInstance();
public ProductServerThread(Socket socket) throws IOException {
this.socket = socket;
}
@Override
public void run() {
try(DataInputStream in = new DataInputStream(socket.getInputStream())) {
System.out.printf("%s : Connected with %s:%s\n",Thread.currentThread().getName(), socket.getInetAddress(), socket.getPort());
int key = in.readInt();
if (key == 1) service.enroll(socket);
if (key == 2) service.search(socket);
} catch (IOException e) {
} finally {
try {
System.out.printf("%s : Connection to %s:%s is terminated%n", Thread.currentThread().getName(), socket.getInetAddress(), socket.getPort());
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
ProductService.class
try-with-resources
에 추가한다.public class ProductService {
private static ProductService instance;
public static ProductService getInstance() throws IOException {
if (instance == null) {
instance = new ProductService();
}
return instance;
}
private final List<Product> productList;
private ProductService() throws IOException {
productList = new CopyOnWriteArrayList<>();
BufferedReader bf = new BufferedReader(new FileReader("product.txt"));
while (true) {
String line = bf.readLine();
if (line == null) break;
productList.add(productFromLine(line));
}
}
public void enroll(Socket socket) throws IOException {
try (DataInputStream in = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
FileWriter writer = new FileWriter("product.txt", true)) {
String productByLine = in.readUTF();
for (String line : productByLine.split("\n")) {
writer.write(line + "\n");
Product product = productFromLine(line);
productList.add(product);
System.out.printf("%s : %s enrolled by %s:%s\n",Thread.currentThread().getName(), product, socket.getInetAddress(), socket.getPort());
}
out.writeUTF("Your Product is Successfully Enrolled");
}
}
public void search(Socket socket) throws IOException {
try (DataInputStream in = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
String findKeyword = in.readUTF();
StringBuilder result = new StringBuilder();
for (Product p : productList.stream().filter(product -> product.name.equals(findKeyword)).toList()) {
result.append("\n").append(p.toString());
}
if (result.toString().equals("")) out.writeUTF("Not Found Product...");
out.writeUTF(result.toString());
}
socket.close();
}
private Product productFromLine(String line){
String[] element = line.replaceAll("\\s+", " ").split(" ");
return new Product(element[1], Integer.parseInt(element[2]), Integer.parseInt(element[3]));
}
}