5-6주차 정리

5 - 6주차가 마무리 되면서 자바의 전반적인 언어적 특성을 다시 한번 복습할 수 있었고 또한 오랜만에 프레임워크에 의존하지 않고 쿼리를 작성해 볼 수 있었다. (엄청난 반복...)

입출력 및 네트워크 부분은 자바를 계속 다뤄 왔음에도 대략적으로 이런것이 있구나 하고 넘어가곤 했었는데(자바의 언어적 특성을 이해하기도 전에 스프링 프레임워크에만 몰빵한 학습의 폐해) 오프라인으로 기계적인 반복학습으로 입출력이나 네트워크를 이해하는데 큰 도움을 얻었다...

이번 6주차 포스팅에는 지금까지 배워왔던 모든 지식을 담아 과제를 리펙터링 해보았다.

5주차 과제 - 상품 등록 및 조회 프로그램 작성

git repository : https://github.com/zezeg2/java-practice/tree/main/src/ch16/assign

  • 소켓 통신을 이용한다
  • 상품(Product)은 상품명, 가격, 수량에 대한 정보를 가지고 있다
  • 상품 등록 : 클라이언트는 상품 정보를 입력하고 이를 서버에 전송하고 서버는 이 상품에 대한 정보를 하나의 파일에 저장한다.
  • 상품 조회 : 클라이언트는 상품명을 입력하여 서버에 저장된(파일) 상품 정보를 조회할 수 있다.

패키지 구조

├── GlobalScanner.java 				// 전역에서 사용되는 커스텀 Scanner
├── Product.java 					// 상품 클래스
├── client 							// 클라이언트측 프로그램
│   ├── ClientRequest.java
│   └── ProductClient.java
└── server 							// 서버측 프로그램
    ├── ProductServerMain.java
    ├── ProductServerThread.java
    └── ProductService.java

공통 클래스

GlobalScanner.java

  • 메모리상에 하나의 인스턴스만 존재한다(싱글톤)

  • try-resource-with 를 통해 Scanner 인스턴스를 회수하도록 하기 위해 Closeable 를 구현한다

  • 사용자가 올바르게 값을 입력 하도록 하는 여러 메서드를 정의한다

    • Scanner getScanner() ⇒ Scanner 인스턴스를 리턴한다
    • int nextNum(String comment)comment 를 통해 입력에 대한 힌트를 주고 사용자가 int 타입의 입력을 받을 수 있도록 문자열 입력시 재입력 하도록 하고 최종 입력값을 리턴한다
    • String nextString(String comment)comment 를 통해 입력에 대한 힌트를 주고 사용자 입력을 받고 입력값을 리턴한다
    • int nextNumOrCheckReplace(String comment, String check, int replace)comment 를 통해 입력에 대한 힌트를 주고 사용자가 문자열 입력시 check 와 비교하여 일치한다면 replace에 해당하는 값을 리턴하도록 한다. 그렇지 않다면 재입력 하도록 하고 최종 입력값을 리턴한다
    • String nextStringOrReplace(String comment, String check, String 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 를 지역변수로 선언
  • 서버측 소켓 19999 포트와 소켓연결 및 소켓의 출력 스트림에 대한 보조 스트림(DataOutputStream) 생성
    • try-resource-with 구문을 통해 자원반납을 의무화 한다.
    • GlobalScanner 를 통해 사용자로부터 입력을 받고 입력값에 따라 프로그램 종료 및 ClientRequestpost()(상품 등록) , get()(상품 조회) 로직을 수행하도록 한다. 이 때 서버와 연결 된 상태의 socket을 인자로 넘겨 ClientReauset 인스턴스가 서버와 통신하며
    • key값은 OutputStream을 통해 서버측에 전달되며 서버측은 해당 key값에 따라 ProductService 인스턴스가 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

  • 메모리 상에 하나의 인스턴스만 생성되도록 한다(싱글톤)
  • void post() : 사용자가 여러 상품에 대한 정보를 입력할 수 있도록 로직 구성
    • 상품 인스턴스를 직렬화 하여 보내는것이 아니라(추후 구현예정) 인스턴스의 필드값을 읽고 한 인스턴스 당 하나의 라인에 정보를 입력하여 서버측에 보낸다(out.writeUTF())
    • 정상적으로 상품이 저장될 시 서버측으로부터 문자열을 전송받는데, 이를 콘솔에 출력한다.
    • 최종적으로 소켓을 정상종료 한다.
  • void get() : 사용자가 상품명 입력하고 이를 서버측에 전송, 서버측에서 데이터 조회 로직을 정상적으로 수행할 시 서버측에서 전송한 데이터를 콘솔에 출력
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

  • ExcutorService 쓰레드풀 생성
  • 19999 포트에서 클라이언트의 소켓연결을 리스닝
  • 클라이언트 소켓과 연결될 시 쓰레드 풀에서 서버 쓰레드(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

  • Thread 클래스를 상속하고 run 메서드를 재정의 한다
  • 실제 등록 및 조회의 로직을 수행하는 ProductService를 멤버로 가진다.
  • socket을 멤버로 가지며 생성자를 통해 socket이 초기화 되도록 한다
  • ProductClient 프로그램에서 입력된 사용자 입력 (key)이 전달되며 이 값에 따라 ProductServiceenroll 혹은 search 메서드가 실행된다
  • 연결수립, 연결 종료시에 콘솔에 로그를 출력한다(실행쓰레드 및 클라이언트 연결정보)
  • finally 구문을 통해 소켓 연결을 종료한다.
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

  • 메모리에 하나의 인스턴스만 생성되도록 한다(싱글톤)
  • List를 멤버로 가지며 생성자 호출시 초기화 되도록 한다.
    • productList 인스턴스를 생성하고 파일을 읽어 productList에 추가한다.
  • void enroll(Socket socket) :
    • 소켓 인스턴스를 인자로 전달받아 클라이언트와 통신한다.
    • 해당 메서드는 소켓으로부터의 I/O stream, FileWriter 자원을 사용하므로 try-with-resources 에 추가한다.
    • ClientRequset로부터 전송받은 데이터를 FileWriter를 통해 파일에 쓰고 라인별로 객체화 하여 ProductList에 추가해준다.
    • 정상적으로 저장시 OutputStream을 통해 정상 처리 되었음을 알리는 문자열을 전송한다
  • void search(Socket socket) :
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]));
    }
}

실행결과

  • 상품 등록 및 조회 실행결과

0개의 댓글