반응형

# Project

v26 -> Command 디자인 패턴 적용 // command를 nextLine()으로 입력받아, switch문으로 해당 역할을 수행하였다면,

          // Command 디자인 패턴을 적용하여 Command class를 인터페이스로 생성하고 통일할 메서드 명을 설정한다.

          // Handler의 기능을 각 별도의 Class로 구분하고, 해당하는 메서드 명을 Command class의 메서드 명으로 변경.

          // App에서는 미리 HashMap을 이용하여, <입력어, 메서드>를 이용하여 입력어에 해당하는 메소드를 호출.

          // Command에서는 오버라이딩 된 해당 메서드를 실행한다.

v27 -> 예외 처리 문법 적용 // try { } catch {} 적용 // try catch 문법을 이용하여, 

v28_1 -> Decorator 디자인 패턴 적용 // 데이터를 저장할 saveData(), 데이터를 불러올 loadData() 메서드를 정의한다.

          // saveData() 정의 // File Class의 기능을 이용한다. // File file = new File("./lesson.csv"); 

          // 현재경로(./)에 파일명 로딩(lesson.csv) // 현재 파일에 작성할 FileWriter를 준비 // FileWriter out = null;

          // 불러온 FileWriter에 불러온 File명을 넘긴다. // out = new FileWriter(file);

          // List에 저장된 데이터를 꺼내 CSV 형식으로 저장한다.  // 원하는 형식대로 저장하게끔 // String.fomat 적용

          // String line = String.format("%d,%s,%s,%s,%s,%d,%d\n", lesson.getNo(), lesson.getTitle(),
            lesson.getDescription(), lesson.getStartDate(), lesson.getEndDate(),
            lesson.getTotalHours(), lesson.getDayHours());

          // line에 저장된 String.format을 out에 작성한다. // out.write(line);

          // 이 작업을 반복문 안에서 처리한다. // for (Lesson lesson : lessonList) { ... count++; }

          // loadData() 정의 // File Class의 기능을 이용한다. // File file = new File("./lesson.csv"); 

          // 현재경로(./)에 파일명 로딩(lesson.csv) // 불러온 파일을 읽을 FileReader와, 데이터를 읽을 Scanner를 준비

          // 불러온 File을 읽기 위해 FileReader에, FileReader가 불러온 데이터를 한 줄씩 읽기 위해 Scanner에 넣는다.

          // in = new FileReader(file); // dataScan = new Scanner(in);

          // dataScan에 들어있는 데이터 한 줄을 String에 담는다. // String line = dataScan.nextLine();

          // 한 줄의 데이터를 구분해둔 값으로 분리한다. // String[] data = line.split(",");

          // 새로 담을 그릇을 준비한다. Lesson lesson = new Lesson();

          // 순서대로 필요에 의하면 형변환을 통해 각자의 값을 담는다. // lesson.setNo(Integer.parseInt(data[0]));

          // lesson.setDescription(data[2]); // lesson.setEndDate(Date.valueOf(data[4]));

          // 또한 현재(새로 로딩되어 백지인 상태) List에 add한다. // lessonList.add(lesson); count++;

          // 불러올 한 줄의 데이터가 없을 때까지 반복문을 돌린다 // while(true) { try { ... } catch(Exception e) { break; } }

          // 파일 읽기 중 오류가 발생할 수 있기 때문에, 오류처리를 한다.

          // 모든 파일 읽기가 끝나고, Scanner를 close()하고, FileReader를 close() 해준다.

          // 닫다가 오류가 발생할 수도 있으니 예외처리를 해준다. // 단 이때의 예외처리는 아무런 작업도 넣지 않는다.

v28_2 -> csv 문자열 기능을 추출 // 리펙토링 // 기능부분을 domain에게 넘긴다.

            // valueOf() 로 분리하기. // cvs data를 ","로 분할하고, 새로운 list에 추가하는 기능을 추출한다.

            // toCsvString()으로 분리하기. // list에서 데이터를 꺼내 CSV형식의 문자열로 변환하는 기능을 추출한다.

v28_3 -> JSON 사용 // csv의 치명적인 단점 // 데이터에 ,가 들어가면, 구분하지 못한다. // 오류 발생

            // BoardObjectFileDao는 Serialized해서 데이터를 저장, BoardJsonFileDao는 Json화하여 데이터를 저장.

            // JSON(JavaScript Object Notation)

            // JSON은 []안에 {}로 라인이 구분되어있고, 그 안에 no : " "안에 데이터가 들어간다.

            // JSON 사용 도구로 구글에서 만든 클래스인 GSON을 많이 사용한다.

            // GSON을 사용하려면, build.gradle dependencies 안에 GSON을 넣어줘야 한다.

            // implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.6' // ver 확인

            // https://mvnrepository.com/artifact/com.google.code.gson/gson // 링크 버전 확인하고 다운로드. 

            // 추가하고 gradle eclipse를 powershell에서 실행시켜줘야 한다. // 사용할, gradle파일 있는 위치에서 실행.

            // 정상적으로 추가가 됐다면 // 'Referenced Libraries' 노드에서 gson 라이브러리 파일이 추가됨.

            // File file = new File("./lesson.json"); // csv파일에서 json파일로 바꿨으니 보관된 파일 정보를 바꿔준다.

            // fromJson(in, Lesson[].class) // fromJson으로 불러온 in(FlieReader안에 불러온 file을 넣음)을 

            // Lesson[].class의 정보를 넘겨주어야 한다. 배열의 정보를 넘겨주어야 list로 구분할 수 있다.

            // new Gson(); // Json을 사용할 도구로 Gson에 넣는다.

            // Array.asList // Gson으로 넘어온 데이터를 ArrayList를 만든다.

            // lessonList.addAll // Array.asList는 add()기능이 없는, ArrayList를 만드는 메소드 이다.

            // 따라서 온전한 ArrayList인 lessonList에 넣어주어야 한다.

v28_4 -> buffer 기능 추가 // Decorator를 이용하여 buffer를 중간 기능에 넣는다. // 처리속도가 더 빨라진다.

v29 -> 바이너리 형식으로 입출력 데이터 기능 변경 // json을 사용하다가 바이너리 형식을 사용하기 때문에

          // fromJson 기능을 사용하지 못한다. // 바이너리로 넘어온 데이터를 일일히 형식별로 나눠서 저장하여야 한다.

          // 보통 실무에서는 Json을 많이 사용하나, 이런 바이너리 데이터 형식을 사용하는 데도 있다.

          // 모든 기능들을 character로 읽어오는 Json과 다르게 바이너리 데이터를 읽어오기 때문에

          // 바이너리 데이터를 읽어오는 애들로 바꿔주어야 한다. (Reader -> InputStream   Writer -> OutputStream)

          // Json는 파일에서 데이터를 꺼내오는 역할과, 꺼내온 데이터를 Lesson[].class에 맞게 가공하는 것을 했다면

          // 바이너리 형식에서는 File에서 꺼내는 애 따로, 빠르게 꺼내는 애 따로, Lesson에 맞게 가공하는 애 따로

          // FileInputStream / BufferedInputStream / DataInputStream 이렇게 따로 구분하여 넣어야 한다.

          // 또한 마지막 DataInputStream의 메소드를 이용해 lesson 데이터에 맞게 가공을 거쳐야 한다.

v30 -> 직렬화와 역직렬화 

           // Serializable화를 통해 lessonList에 맞는 규격으로 파일을 내보낸다면,

           // 불러올 때도 lessonList에 맞는 규격에 데이터를 담으면 된다.

           // Lesson class에서 implements Serializable를 명시하고 // serialVersionUID는 필요에 따라 추가 선언

           // out.writeObject(lessonList); // ObjectOutputStream을 통해 lessonList의 규격이다.라고 내보낼 수 있다.

           // new ObjectInputStream // ObjectInputStream을 통해 내보냈던 데이터를 받아들인다.

           // lessonList = (List<Lesson>) in.readObject();

           // 불러온 데이터(Object)가 Lesson규격을 따르는 리스트임을 앞에 명시해주어 형태를 갖게 한다.

           // 그 데이터를 lessonList에 담는다. // serialVersionUID의 값을 별도 지정하지 않았다면,

           // Lesson의 필드 추가/삭제 등 변동이 있을 시 실행 오류가 발생한다.

           // 따라서 IOException 예외처리를 Exception으로 바꾸어 주어야 한다.(상위 클래스로)

v31_1 -> Obsever 디자인 패턴 적용 //  (= 옵저버 listener 리스너) 

옵저버 디자인 패턴 ? // 옵저버 패턴이란 특정 상황에 실행되는 메서드들을 모아서 실행하는 방법.

          // 하나의 클래스에 다수의 메서드를 보관하는 것이 유지 보수하기에 비효율 적이다.

          // interface를 만들어서, 특정 상황에 해당되는 메서드를 규칙으로 선언하고.

          // interface를 상속받아 해당 상황에 다수의 메서드(class로 분리)이 작동하게끔 한다.

          // 인터페이스에서는 특정 상황(ex 시작, 종료)의 메서드를 선언

          // 특정 상황이 1개 이상일 경우(ex 시작, 종료) 

          // 인터페이스를 규칙을 따르는 helper 클래스를 만든다.

          // helper 클래스에서는 인터페이스의 메서드를 일반 메서드로 구현한다.

          // 상속받는 helper 클래스를 만드는 목적

          // 메서드마다 시작에만 작동을 하거나 종료에만 작동하는 경우가 있다.

          // 인터페이스를 다이렉트로 받으면, 모든 메서드를 구현해야 한다.

          // 인터페이스-helper클래스를 상속받으면 필요 메서드만 구현하면 된다.

          // 실행시 작동하는 메서드는 실행 메서드만 오버라이딩.

          // 종료시 작동하는 메서드는 종료 메서드만 오버라이딩.

          // 나중에 실행(종료)시에 메서드가 추가로 필요하다면

          // helper클래스를 상속받으며, 실행(종료)메서드 명을 따르는 class를 만든다.

          // 이 메서드들을 Observer(또는 listener)라고 부르며,

          // 이 메서드들을 관리하는 패턴이 옵저버 디자인 패턴이다.

          // 옵저버들이 공통적으로 해당되는 특정 class에 observers List를 만든다. // ex Car class

          // List<CarObserver> observers = new ArrayList<>(); // 넘어온 Observer들의 이름을 받을 list를 만든다.

          // public void addCarObserver(CarObserver observer) { observers.add(observer); }

          // list에 넘어온 Obsever의 이름을 추가한다.

          // public void removeCarObserver(CarObserver observer) { observers.remove(observer); } 

          // list에 넘어온 Obsever의 이름을 삭제한다.

          // public void start() { for (CarObserver observer : observers) { observer.carStarted(); } }

          // start() 메소드가 호출되면, observers 배열의 carStarted() 메소드를 다 호출한다.

          // public void stop() { for (CarObserver observer : observers) { observer.carStopped(); } }

          // stop() 메소드가 호출되면, observers 배열의 carStopped() 메소드를 다 호출한다.

          // start()와 stop()에서 호출되는 list는 둘이 동일하다.

          // 인터페이스 규칙을 따르는 helper클래스에서 모든 인터페이스의 메소드를 구현하였다.

          // 옵저버들은 start()와 stop() 각자에 맞게 오버라이딩 하였다.

          // --------------------------------------------------------------------------------------------------------

          // 옵저버 패턴으로 데이터 로딩 기능과 데이터 저장 기능을 별도 class로 추출하자.

          // v31_1은 호출 규칙을 정한다. // ApplicationContextListener(interface)

          // ApplicationContextListener(interface)에 contextInitialized();(실행) contextDestroyed();(종료)를 만든다.

          // public void service() // App으로 돌아가서 main메소드를 인스턴스 service 메소드로 바꾼다.

          // Scanner, Deque, Queue List 등등 static 필드를 인스턴스 멤버로 바꿔준다. (static을 뗀다.)

          // loadBoardData() saveBoardData() printCommandHistory() 등등 static 메서드를 인스턴스 멤버로 바꿔준다.

          // 맨 밑에 main 메소드를 만들어 service()를 호출한다.

          // 실무에서는 main 메소드에 명령어를 많이 넣지 않고, 스태틱 메서드를 잘 사용하지 않는다. (한번만 쓰더라도)

          // Interface에 규칙을 따르는 필드와 메서드 정의.

          // Set<ApplicationContextListener> listeners = new HashSet<>(); // 옵저버 목록을 관리할 객체를 준비한다.

          // listeners 는 ApplicationContextListener의 규칙을 따르는 옵저버의 목록이다.

          // 같은 값을 중복 저장하지 않도록 set을 사용한다. // set은 등록 순서를 따지지 않는 단점이 있다.

          // public void addApplicationContextListener(ApplicationContextListener listener) // 옵저버 등록 메서드

          // public void removeApplicationContextListener(ApplicationContextListener listener) // 옵저버 제거 메서드

          // listeners는 위의 addApplicationContextListener Set목록에 옵저버를 추가하며,

          // listeners는 위의 removeApplicationContextListener Set목록에 옵저버를 삭제하며,

          // private void notifyApplicationInitialized() { // 어차피 내부에서만 호출할거라 private로 선언

          // for (ApplicationContextListener listener : listeners) { listener.contextInitialized(); } // 시작시 실행.

          // notifyApplicationInitialized() // 메서드명만 보면 알 수 있게 메서드 명을 짓는다.

          // 코드로 되어있으면 확인이 필요하고 주석을 다는 코딩은 좋은 코딩이 아니기 때문.

          // private void notifyApplicationDestroyed() { // 종료시 실행.

          // for (ApplicationContextListener listener : listeners) { listener.contextDestroyed(); } }

v31_2 -> 중간클래스 추가 // interface의 규칙을 구현하는 중간클래스

          // public class DataLoaderListener implements ApplicationContextListener {

          // interface의 기능을 실제로 구현한다.

          // contextInitialized() // App 실행시 데이터를 로딩하는 메서드

          // contextDestroyed() // App 실행 마지막에 데이터를 저장하는 메서드

          // App을 실행할 때 가장 먼저 List를 만들어야 하는 DataLoaderListner이다.

          // load로 데이터를 받아오기 때문이다. // 데이터가 없어도 빈 List를 생성한다.

          // App에서 처리하던 데이터 로딩과 저장 기능을 가져오기 때문에 new ArrayList를 App에서 옮겨온다.

          // List lessonList = new ArrayList<>(); // memberList boardList 포함.

          // App에서는 DataLoaderListener가 만들어 준 List들을 가져와서 사용하면 되는데

          // 하위 클래스에서 만든 필드를 상위 클래스로 가져올 수 없다.

          // 따라서 상위 클래스에서 빈 객체를 만들어, 하위 클래스로 전달해야 한다.

          // 그러나 어떤 List를 만들지 모르기 때문에 특정 List를 만들어서 전달 할 수는 없기 때문에

          // Map<String, Object> context = new HashMap<>(); // List를 주고 받을 HashMap을 준비한다.

          // public void contextInitialized(Map<String, Object> context) { // HashMap을 이용하여 List를 주고 받는다.

          // context.put("boardList", boardList); // DataLoaderListener에서 put으로 로딩된 List를 HashMap에 넣고

          // List boardList = (List) context.get("boardList"); // App에서 get으로 저장된 List를 HashMap에서 꺼낸다.

          // service()에 app.addApplicationContextListener(new DataLoaderListener()); 추가한다. // 옵저버 추가

          // 실행 순서

      1.  // main 메소드의 App app = new App(); 가 실행된다. // 인스턴스 필드를 생성한다.

      2.  // app.addApplicationContextListener(new DataLoaderListener()); // App과 연결

          // new DataLoaderListener()의 인스턴스 필드를 생성한다.

          // new App에 생성된 HashSet listeners에 add로 new DataLoaderList를 연결한다.

          // App에 listeners에는 DataLoaderList의 주소(new List들)가 담겨있다.

      3.  // service()를 실행한다. // 가장 먼저 notifyApplicationInitialized();가 실행된다.

          // new App의 listeners안에 들어있는 listener(DataLoaderListener)에게 context를 전달한다.

      4.  // listener의 contextInitialized를 실행 시킨다. // HashMap인 context를 같이 넘긴다.

      5.  // loadBoardData(); 를 실행한다. // loadLessonData(); // loadMemberData(); 마찬가지

      6.  // loadBoardData를 통해 DataLoaderListener의 각 List에 load된 List가 들어있다.

      7.  // context.put("boardList", boardList); // 넘겨받은 HashMap에 DataLoaderListener의 List를 저장한다.

      8.  // List boardList = (List) context.get("boardList"); // App에서 DataLoaderListener의 List를 꺼낸다.

v31_3 -> 중간 클래스 추가 +1 // 옵저버를 사용하는 목적

          // public class GreetingListner implements ApplicationContextListener // 실행, 종료 문구 출력 옵저버

          // app.addApplicationContextListener(new GreetingListner()); // App에 코드 추가

          // addApplicationContextListener 기능으로 listeners에 GreetGreetingListner 추가됨.

          // 즉 특정 상황(실행, 종료)시 Interface의 규칙을 오버라이딩, listener.contextInitialized로 실행시킨다.

          // Iterator처럼 호출 규칙을 통일 시킨다고 생각하면 이해하기 쉽다.

   결론 // Set listeners = new HashSet<>(); // DataLoaderListener, GreetGreetingListner 등 옵저버들의 목록 저장

         // Map<String, Object> context = new HashMap<>(); // DataLoaderListener의 List를 저장한다. 

v32_1 -> 네트워킹 // Client와 Server 구분 // 왜 구분을 해야 하는가?

         // 기존에 사용하는 App의 경우에는 한 컴퓨터에서만 사용이 가능 하다.

         // 따라서 모든 App을 사용하는 컴퓨터가 데이터가 다르며, 다른 컴퓨터의 파일에 접속을 할 수 없다.

         // Client와 Server를 구분해 두면, Server에 파일을 둠으로써 Client들은 파일을 공유할 수 있다.

         // Actor(사용자)가 ClientApp(UI 제공)을 이용해서 요청을 한다.

         // ServerApp(데이터 처리)는 ClientApp에서 넘어온 요청에 대한 응답을 한다.

         // 무언가의 데이터를 요청하는 쪽이 Client이다.

         // Client 폴더와 Server 폴더를 터미널에서 mkdir로 생성 및 gradle init 및 gradle eclipse 

         // build.gradle에 id application 대신 id eclipse로 바꾸고 application 블럭 삭제

         // tasks.withType(JavaCompile) 블럭 기타 파일에서 가져와서 넣기

         // src/main/resources와 src/test/resources에 README.md를 넣어준다. // 파일이 없으면 깃에 업로드 되지 않음

v32_2 -> Client와 Server의 연결 //

         // Socket socket = new Socket("localhost", 9999);

         // localhost는 ip를 의미하며, 접속하려는 서버의 IP를 의미한다.

         // 9999는 서버소켓의 포트를 의미한다.

         // PrintStream out = new PrintStream(socket.getOutputStream()); // getOutputStream은 사용하기가 불편하다.

         // 따라서 PrintStream 이라는 데코레이터를 붙혀준다.

         // out.println("Hello"); // 서버에 메세지를 전송한다. // 서버가 메세지를 받을 때까지 리턴하지 않는다.

         // blocking 방식으로 동작한다.

         // Scanner in = new Scanner(socket.getInputStream()); // getInputStream도 사용하기가 불편하다.

         // Scanner는 데코레이터는 아니지만, 데코레이터 역할을 수행하는 Helper 객체이다

         // 데코레이터의 조건은 같은 조상을 공유해야 한다. 

         // String message = in.nextLine(); // 서버가 응답한 메세지를 수신한다.

         // 서버로부터 메세지를 받을 때까지 리턴하지 않는다. // blocking 방식으로 동작한다.

         // java.net.ConnectException // 예외처리 : 서버와 연결이 안됐을 때 나타나는 메세지.

         // Server쪽 연결 준비

         // ServerSocket serverSocket = new ServerSocket(9999) // 서버 소켓(포트)을 준비한다.

         // Server는 서버소켓(포트)를 만들고 기다리는 입장 // 서버 소켓은 서버와 연결하는 패키지이다.

         // java.net.BindException : Address already in use  // 9999포트가 이미 사용중(실행중)이라는 뜻

         // 예외처리 : 이미 서버가 실행되어 있는 상태에서 또 실행했을 때

         // 이전 서버가 정상적으로 종료되지 않은 상태에서 또 실행한 경우

         // Socket socket = serverSocket.accept(); // 서버에 대기하고 있는 클라이언트와 연결한다.

         // ServerSocket을 생성하고, accept가 되지 않으면, 계속 서버는 대기한다.

         // Scanner in = new Scanner(socket.getInputStream());

         // PrintStream out = new PrintStream(socket.getOutputStream()))

         // String message = in.nextLine(); // 클라이언트가 보낸 메세지를 수신한다.

         // out.println("Hi"); // 메세지를 받을 때까지 리턴하지 않는다. 

         // 실행 순서

     1.  // 서버 App을 미리 실행하고 고객 App을 실행한다. 

     2.  Server // ServerSocket serverSocket = new ServerSocket(9999) // Server에서 서버 소켓을 준비하고 대기함.

     3.  Client // Socket socket = new Socket("localhost", 9999); // Client에서 소켓에 접속함.

     4.  Server // Socket socket = serverSocket.accept(); // Server에서 Client소켓을 확인함.

     5.  Server // String message = in.nextLine(); // Server에서 Client 메세지를 받을 String을 준비한다.

     6.  Client // out.println("Hello"); // Client에서 Server에 메세지를 보냄.

     7.  Client // String message = in.nextLine(); // Client에서 Server 메세지를 받을 String을 준비한다.

     8.  Server // out.println("Hi"); // Server에서 Client에 메세지를 보냄.

         // 위 예제는 네트워킹을 처음 접하는데에 있어 보다 쉽게 조금 느끼기 위해 만든 과정이다.

         // ClientApp // 서버ip와 포트를 입력 받고 서버와 연결시킨다.

         // 서버ip와 포트를 입력받는데에, 오류가 발생할 수 있으니 try문 안에 넣는다.

         // try문 안에서 변수 선언은 불가하기 때문에, 또한 try문 안에서만 사용이 가능하기 때문에

         // String serverAddr = null;  int port = 0; Scanner   keyScan = new Scanner(System.in);

         // 사용자로부터 입력받을 변수(serverAddr, port) 다른 블럭에서도 사용할 KeyScan을 먼저 선언한다.

         // try문에서 에러가 발생한다면 keyScan을 닫아줄 수 없으니, catch 안에 close()를 넣어준다.

         // try ()안에 Socket    PrintStream    Scanner를 선언하여 문제 발생시 자동적으로 close되게 한다.

         // ServerApp // 서버ip와 포트를 입력 받고 서버와 연결시킨다.

         // try ()안에 ServerSocket를 선언하여 문제 발생시 자동적으로 close되게 한다.

         // ServerSocket을 생성하고 serverSocket.accept();을 기다린다. // Client에서 접속할 때까지

         // try ()안에  Socket    PrintStream    Scanner를 선언하여 문제 발생시 자동적으로 close되게 한다.

         // processRequest(Socket clientSocket) // Client와 Server가 요청을 주고받는 것을 별도 메서드로 추출한다.

         // accept() 이후에 Server가 메세지를 전송하고 Client의 응답을 받는 과정을 별도의 메서드로 추출한 것이다.

v32_3 -> Server와 Client 분할 // Client(User Interface)가 볼 부분과 서버에서 관리할 코드를 분할한다.

           // Client에서 담당할 프로그램 분할 // 사용자로부터 명령을 입력받는 부분을 ClientApp에서 담당한다.

           // App에서 Client의 명령을 입력받고 해당 커멘드에 맞는 문구를 처리하는 service()를 Client로 가져온다.

           // notifyApplicationInitialized   notifyApplicationDestroyed   boardList  lessonList  memberList // 삭제

           // 데이터는 Server가 관리하게 할 것이기 때문에 Client는 데이터를 직접적으로 다루면 안된다.

           // 사용자로부터 입력을 받는 Scanner, 사용자에게 입력을 유도하는 Prompt를 인스턴스로 선언한다.

           // service()에는 사용자의 command를 저장하는 commandStack, commandQueue를 보관한다.

           // service() 내부에서만 사용되기 때문에 내부에 선언한다. // 추후 필요하면 외부에 선언하면 되기 때문

           // 32_3 Client에서는 우선 네트워크 부분을 주석처리하고, util과 handler에서 command를 가져왔다.

           // commandMap.put은 이후에 추후로 추가할 예정이기 때문에 일단 지워둔다.

           // HashMap<String, Command> commandMap = new HashMap<>(); // HashMap안에는 데이터가 없다.

           // Server에서 담당할 프로그램 분할 // 데이터는 Server가 관리한다.

           // App에서 Client의 명령을 입력받고 해당 커멘드에 맞는 문구를 처리하는 service()를 Client로 가져온다.

           // notifyApplicationInitialized   notifyApplicationDestroyed   boardList  lessonList  memberList // 빼고 삭제

           // 데이터에서 옵저버 프로그램을 관리하며, List들을 관리해야 한다. // 즉 Client의 기능 부분을 다 삭제한다.

           // domain 폴더와, context 폴더, DataLoaderListner 파일을 가져왔다.

           // 32_3 Server에서는 우선 네트워크 부분을 주석처리하고, 옵저버에 해당하는 메서드와 필드를 가져왔다.

           // ClientApp을 복사해와서 ServerAppTest로 이름을 변경하여 ServerApp을 테스트한다.

           // ServerAppTest에서 추가 기능이 정상적으로 작동하는지 기능을 덧붙혀 테스트한다.

v32_4 -> HashMap 기능 추가 // 

           // ClientApp에서 모든 UI(User interface)에 대한 부분을 담당 할 수 없으니,

           // 작업에 대한 것(command)을 분리한다.

           // ClientApp에서 command를 다루는 부분을 별도의 메서드로 추출한다. // processCommand()

           // service()에는 서버 연결에 대한 코드를 집어 넣는다 // 32_3에서 주석처리 되었던 기능

           // 각 Command에게 service에서 연결된 Server와의 통신 수단을 넘겨준다.

           // processCommand(out, in); // Client가 직접 Server와 송수신 받지 않는 이유

           // 기능을 분할해 두었기 때문에, 하나의 메서드에서 처리할 수 없고, 하나의 class에서 처리할 수 없다.

           // 따라서 각 기능에게 Server와의 연결권 (out, in)을 넘겨준다.

           // Server에 Command를 보관하지 않는 이유 // 

           // ex) add 기능을 보면, ID PWD 등등 일일히 하나하나 입력할 때마다 서버와 송수신하기엔 비효율적이다.

           // 서버 부하, 오류 발생, 작성 취소 등의 이유와 또한 항목이 많다면 효율적이지 않다.

           // 따라서 Client에서 기능에 대한 처리를 다 끝내고, 그 항목을 Object로 넘겨받아 server에서 처리하는 것이다.

           // HashMap의 경우에 기존에는 prompt와, List를 넘겨 받았다. // 이제 List는 Server에게 있다.

           // commandMap.put("/board/list", new BoardListCommand(out, in)); // Command에게 out,in을 넘겨준다.

           // commandMap.put("/board/add", new BoardAddCommand(out, in, prompt)); // prompt가 필요한 경우

           // 해당 기능들의 생성자를 변경하고, 메서드를 수정한다. // Command 메서드 전부 수정 필요.

           // Client 종합 // Server와의 연결(out, in)을 메서드에 넘기고 // 또 그 연결권을 세부 메서드에게 넘기는 식

           // ServerApp은 sevice()를 통해 Client와 연결한다. // 데이터 load를 먼저 하고 Clinet와 연결함.

           // while 반복문을 통해 사용자가 특정 명령어를 입력할 때까지 반복되게 한다. // 무한 반복

           // if (processRequest(socket) == 9) { break; } // while문에 특정 값을 리턴하면 빠져나가게끔 장치.

           // 서버를 온전히 닫기 위한 장치이다. // 서버 종료. // server 종료 // 서버 닫기 // 서버 끄기 

           // server stop // 서버 스탑 // server off // 서버 오프

           // Client에서 processCommand를 통해 들어오는 입력에 대응하는, processRequest(socket) 메서드를 만든다.`

           // processRequest // 넘어온 socket을 통해 Client와의 연결(in, out)을 사용한다.

           // Server에서 List를 한번에 받아오려 readObject()를 사용하려하나, Scanner라 사용이 불가능하다.

           // Scanner 대신 ObjectInputStream를 사용하고, ObjectInputStream에서는 nextLine()이 불가능하다.

           // readUTF()로 바꿔준다. 또한 String 변수명이 message보다 request가 어울리기 때문에 변수명도 바꿔준다.

           // 코드를 짜는 첫 순간부터 모든걸 고민하지 말고, 코딩하다가 이렇게 막히는 경우에 처리도구를 바꿔준다.

           // Server와 Clinet는 각자 서로 연락을 주고받는 규격이 항상 같아야 한다.

           // ex) ObjectInputStream을 사용하면 ObjectOutputStream을, Reader면 Writer를 사용

           // 이것을 프로토콜(규칙)이라고 한다. // 프로토콜에 맞게 Client, Server 모두 수정

           // Client // processCommand에서 String command를 입력후 out.writeUTF(command);로 보내면

           // Server // processRequest에서 String request = in.readUTF(); 으로 받아 해당 명령어를 처리하는 방식이다.

           // ObjectOutputStream // ObjectStream은 버퍼를 보유하고 있어, out.flush();를 통해 보내줘야만 한다.

           // request.equals("/server/stop") // server를 강제 종료하기 위한 명령어를 만들어 준다.

           // 실무에서는 이 리턴 값에 접근하려면 ID와 PWD를 만족해야 가능하게끔 한다.

           // List boards = (List) context.get("boardList"); // 서버에서 List를 만들어 가지고 있으며 필요시 리턴한다.

           // else if (request.equals("/board/add")) // Client의 입력 값이 Client의 Command 메소드명과 같을 때

           // Command 메소드에서 사용할 리스트를 server에서 만들어 command 메소드에게 전달한다.

           // 만약 request.equals("/board/detail")과 같이, List에 대한 값이 필요할 때에는, command에서 값을 물어보고

           // 입력 받은 값을, Server에 제공하게끔 조치하면 된다. // 밑에서 추가적으로 다룸.

           // command를 통해 요청받은 List가 정상적으로 있다면, out.writeUTF로 서버에서 List를 보내줘야 한다.

           // BoardDetailCommand(ObjectOutputStream out, ObjectInputStream in, Prompt prompt) // 생성자 수정

           // BoardDetailCommand처럼 Command처리 하는 모든 메서드도 수정해야 한다.

           // Client에서 List를 전달 해주지 못하기 때문에 Server와 연결할 수 있는 in out을 넘겨주고

           // ObjectOutputStream out; ObjectInputStream in; // 변수 선언 후, 생성자로 넘겨받은 in out에 넣어야 한다.

           // BoardDetailCommand의 경우 index의 번호가 필요한데, 사용자로부터 index를 입력받거나

           // int no = prompt.inputInt("번호? "); // no를 입력받아, 해당 index를 찾아내는 방법이 있다.

           // no를 입력 받으면 List에서 값을 비교해야 하기 때문에 Server에서만 처리가 가능하다.

           // out.writeUTF("/board/detail"); // Server로 해당하는 명령어 처리를 보내고,

           // out.writeInt(no); // 사용자로부터 입력 받은 no 값을 Server로 보낸다.

           // out.flush(); // ObjectOutputStream은 자체적으로 버퍼를 가지고 있어, 꼭 flush를 써서 확실히 보내야 한다.

           // String response = in.readUTF(); // Server에서 정상적으로 처리 됐는지 구분 값을 받는다.

           // response의 값으로 FAIL이 온다면, Server에서 보낸 값을 출력한다 // "해당 번호의 게시물이 없습니다."

           // FAIL이 아니라면 정상적인 리스트를 보냈다고 판단한다. // 코드를 그렇게 짜 두었기 때문.

           // Board board = (Board) in.readObject(); // Server에서 보내준 List는 Board 객체이기 때문에 Board에 담는다.

           // 해당 board를 편의에 맞게 원하는 데이터를 출력하게끔 조치하면 된다.

v32_5 -> command처리 메서드 리펙토링 // 

           // List들을 공유할 수 있게 인스턴스필드로 선언한다. // 단, 로딩이 끝난 후에 HashMap에 값을 넣어줘야 한다.

           // 따라서 sevice()에서 notifyApplicationInitialized이후 데이터를 불러온 이후에 HashMap에 값을 넣는다.

           // if (request.equals("/board/list")) { // request들에 해당하는 command 처리 메서드들을 분리한다.

           // 이클립스에서 alt+shift+m을 이용하면 손 쉽게 메서드들을 분리할 수 있다.

           // 떼어낼 부위 드레그, alt+shift+m 후 메서드명 정의, access modifier(접근범위) 설정 후 처리

           // 이후 switch case문으로 바꿔준다. // if문보다 짧고 간결해서 직관적으로 바뀐다.

v32_6 -> 커멘드 패턴 적용 // v32_3 Client command 패턴 적용처럼 Server에 command 패턴을 적용한다.

            // ClientApp이 Command기능 부분을 분리한 것처럼, Server에서도 기능을 분리한다.

            // Command에 해당하는, Servlet을 만들어 그 기능을 담당하도록 한다.

            // servlet 폴더를 생성하고 기능별 메서드를 추출한다. // Client는 command, Server는 servlet

            // public interface Servlet { // 이 Servlet은 Client와 통신하기 위해 Server로부터 in, out을 받는다.

            // void service(ObjectInputStream in, ObjectOutputStream out) throws Exception;

            // throws Excetion으로 service()호출자에게 예외처리를 넘긴다. // 따라서 throws Exception을 붙여야 함.

            // public BoardUpdateServlet(List boards) { // 이건 생성자 // Servlet을 구현하는 class로 추출한다. 

            // Server에서는 List를 보유하고 있고, Server에서 List에 대한 처리를 할 class이니 생성자로 List를 받는다.

            // update add는 board(Object)객체를 받고, detail과 delete는 no(int)를 넘겨받는다.

            // List의 기능 set remove add와 Board의 기능 get을 이용해 해당 부분을 채워준다.

            // ServerApp에서 처리하던 기능을 해당 Servlet에게 넘겼기 때문에

            // Map<String, Servlet> servletMap = new HashMap<>(); // HashMap으로 해당 Servlet을 관리한다.

            // Servlet servlet = servletMap.get(request); // switch case문을 사용할 필요 없이 Map을 이용하면 된다.

            // servlet.service(in, out); // 해당 sevlet의 service 호출 및 Client와의 연결을 넘겨준다. (생성자로 받기 때문)

v32_7 -> Data Access Object // DAO 적용 // 데이터 관련 처리를 전담하는 class를 만든다.

            // 현재 각 List들을 배열, Queue, Stack, File, 웹하드, 등등 다른 방식으로 교체를 한다면

            // 교체시마다 모든 Command패턴을 수정해야되는 단점을 가지고 있다.

            // 따라서 데이터 처리 코드를 담당하게 별도로 객체화 시킨다. // DAO

            // DAO란? // 데이터 처리 방식을 캡슐화(추상화)하여 객체의 사용을 일관성 있게 만든다.

            // 즉 데이터 처리 방식에 상관 없이 메서드 사용을 통일화 한다.

            // 각 Servlet 기능에서 Client와의 연락을 주고 받는 역할만 담당하게 한다는 이야기 이다.

            // ex) AddServlet에서는 Add와 관련된 연락을 Clinet와 주고 받는 역할, Delete는 delete에 대한 연락 등등

            // Servlet이 Client로부터 no나 board 데이터를 받으면, 이 데이터를 DAO에게 넘겨준다.

            // DAO는 Servlet로부터 no나 board 데이터를 받으면, 이 데이터를 가지고 실질적인 CRUD 기능을 담당한다.

            // dao 폴더를 만들고 BoardFileDao class를 만든다. // List에 대한 실 접근을 FileDao가 담당하게 한다.

            // List 데이터 담당이 생겼기 때문에 DataLoaderListener가 이제 List 데이터에 실제로 접근하지 않아도 된다.

            // 경로만 넘겨주고 야 가서 깨워와 정도의 알람시계 역할을 담당하고,

            // FileDao가 List를 깨워서 데려오면 context(List 전달바구니)에 넣는 역할만 담당하면 된다.

            // FileDao가 데이터를 관리하기 때문에 DataLoaderListener의 saveBoardData loadBoardData를 가져온다.

            // List<Board> list; // saveBoardData와 loadBoardData에서 처리 할 인스턴스를 선언한다.

            // data를 불러오고 저장할 파일명도 수정될 수 있다.

            // 따라서 BoardFileDao를 실행할 때 filename을 넘겨주고 filename을 생성자로 받는다.

            // insert  getList  get  update  delete 기능을 추가한다. // get과 getList는 예전에는 이런식으로 지었다.

            // get은 findByNo, getList는 findAll로 이름을 변경한다. // 최신 트렌드이다. 보다 직관적으로 바뀜.

            // update, add, delete 후, 데이터를 바로바로 저장하도록 한다. // update, add, deletesavedata(); 추가

            // out.reset(); // update, add, delete 후 기존 캐시된(임시보관된) 데이터를 보관할 수 있어 초기화를 시켜준다.

            // loadData(); // 생성자에 클래스 실행시 아예 데이터를 로딩해올 수 있게 loadData(); 추가

v32_8 -> FileDao generic 적용 // AbstractObjectFileData 추가 // saveData, loadData 호출 방법 통일시키기

            // BoardObjectFileDao 이름 변경 // BoardFileDao 등등 클래스명에 Object를 추가시켜준다.

            // AbstractObjectFileData class 생성 // loadData와 saveData indexOf를 추상클래스에서 정의한다.

            // protected String filename; protected List list; 인스턴스도 추상클래스에서 정의한다.

            // 인스턴스와, loadData saveData indexOf가 하위 클래스에서 접근 가능하게 protected로 접근범위 변경

            // loadData와 saveData는 board lesson member 코드가 동일하기 때문에 AbstractObjectFileData로 뽑는다.

            // protected abstract <K> int indexOf(K key); // indexOf의 경우 List마다 다를 수 있어 제네릭을 적용한다.

            // AbstractObjectFileData에서 정의할 수 없다 // List마다 다를 수 있기 때문.

            // BoardObjectFileDao extends AbstractObjectFileDao<Board> { // 추상클래스를 상속 받아 만든다.

            // 인스턴스, loadData saveData를 상위 클래스에서 정의했기 때문에 지운다.

            // protected <K> int indexOf(K key) { // indexOf의 경우 List마다 Key타입이 가변적이다. 

            // if (list.get(i).getNo() == (int) key) { // 해당 List에서 key 사용 부분에 명시적 형변환을 해준다.

v32_9 -> Json 적용하기 // 파일 입출력 방식 변경

            // implementation group'com.google.code.gson'name'gson'version'2.8.6' // build.gradle에 추가 //

            // gradle cleanEclipse // build.gradle은 v28_3 확인하기 // 파워쉘에서 Eclipse 설정파일 지우기

            // gradle eclipse // 설정파일 다시 셋팅

            // AbstractObjectFileDao를 복사하여 AbstractJsonFileDao로 새로 파일을 만든다.

            // (BufferedReader in = new BufferedReader(new FileReader(file))) // ObjectInputStream은 필요 없다.

            // Json은 character로 읽어오기 때문에, 기존에 바이너리로 읽어오는 형식인 inputStream은 사용할 수 없다.

            // ObjectInputStream을 사용하는 이유는 읽어온 바이너리 데이터를, List 형식에 맞게끔 바꾸려 하였던 것이다.

            // list.addAll(Arrays.asList(new Gson().fromJson(in, Lesson[].class))); // 기존에는 이렇게 Json을 사용하였다면,

            // Generic을 사용하여 어떠한 list의 데이터가 넘어올 지 모른다. // Lesson[].class 자리를 말함.

            // AbstractJsonFileDao<T> // T가 실제로 어떤 것을 가리키는지 파악 하는 방법

            // Class<?> currType = this.getClass(); // 현재 클래스의 정보를 알아낸다. // BoardJsonFileDao를 가리킴

            // Type parentType = currType.getGenericSuperclass(); // 제네릭 타입의 수퍼 클래스 정보를 알아낸다.

            // AbstractJsonFileDao<Board>를 가리킨다.

            // ParameterizedType parentType2 = (ParameterizedType) parentType;

            // parentType의 타입 파리미터를 추출한다. 

            // 만약 타입 파라미터가 class My<T,S,U,V> {...} 라면 타입 파리미터 목록은 T, S, U, V 의 목록이다.

            // Type[] typeParams = parentType2.getActualTypeArguments(); // parentType2의 배열을 만들어 준다.

            // parentType2에서는 타입이 T하나 뿐이라 T밖에 없다. // My<T,S,U,V>라면 T,S,U,V 순서의 배열을 만든다.

            // Type itemType = typeParams[0]; // T를 뽑아내고 싶은 것이니, 배열의 0번방을 찾는다.

            // T[] arr = (T[]) Array.newInstance((Class) itemType, 0); // T타입으로 만들어진 크기 0의 배열을 만든다.

            // 이 배열을 사용하려는 것이 아닌, arr.getClass()로 T의 class 정보를 가져오기 위해 만든 것이다.

            // T[] dataArr = (T[]) new Gson().fromJson(in, arr.getClass()); // 새로 배열을 만든다.

            // for (T b : dataArr) { list.add(b); } // for문으로 list에 값을 넣는다.

            // BoardObjectFileDao를 복사해와서 BoardJsonFileDao로 바꾸어 준다.

            // BoardObjectFileDao가 상속받는 수퍼클래스를 // AbstractJsonFileDao로 바꿔준다.

            // BoardAddServlet에서 인스턴스와 생성자의 BoardObjectFileDao을 BoardJsonFileDao로 바꾸어 준다.

            // Object로 파일 입출력을 사용하다가 Json으로 바꿨기 때문이다.

실수 조심// Object는 파일 입출력을 ObjectInputStream, ObjectOutputStream 사용했고,

            // Json 파일 입출력을 FileReader와 FileWriter로 사용한다.

            // public void service(ObjectInputStream in, ObjectOutputStream out) // BoardAddServlet service 메서드

            // 위 메서드의 파라미터 ObjectInputStream, ObjectOutputStream는 Client와 연결을 말한다.

            // 파일 입출력을 Object에서 Json으로 바꿨다고, service() 파라미터도 바꿔야지라고 생각하면 안된다.

            // DataLoaderListener로 넘어가서 이제 Object에서 Json으로 파일 입출력 객체를 바꿔준다.

            // BoardJsonFileDao boardDao = new BoardJsonFileDao("./board.json");

v32_10 -> interface 적용

            // data 저장 방식 변경 시마다 새 DAO 클래스를 정의해야 한다. // ser2 -> json 변경시 FileDao 4개 정의

            // 기존이 사용하던 DAO를 새 DAO로 교체해야 한다. // ObjectFileDao -> JsonFileDao

            // 모든 Servlet DAO 연결 변경, DataLoaderListener 변경 필요. // 따라서 interface 문법을 도입한다.

            // 데이터를 저장하고 꺼내는 방식(파일, 클라우드저장소, DB)에 상관없이

            // DAO 사용법을 통일하기위해 인터페이스를 생성한다.

            // public interface BoardDao { // public int insert(Board board) throws Exception; // 항목 선언

            // BoardObjectFileDao, BoardJsonFileDao에 implements BoardDao를 적용한다. // 구현은 이미 했음.

            // BoardAddServlet 등 Servlet 기능들도 BoardDao로 바꿔준다.

            // BoardDao라는 규칙을 통과한 애이라면, 누구던 Servlet기능을 사용할 수 있게끔 해주는 것이다.

            // 필요시 BoardDao 규칙을 따르면 BoardObjectFileDao, BoardJsonFileDao 및 등등으로 아무거나 교체 가능

            // servletMap.put("/board/list", new BoardListServlet((BoardDao) context.get("boardDao"))); // ServerApp

            // (BoardDao) context.get("boardDao")) // BoardDao에게 BoardListServlet기능을 할 List를 달라고 한다.

            // context.put("boardDao", new BoardJsonFileDao("./board.json")); // DataLoaderListener

            // DataLoader가 어떤 파일을 꺼낼지에 따라 경로와, 해당 파일의 Dao를 넣어주면 된다.

v32_11 -> Proxy // 대행객체를 말한다. // 실제 작업 객체의 사용법을 담은 대행 객체를 만든다.

            // Proxy는 Server만들며, 실제 작업이 어떻게 이루어지는 지는 Client쪽에서 알 필요가 없다.

            // 사용법이라고 생각하면 이해가 쉽다. // Client 개발자를 위해 사용법을 넘겨주는 것이다.

            // 따라서 Server에서 실제로 일을 하는 클래스는 따로 있고, Client에 넘기는건 Proxy를 넘긴다.

            // ORB(Object Request Broker) // 객체의 작업 요청을 중재해주는 중개자 // Stub와 Skeleton을 말함

            // Stub // Client측 중개자 // ex) BoardDaoProxy // 얘는 ORB면서 Proxy이다.

            // Skeleton // Server측 중개자 // ex) BoardListServlet // 얘는 ORB이다. Proxy는 아니다.

            // Remote Object // 실제 작업을 수행하는 객체 // ex) BoardJsonFileDao // 얘는 RemoteObject이다.

            // 원격 함수 호출 자동화 기술 // RPC(Remote Procedure Call)

            // 원격 객체 호출 자동화 기술 // ORB 자동 생성

            // 1. RMI // Remote Method Invocation

            // 2. EJB // Enterprise Java Beans

            // 3. CORBA // Common ORB Architecture // 언어 종속성 탈피

            // 4. Web Service // CORBA + web기술 + XML 데이터 포멧

            // 5. RESTful(Representational State Transfer) // Web Service - XML + HTTP 프로토콜 // ex) WSDL 등등

            // OS, 언어 종속성 탈피 // 언어(c, java, python 등)에 상관 없이 ORB를 접근 할 수 있다.

            // Web 기술 기반 구축 // JSON, XML 데이터 포멧 사용

            // 1~4은 ORB를 자동 생성하는 기술이지만, 5는 ORB를 필요로 하지 않는다.

            // 5는 HTTP 프로토콜에 따라 요청 과응답을 수행한다.

            // ClientApp 개발자가 ServerApp 개발자가 통신을 어떻게 주고 받는 지 같이 얘기한다면 굉장히 힘들 것이다.

            // ServerApp 개발자가 알아서 in.readUTF() out.writeUTF() 통신 프로토콜을 정해서 프로그래밍 하고

            // 받아야 하는 정보(사용자로부터 받아야하는 입력 정보 등)만 ClientApp개발자가 프로그래밍하도록 한다.

            // 즉 원래라면 ClientApp에 들어가 있는 Server와의 통신 규칙은 Server 프로그래머가 만들어야 하는게 맞다.

            // BoardDao // 32_10에서 만든 Dao를 Client에 복사한다. // Server와의 통신부분을 떼어내기 위함.

            // BoardObjectFileDao를 가지고 BoardDaoProxy를 만든다. 

            // 프록시 객체는 항상 작업 객체와 동일한 인터페이스를 구현해야 한다.

            // 여기서 작업 객체란 실제 일을 하는 Remote Object를 말한다. // BoardJsonFileDao, BoardObjectFileDao

            // handler에서 서버와 통신을 주고 받는 부분을 BoardDaoProxy에게 넘겨준다. // 역할 분할

            // BoardUpdateCommand에는 update작업할 게시물 번호와 내용을 사용자에게 받고

            // BoardDao(실제로는 BoardDaoProxy)에게 작업을 요청하는 역할만 담당한다. // 사용자로부터 입력만 담당

            // BoardDaoProxy가 이제 Server와 통신하여 데이터를 넘겨주고, 에러를 수신하는 등 송수신을 담당한다.

            // ClientApp에서는 BoardDaoProxy boardDao = new BoardDaoProxy(in, out);를 통해

            // BoardDao에게 일을 시키는게 사실 BoardDaoProxy에게 일을 시키도록 만든다.

            // 32_4와 비교하면, 기존에는 command가 직접 사용자로부터 입력과 서버와의 송수신을 담당했는데,

            // command는 사용자로부터 입력만 proxy는 서버와의 송수신만 담당하게끔 역할을 분리한다.

            // 32_4에서 ClientApp기준으로 command에게 in out을 넘겨주던 것을,

            // 32_11에서는 BoardDaoProxy boardDao = new BoardDaoProxy(in, out); // Proxy 객체에게 in out을 넘기고

            // commandMap.put("/board/list", new BoardListCommand(boardDao)); // command에게는 Proxy를 넘긴다.

            // Proxy는 Server의 Remote Object과의 통신 규칙이기 때문에, ServerApp 개발자가 만든다.

v33_1 -> stateful 통신 방식(기존)을 stateless로 변경

          // stateless 방식의 경우에는 요청/응답을 단 한번만 수행한다.

          // command 요청을 받고, 응답을 하면 연결을 끊어버리는 식으로 바꾸어야 한다.

          // v32_11 Client에서는 service()에서 서버 연결 담당, processCommand()에서 command를 무한히 처리했다.

          // 즉 서버와 연결이 한번 이루어지면 접속이 끊기지 않은 채로 Command를 입력하는 방식이였다.

          // ClientApp을 실행하면 service()에서 processCommand()를 호출한다.

          // service()에서 접속할 서버와 포트를 받고, command를 입력을 미리 받는다. // Queue Stack도 여기서 처리

          // Command()에서 서버와 연결을 하고 command에 대한 처리를 담당하게끔 바꾼다.

          // processCommand()에서 명령을 입력받고, command 검증을 하는 부분을 // service로 넘긴다.

          // service()에서 입력 받은 command를 processCommand() 파라미터로 넘긴다. // processCommand(command)

          // 기존 파라미터였던 in, out을 processCommand() 안에 선언한다. // 통신을 processCommand()가 담당하게 함

          // 기존의 명령 "quit"은 단발성 접속으로 인해 더이상 필요하지 않다.

          // 기존의 명령 "/server/stop"는 CommandMap에 새로 넣어 사용한다.

          // 별도 클래스로 분리하지 않고 익명클래스로 사용한다.

          // 생성자로 commandStack, commandQueue를 생성하게끔 변경한다.

          // ServerApp에서는 request를 처리하던 While문을 제거한다

          // switch문으로 관리하던 quit은 필요 없어졌기 때문에 제거하고, /server/stop만 사용하는 if문으로 바꾼다.

v33_2 -> stateless 단점 개선 // 현재 1번의 요청/응답을 수행하지만, Client가 접속하고 너무 많은 셋팅들을 한다.

           // ClientApp을 실행했을 때, 서버와 포트를 묻고, command 명령어들을 Map에 넣는 작업을 생성자에 맡긴다.

           // 생성자에 셋팅만 미리 해두는 것이라 접속이 필요하지 않다. // 정말 필요할 때 server를 사용하게끔 조치

           // 생성자에서 셋팅을 다 준비해뒀으니, service()에서 command 요청을 미리 받아둔다.

           // processCommand(command)에서는 넘어온 command를 Map에서 꺼내 실행(execute)하는 역할만 담당한다.

           // 실제 서버와 접속하는 역할을 각 BoardDaoProxy에게 맡긴다 // 33_1에서는 in, out을 넘겼다.

           // BoardDaoProxy boardDao = new BoardDaoProxy(host, port); // BoardDaoProxy 접속할 주소만 알려준다.

           // BoardDaoProxy에서 실제 안의 메서드를 실행할 때, in, out을 선언하여 서버와 통신하도록 한다.

v33_3 -> ProxyHelper // 한명이 접속하여 요청 중에 있으면 다음 사람은 접속할 수 없다는 단점을 고친다.

           // 33_2에서 실제 메서드를 실행할 때, in out의 코드가 모두 같다.

           // Worker 인터페이스로 socket in out 코드를 별도로 빼내자

           // Object execute(ObjectInputStream in, ObjectOutputStream out) throws Exception;

           // Worker interface 선언 // Exception은 호출한데서 처리하도록 throws 처리

           // DaoProxyHelper가 작업을 수행할 때 일시적으로 사용하는 도구 // 의존관계

           // UML class diagram // [DapProxyHelper]-------->[Worker]

           // DaoProxyHelper 생성 // BoardDaoProxy가 서버와 통신할 때 필요한 준비 작업을 수행 // 그래서 Helper

           // 서버와 연결한 후 입,출력 스트림을 준비하는 일을 한다. //  파라미터로 넘겨 받은 작업 객체를 실행한다. 

           // DaoProxyHelper와 Worker클래스의 목적과 사용 방법 

           // 33_3의 BoardDaoProxy를 보면 socket, in, out(서버 연결)을 모든 메서드에서 공통적으로 준비한다.

           // 따라서 host와 port를 넘겨, 서버 연결을 준비하고 싶은 목적이 있다.

           // 기존에는 host와 port가 달라지면, BoardDaoProxy 등 3개에 별도 host와 port를 바꿔줘야 한다.

           // DaoProxyHelper를 생성하여 in out을 생성하면, DaoProxyHelper에만 host와 port를 바꿔주면 된다.

           // DaoProxyHelper가 host와 port를 이용해 in out을 리턴한다고 하면 비효율 적이다.

           // DaoProxyHelper를 실행하는 바깥의 메서드에 리턴되는 in과 out을 받을 객체를 만들어야 하기 때문이다.

           // in과 out을 받는 interface를 만들어, DaoProxyHelper가 그 interface를 파라미터로 받는다면,

           // DaoProxyHelper에서  socket, in, out 생성 파라미터로 받은 interface 구현체에게 in, out을 넘긴다면,

           // interface 구현체가 in out을 받아 원하는 command를 서버와 처리하고 데이터를 리턴한다면,

           // in out을 굳이 바깥에 둘 필요가 없다. // in out을 통해 데이터를 받아온다면,

           // add와 delete는 int를 리턴하고, list는 list<Board> 리스트를, detail은 Board를 리턴한다.

           // DaoProxyHelper와 Worker는 리턴값을 Object로 선언하여 모든 리턴값을 넘겨 받는다.

           // BoardDaoProxy에서 값을 리턴할 때, 해당 경우에 맞게 명시적 형변환을 해주어 리턴하면 된다.

           // DaoProxyHelper와 Worker는 어떤 메서드가 호출할 지 모르기 때문에 Object를,

           // BoardDaoProxy는 해당 메서드에서 어떤 데이터를 리턴해야되는지 알기 때문에 명시적 형변환을 한다.

           // 따라서 이 문법의 핵심은 서버통신시에만 쓰이는 in out을 Helper에게 넘겨 Helper가 생성하고

           // Helper가 처리하고싶은 코드를 파라미터로 받아 Helper안에서 해당 기능을 처리하도록 한다.

           // 코드를 파라미터로 넘기는 방법은 없기 때문에, 인터페이스를 선언하고 오버라이딩한 객체를 넘기는 것이다.

           // 해당 기능은 해당 기능이 호출될 때만 별도로 사용되기 때문에, 익명클래스로 선언할 수 있다.

           // 특히나 command 패턴과 같이 execute로 호출되는 패턴의 경우 메서드가 하나라 람다문법이 적용 가능하다.

v34 -> Thread 적용 // v33로 Client에서 속도를 줄인다 하여도, 서버접속은 1명밖에 안되는 단점을 고친다.

         // Thread를 다이렉트로 상속받아도 되지만, Thread의 상위 클래스인 Runnable 인터페이스를 구현한다.

         // processRequest(socket); // Client 별로 processRequest를 실행시켜주면 된다.

         // 따라서 new Thread(() -> { processRequest(socket); }).start(); // 람다문법으로 선언한다.

         // 원래라면 Runnable을 구현하는 class를 만들고, new Thread(new class())로 객체를 만들어 사용한다.

v35 -> Flyweight 디자인 패턴 적용 // Flyweight 디자인 패턴의 응용 기법인 Pooling 기법을 사용한다.

         // Client가 접속할 때마다 Thread를 생성하는게 비효율적이다.

         // Thread는 실행 후 재사용이 불가하기 때문에 가비지가 된다 // 메모리가 낭비된다.

         // Thread를 재사용하게끔 만들자 // Thread는 끝나면 재사용이 불가능하니깐 끝나지 않게 하자.

         // Thread를 생성하고 start()를 호출하여 Running 상태로 만든다. // 사용이 끝난건 Dead 상태

         // wait()이나 sleep()을 사용하여 Thread를 NotRunnable 상태로 만든다.

         // NotRunnable 상태를 notify() notifyall()을 사용해서 Running 상태로 만든다.

         // ThreadPool을 사용할 때에는 직접 Thread를 만들지 않는다.

         // ThreadPool은 Server에서 Client의 접속을 위한 Thread들을 모아놓는 작업이기 때문에 Server에서만 작업한다.

         // ExecutorService executorService = Executors.newCachedThreadPool();

         // Executors.newCachedThreadPool();의 리턴 값이 ExecutorService 객체이다.

         // 또한 ExecutorService는 interface이다. 따라서 new ExecutorService할 수 없다.

         // ExecutorService.submit(new Runnable() {...}); // new Thread(() -> { ... }).start()라고 생각

         // ExecutorService.submit() // 스레드풀에 스레드가 없으면 새로 만들어 Runnable 구현체를 실행한다.

         // 스레드풀에 스레드가 있으면 그 스레드를 이용하여 Runnable 구현체를 실행한다.

         // executorService.shutdown(); // 스레드풀에 소속된 스레드들의 작업이 모두 끝나면 종료하는 뜻이다.

v36_1 -> DBMS 적용 // Oracle MySQL MS-SQL DB2 Altibase Tibero Qbrid // DBMS들

        // MariadDB 사용 // https://mariadb.com // 오른쪽 상단 download 클릭

        // MariaDB PlatFrom 탭에서 OS 확인 후 다운로드. // connector 탭에서 Java8 확인 다운로드

        // MariaDB PlatFrom에서 다운로드 받은거 실행,

        // Modify password 머시기는 비밀번호가 나중에 코드에 나타난다. // 1111등 아주 쉬운걸로 하자.

        // Enable access 머시기 // 원격으로 접속하게끔 하겠냐는건데 체크

        // Use UTF8 머시기 // UTF8로 출력하겠다는거 체크

        // 나머지는 건드리는게 없다. Next 후 Install // 포트번호는 꼭 기억하기.

        // "서비스"라고 윈도우 시작바에 검색해서 실행하고 MariaDB의 상태가 실행중이면 정상적으로 설치 된 것이다.

        // "시스템 환경 변수 편집" 윈도우 시작바에 검색 - 고급 - 환경변수 - 시스템 변수 - path변수 더블클릭

        // 환경 변수 편집 - 새로 만들기 - C:\Program Files\MariaDB 10.4\bin - 맨 위로 이동 // graalvm이 2번째 위치

        // C:\Program Files\MariaDB 10.4\bin은 MariaDB 설치폴더 bin 경로를 말한다. 

        // 적용 후 PowerShell 접속 // 기존에 켜져있었으면 껐다 다시 켜야 함.

        // PowerShell에서의 명령어 // mysql 접속 전

        // $ mysql -u root -p // Enter password: 1111 // MariaDB 설치할 때 입력했던 비밀번호 입력.

        // mysql에서의 명령어 // 데이터베이스 접속 전

        // $ CREATE USER 'study'@'localhost' IDENTIFIED BY '1111'; // 로컬에서 접속 가능한 아이디 생성

        // $ CREATE USER 'study'@'%' IDENTIFIED BY '1111'; // 원격에서만 접속 가능한 아이디 생성

        // $ quit // 접속 해제 

        // $ select user, host from mysql.user; // MySQL 사용자 목록 조회

        // $ CREATE DATABASE studydb CHARACTER SET utf8 COLLATE utf8_general_ci; // MySQL 데이터베이스 생성

        // $ show databases; // 데이터베이스 목록 조회

        // $ use studydb; // 사용할 database 접속

        // 자세한 명령어는 bitcamp-docs - dbms - mariadb-settings 확인하기. 

        // database 안에서의 명령어는 bitcamp-docs - db - project-ddl을 참고하기

        // database에서 게시판 형식 상세보기

        // create table lms_lesson ( // table 생성 // 너무 길어 자세한건 저거 위에 꺼 참고하기 들어가서 보기

        // $ describe lms_board; // $describe [게시판 이름]

        // JDBC API(Java DataBase Connectivity) // DBMS에 상관없이 Proxy 호출 규칙을 위해 만들어짐.

        // MariaDB(DBMS)를 다룰때 SQL문을 사용하는데 MariaDBClient를 사용해서 다룬다. // 프로그램 제공

        // implementation group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '2.5.4' // build.gradle에 추가

        // gradle cleanEclipse // build.gradle은 v28_3 확인하기 // 파워쉘에서 Eclipse 설정파일 지우기

        // gradle eclipse // 설정파일 다시 셋팅

        // java.sql.* // javax.sql.* // JDBC 패키지 // java내에서 패키지로 사용

        // JDBC Driver // JDBC API 규칙에 따라 만들어진 클래스 // proxy(대행자)의 역할을 한다.

        // MariaDB와 다이렉트로 연락을 담당하는 애가 JDBC Driver이다. // proxy 역할

        // JDBC Driver는 따라서 JDBC API 규칙으로 만들어 지는 것이다. // ClientApp에서 JDBC Driver를 호출한다.

        // MariaDB 다운받을때 connector에서 다운 받은 애가 JDBC Driver이다. // Proxy

        // JDBC progamming Framework // Persistence Framework // JDBC Driver들을 호출하는 애들을 이야기 함.

        // 즉 App -> Framework -> Driver -> DB

        // Java App -> MariaDB JDBC Driver -> DB // 아직 Framework는 현재 프로젝트에 적용이 되지 않았다.

        // Java App -> MariaDB JDBC Driver // 과정

        // 1. Driver 구현체 로딩 -> 2. Connection 객체 준비 -> 3. statement 객체 준비 

        // SQL 전달 // insert, update, delete // executeUpdate(SQL) // select // executeQuery(SQL)

        // -> 4. ResultSet 객체 준비 // select 결과를 가져오기 // next(), getXxx()

        // ex) Mybatis Hibernate JPA // Framework들

        // ClinetApp에서 command를 입력하면 해당 command가 Proxy를 호출하고, Proxy가 DBMS와 연락을 담당한다.

        // interface BoardDao를 구현하는 BoardDaoImpl를 생성한다.

        // BoardDao를 구현하는 이유는 편의에 따라 BoardDaoImpl, BoardDaoProxy를 결합할 수도 있기 때문이다.

        // public List findAll() throws Exception // list 출력하는 방법.

        // Class.forName("org.mariadb.jdbc.Driver"); // mariaDB를 사용하였기 때문에, DB에 맞게 바꿔준다.

        // Connection con = DriverManager.getConnection("jdbc:mariadb://localhost:3306/studydb", "study", "1111");

        // JDBC Driver를 이용하여 MariaDB 에 접속한다. // MariaDB와 연결 // Server와 연결

        // Statement stmt = con.createStatement(); // MariaDB에 명령을 전달할 객체 준비

        // ResultSet rs = stmt.executeQuery("select board_id, conts, cdt, vw_cnt from lms_board");

        // MariaDB의 lms_board 테이블에 있는 데이터를 가져올 도구를 준비 // select 객체 from 어디서 가져올건지

        // rs.next() // 다음 rs가 있는지 확인 // board.setNo(rs.getInt("board_id")); // board_id에 해당하는 객체 가져오기

        // 가져올 객체를 가져와서 board에 담는다. // list.add(board); // board를 list에 추가하는 반복문을 사용 후 리턴

        // stmt.executeQuery("select board_id, conts, cdt, vw_cnt from lms_board");

        // stmt.executeQuery("select 컬럼명 from 테이블명");

        // BoardListCommand // 바뀐게 없다. List객체를 받는 방식은 동일하기 때문

        // BoardDetailCommand // BoardListCommand에서 반복문을 돌리지 않고 Board만 리턴하면 된다.

        // public int insert(Board board) throws Exception { // add하는 방법.

        // con.setAutoCommit(true); // add의 경우에 이걸 true로 Auto값을 바꾸어 줘야 한다.

        // 이것은 Oracle을 사용하면서, MariaDB를 사용해서 발생하는 건인데 그냥 그렇다고 이해하자.

        // $ describe lms_board // mariaDb에서 확인해보면 board_id는 auto-increment라서 값을 넣어줄 필요가 없다.

        // cdt는 Default를 보면 현재 시간을 넣음을 알 수 있다. vw_cnt는 새로 add하는 과정에서는 0이여야 한다.

        // 따라서 board_id conts cdt vw_cnt 중에서 실제 값을 입력하여 넣어줘야 하는 것은 cdt하나밖에 없다.

        // stmt.executeUpdate("insert into lms_board(conts) values('" + board.getTitle() + "')");

        // stmt.executeUpdate("insert into 테이블명(컬럼명1, 컬럼명2,...) values(값1, 값2, ...");

        // BoardAddCommand // 바뀐게 없다. 값은 다 받아야 하기 때문

        // public int update(Board board) throws Exception { // update하는 방법.

        // no, title, date, viewCount 중에서 값이 업데이트 되는 건 title 밖에 없다. 따라서 title만 업데이트를 시킨다.

      // stmt.executeUpdate("update lms_board set conts='"+ board.getTitle() + "' where board_id=" + board.getNo());

        // stmt.executeUpdate("update 테이블명 set 컬럼명=값1 컴럼명=값2 where 조건);

        // BoardUpdateCommand // 값에서 정말 업데이트 되어야 하는 값만 받게끔 고친다.

        // public int delete(int no) throws Exception { // delete하는 방법

        // stmt.executeUpdate("delete from lms_board where board_id=" + no);

        // stmt.executeUpdate("delete from 테이블명 where 조건);

        // BoardDeleteCommand // boardDao.delete(no) 리턴값으로 삭제 여부를 판단한다.

        // ClientApp에서 서버와 포트를 받는 코드를 지운다. // ServerApp와 연결을 할 필요가 없기 때문이다.

        // 이미 위에서 MariaDB에 접속하는 코드를 각 Command에 넣어두었기 때문에 ServerApp은 더이상 쓰지 않는다.

        // 원래 ServerApp의 목표는 데이터를 보관하고 해당 Command에 맞는 데이터를 꺼내주기 위함이였다.

        // 기능을 이제 MariaDB가 대신하기 때문에 더이상 ServerApp이 필요가 없고 서버ip와 포트를 받을 필요가 없다.

v36_2 -> Connection 기능 추출 // 모든 Command에 공통적으로 적용되는 코드를 만들어서 넘긴다.

        // Connection con; // 미리 ClientApp에서 Connection 기능을 별도로 추출한다.

        // Class.forName("org.mariadb.jdbc.Driver"); // Driver를 불러오고

        // con = DriverManager.getConnection("jdbc:mariadb://localhost:3306/studydb", "study", "1111"); // 객체 입력

        // BoardDao boardDao = new BoardDaoImpl(con); // BoardDaoImpl에 생성자로 넘겨준다.

        // BoardDaoImpl에서는 파라미터로 넘어온 con을 받아서 사용한다.

        // 이렇게 하는 이유? // BoardDaoImpl 내부적으로 상세 메서드마다 con을 생성해서 처리할 수도 있고

        // 굳이 ClientApp에서 받지 않고, BoardDaoImpl에서 생성자를 만들어 사용할 수도 있다. // 틀린 방법이 아니다.

        // Command에서 con을 만들어 처리한다면, Command를 입력할 때 마다, 별도의 con이 만들어진다.

        // ClientApp에서 만들어 con을 넘긴다면 하나의 con으로 모든 Command가 이용할 수 있으며,

        // 코드가 더 간단해지고 직관적으로 바뀌게 되기 때문이다.

v37_1 -> Application Server 구조로 변경 // Stateless 방식

        // Application Server(AS)  // Server에서 application 작업을 처리

        // Web Application Sever(WAS) // AS기능을 Web에 적용시킨 것이다.

        // 기능 변경을 하더라도 ClientApp을 다시 설치할 필요가 없다. // ClientApp은 기능 호출만 담당하기 때문

        // ClientApp에서 ServerApp에 명령어를 호출하고, ServerApp에서 맞는 Servlet이 응답하여 DBMS에 접속한다.

        // Client에게 bitcamp://localhost:9999/board/list와 같이 접속할 포트와 명령어까지 한번에 입력받는다.

        // ClientApp()에서 실행하던 기능들을 전부 지운다. CommandQueue와 CommandStack은 제외

        // processCommand(String command)에 접속할 포트와 명령어를 입력받기 위한 변수를 준비한다.

        // 입력받은 값에서 서버, 포트와 명령어를 구분하여 처리한다. // startsWith(), substring(), split() 사용

        // 서버와 포트, 명령어를 구분해 두었다면 이제 연결소켓을 준비한다.

        // 데이터를 한 줄씩 보내고 읽기 위해서 PrintStream out와 Scanner in을 준비한다.

        // Server에서 !end!라고 보내기 전까지는 명령어(servletPath)를 무한정 처리하게 반복문 처리한다.

        // ServerApp에서는 processRequest() 메서드 말고는 건드리지 않는다.

        // v37_1단계에서는 단지, ClientApp에서 보낸 request를 출력하고 간단한 인삿말을 전송한다 // 연결 여부 확인용

v37_2 -> Application Server 구조로 변경 // Servlet + DAO 적용

        // mvnmvnrepository.com 또는 search.maven.org 접속 - 'mariadb jdbc client' 검색

        // - 라이브러리 정보를 dependencies {} 블록에 추가 - 'gradle cleanEclipse' - 'gradle eclipse'

        // Client에서 Server로 해당 커멘드 작업을 가져오는 것이기 때문에, DAO 관련 클래스를 가져온다.

        // DataLoaderListener에게 MariaDB 연결 하도록 한다. // 연결권을 BoradBoardDaoImpl에게 넘겨준다.

        // contextDestroyed()에서 con.close()를 하도록 한다. // 사용이 끝났으면 닫아줘야 하기 때문

        // default void service(Scanner in, PrintStream out) throws Exception {} // Servlet 인터페이스에 추가

        // default void service(ObjectInputStream in, ObjectOutputStream out) throws Exception // 기존 코드 변경

        // Servlet의 메서드에 default를 붙히면, 이미 구현이 되어있는 메서드이기 때문에 구현을 강제하지 않는다.

        // default를 붙히지 않으면, 남은 메서드를 구현하지 않아 실행 자체가 불가능하다.

        // default를 붙히면, 둘 다 구현하지 않았도 Servlet을 구현한 Class 행세를 할 수 있다는 단점이 있다.

        // public void service(Scanner in, PrintStream out) throws Exception { // 한 줄씩 주고 받기로 하였다.

        // 따라서 ScannerPrintStream 를 사용하여 데이터를 주고 받는다.

        // 메서드의 내용은 ClinetApp에서 사용하던 Command를 참고해서 고친다. // Server의 Servlet을 바꾸는거 맞음.

        // v37_1단계에서는 Client 수정 했기 때문에 그에 맞는 Server 응답을 해주어야 한다.

        // v37_1단계에서 단지 연결 여부를 확인하기 위해 기존 코드 주석 처리 해줬던 것을 해제한다.

        // out.println("!end!"); // 바뀐 프로토콜에 맞게 코드를 수정&추가해준다.

v37_3 -> Application Server 구조로 변경 // 사용자에게 응답을 요청하는 통신규칙 추가 // !{}!

        // ClientApp 에서 !{}!에 대한 통신 규칙 추가 // ClientApp에서 어떠한 command도 처리하지 않는다.

        // ClientApp, Prompt를 제외한 나머지 모조리 삭제

        // Server() Servlet 부분에 Client와의 통신 규칙을 추가하여 수정한다.

v37_4 -> Application Server 구조로 변경 테스트 // MemberSearchServlet 기능 추가시, ClientApp 변경 불필요 확인

v37_5 -> 스레드 DB 종료 문제 // 한 스레드에서 quit으로 작업을 끝내면 나머지 스레드에서 DB작업이 같이 종료된다.

        // executorService.isTerminated() // 스레드가 끝났는지 검사한다.

        // 검사를 한번하고 종료할 수 있으니, 반복문 안에 둔다.

        // Thread.sleep(500); // 반복문에서 계속 실행 될 수 있으니 슬립을 통해 시간의 텀을 둔다.

       // boolean serverStop = false; // ServerApp에 quit시 다음 클라이언트 요청을 받지 않고 종료하기 위한 변수 선언

        // serverStop = true; // quit 메서드 호출 시 serverStop true값으로 변경되게 코드 추가

        // if (serverStop) { break; } // ServerApp service에서 serverStop이 true일때 요청을 더 받지 않고 종료하게끔 설정

v38_1 -> 트랜잭션 적용 전 // PhotoBoard 게시판 추가하기

        // PhotoBoard // domain 패키지 아래 사진 게시물의 데이터 타입을 정의할 클래스 생성

        // PhotoBoardDao // dao 패키지 아래 사진 게시물의 CRUD 관련 메서드 호출 규칙을 정의

        // PhotoBoardDaoImpl // PhotoBoardDao 인터페이스를 구현한다. 

        // DataloaderListener // PhotoBoardDao 객체를 추가한다.

        // mariaDB를 사용하므로, CRUD 모두 SQL문을 사용하여야 한다. // findByNo

        // Statement stmt = con.createStatement() // MariaDB에 명령을 전달할 객체 준비

ResultSet rs = stmt.executeQuery( "select" + " p.photo_id," + " p.titl," + " p.cdt," + " p.vw_cnt," +

 " l.lesson_id," + " l.titl lesson_title" + " from lms_photo p" + " inner join lms_lesson l on p.lesson_id=l.lesson_id"

 + " where photo_id=" + no) 

        // PhotoBoard 데이터 타입 외, Lesson 데이터 타입은 조인으로 해결 // SQL문 사용하여 CRUD 구현

        // PhotoBoard photoBoard = new PhotoBoard(); // PhotoBoard 데이터 받을 객체 준비

        // photoBoard.setNo(rs.getInt("photo_id")); // 해당 데이터 맞는 컬럼에 넣기 

        // Lesson lesson = new Lesson(); // Lesson 데이터 받을 객체 준비 // 생성자로 받거나 하는거 아님.

        // Lesson의 경우에 데이터를 받을 빈 객체를 생성하여 저기에다 넣고, photoBoard 객체에 넣는다. // 밑에 서술

        // lesson.setNo(rs.getInt("lesson_id")); // Lesson 데이터에 맞는 컬럼에 넣기.

        // photoBoard.setLesson(lesson); // PhotoBoard에 Lesson 객체를 생성하여 받는다.

        // return photoBoard; // photoBoard에 lesson 객체를 담아서 리턴한다.

        // Lesson lesson; // domain에 있는 PhotoBoard에 Lesson 객체를 생성하여 받는다. // 위 내용 상세

v38_2 -> 트랜잭션 적용 전 // PhotoFile 생성 // 사진 첨부 파일 데이터를 저장하는 클래스

        // PhotoFile // domain 패키지 아래 사진 게시물의 데이터 타입을 정의할 클래스 생성

        // PhotoFileDao // dao 패키지 아래 사진 게시물의 CRUD 관련 메서드 호출 규칙을 정의

        // PhotoFileDaoImpl // PhotoFileDao 인터페이스를 구현한다. 

        // DataloaderListener // PhotoFileDao 객체를 추가한다.

        // PhotoBoardAddServlet // 수정

        // Lesson lesson = lessonDao.findByNo(lessonNo); // lessonNo를 입력받고, 해당 lessonNo가 유효한지 확인

        // LessonDao lessonDao; // PhotoBoard는 Lesson의 사진을 찍어 보관하기 때문에, 

        // 입력받은 lessonNo가 유효한지 확인하려면, LessonDao를 사용하여야 한다.

        // ArrayList<PhotoFile> photoFiles = new ArrayList<>(); // photoBoard에 photoFile이 들어가므로 List를 만든다.

        // String filepath = in.nextLine(); // photoFile path를 받고 유효한지 확인한다.

        // PhotoFile photoFile = new PhotoFile(); // PhotoFile 객체 생성

        // photoFile.setFilepath(filepath);  photoFile.setBoardNo(photoBoard.getNo()); // 값 입력

        // photoFiles.add(photoFile); // ArrayList photoFiles에 photoFile 데이터 추가.

        // for (PhotoFile photoFile : photoFiles) { photoFileDao.insert(photoFile); } // 추가한 파일 목록을 보여준다.

        // PhotoFileDao photoFileDao; // photoFile을 insert하려면 photoFileDao가 필요하기 때문에 위에 선언한다.

        // 위에 선언한 LessonDao와 PhotoFileDao를 생성자로 전달 받아 사용한다.

        // servletMap.put("/photoboard/list", new PhotoBoardListServlet( photoBoardDao, lessonDao));

        // ServerApp sevletMap에 추가한다. // add와 detail update delete 기능을 만들어 추가한다.

        // PhotoFile 객체를 생성하고, 일일히 값을 집어넣는 것은 굉장히 비효율 적이다.

        // public PhotoFile(String filepath, int boardNo) // PhotoFile에 filepath와 boardNo를 받는 생성자 추가

        // photoFiles.add(new PhotoFile(filepath, photoBoard.getNo())); // 생성자를 통한 값 입력

        // photoFiles.add(new PhotoFile().setFilepath(filepath).setBoardNo(photoBoard.getNo()));

        // 체인 방식을 이용하여 입력하는 방법도 있다.

        // new PhotoFile()에 대해 setFilepath(filepath) - new PhotoFile()에 대해 setBoardNo(photoBoard.getNo())

        // public PhotoFile(int no, String filepath, int boardNo) { this(filepath, boardNo); this.no = no; } 

        // 추가 no를 받는 생성자에 PhotoFile(String filepath, int boardNo)를 호출하는 this(filepath, boardNo)를 추가

v38_3 -> 트랜잭션 적용 전 // Server Prompt 생성

        // Client에게 입력을 요청하는 부분의 코드가 중복되기 때문에 리펙토링 한다.

        // util 패키지를 만든다. // prompt를 생성한다. 

        // public static int getInt(Scanner in, PrintStream out, String title) // Client 통신할 in, out와 출력할 title 받는다

        // Client에게 입력을 요청하는 servlet 부분을 Prompt를 적용해 바꾼다. // 리펙토링

        // PhotoBoardAddServlet // 첨부파일 입력을 별도의 메서드로 분리한다. // 리펙토링

        // private List<PhotoFile> inputPhotoFiles(Scanner in, PrintStream out) // 리텍토링

        // photoFiles.add(new PhotoFile().setFilepath(filepath)); // photoBoard.getNo()는 photoFile에서 처리할 수 없다.

        // photoFile.setBoardNo(photoBoard.getNo()); // inputPhotoFiles 적용 후, 나와서 처리한다.

        // List<PhotoFile> photoFiles = inputPhotoFiles(in, out); // inputPhotoFiles에서 작업한 photoFiles를 받는다.

// for (PhotoFile photoFile : photoFiles) { photoFile.setBoardNo(photoBoard.getNo()); photoFileDao.insert(photoFile); }

        // photoFiles에는 photoBoard.getNo()가 되어있는 상태가 아니기 때문에 반복문으로 값을 추가적으로 넣는다.

        // PhotoBoardUpdateServlet // 첨부파일 목록을 출력하는 부분을 별도의 메서드로 분리한다. // 리펙토링

        // private void printPhotoFiles(PrintStream out, int boardNo) throws Exception // 별도 분리

        // private List inputPhotoFiles(Scanner in, PrintStream out) // 첨부파일 입력 부분을 별도의 메서드로 분리한다.

        // add와 같이, photoBoard.getNo() 관련 작업은 PhotoBoardUpdateServlet에서 처리한다.

v38_4 -> 트랜잭션 적용 후 // 사진 게시글 입력과 첨부 파일 입력을 한 단위로 다루기

        // 트랜잭션 // 여러개의 데이터 변경 작업을 한 단위로 묶은 것.

        // PhotoBoard 게시물 추가와, PhotoFile 사진 파일 첨부 관련한 작업을 묶어서 작업하기 위함.

        // 트랜잭션을 사용해야 하는 이유

        // 첨부 파일이 DB 컬럼에서 허용된 길이 보다 더 긴 값을 갖을 때 (ex 첨부파일 1, 2는 정상 3은 오류)

        // 파일 첨부에서 오류가 발생했음에도, 사진 게시글이 정상적으로 입력된다. // Autocommit으로 처리하기 때문

        // 또한 3번의 첨부파일로 오류가 발생했음에도 1번파일과 2번파일이 업로드가 되어버린다.

        // 비정상적인 게시물인데 업로드가 되기 때문. 

        // 추가 PhotoBoardUpdateServlet의 경우를 보면, Update시 먼저 Board를 업데이트 후, PhotoFile을 추가한다. 

        // PhotoFile에서 오류가 발생하여도 Board의 내용은 이미 업데이트가 된 상태이다.

        // Client가 만약 파일 첨부가 제대로 되지 않았다면 Board의 내용을 업데이트 하지 않을 수도 있는데,

        // 파일 첨부가 제대로 되지 않았더라도, Board가 업데이트 되어버리는 단점을 가지고 있는 상태이기 때문에

        // 트랜잭션으로 Board 수정과 파일첨부를 묶어야 하는 이유이다.

        // DataLoaderListener.con.setAutoCommit(false); // 트랜잭션을 시작하기 위해 auto-commit을 수동으로 바꾼다.

        // 이 이후부터 데이터 변경 작업은 모두 임시 테이블에 보관된다.

        // DataLoaderListener.con.commit(); // commit 명령으로 DBMS에 보낼 때만 진짜 테이블에 적용된다.

        // DataLoaderListener.con.rollback(); // 오류 발생시 rollback()으로 진짜 데이터의 상태로 돌아갈 수 있다.

        // DataLoaderListener.con.setAutoCommit(true); // 모든 작업이 끝난 후 원래대로 AutoCommit을 true로 바꾼다.

        // DataLoaderListener의 con을 public static으로 변경해주어야 사용이 가능하다.

v39_1 -> Connection 개별화하기 // Connection(DB연결)을 하나로 사용함에 따라 발생하는 문제를 해결하기 위함이다.

        // 서버에서 나가는 ip는 동일하고, 그 안에서 ip가 나누어지는 상태일 때 (사용자는 다르나, Client 같은 ip로 인식)

        // 하나의 Client가 접속한 상태에서 데이터를 잘못 입력한 상태일때 (임시 table에 저장된 상태)

        // 또 다른 Client가 접속하여 데이터를 입력하고 commit을 하게 되면,

        // 임시 table에 저장된 데이터도 commit이 된다. // commit이 되어 rollback이 불가능하다.

        // Dao에게 insert findAll findByNo등 실제 DB작업 할때 DB와 연결하도록 BoardDaoImpl에게 주소를 넘긴다.

        // 기존에는 MariaDB연결을 DataLoaderListener에서 담당하고, Connection을 각 DaoImpl에게 전달하였다.

        // context.put("boardDao", new BoardDaoImpl(jdbcUrl, username, password)); // DaoImpl에게 연결 주소를 준다.

        // public BoardDaoImpl(String jdbcUrl, String username, String password) // DaoImpl의 생성자를 변경한다.

        // Connection con = DriverManager.getConnection(jdbcUrl, username, password)

        // DaoImpl안의 기능에서 Connection을 생성하여 처리한다.

v39_2 -> Connection 개별화하기 // Factory Method 패턴 적용

        // Connection 객체는 DriverManager를 통해 생성하지만, 생성 방법이 바뀔 수 있다.

        // Connection 객체 생성 방법이 바뀌면, DAO 구현체를 모두 변경해야 한다.

        // DB연결 Connection의 방법이 바뀔 수도 있기 때문에 별도의 Class에서 Connection을 생성한다.

        // public class ConnectionFactory // Connection 객체 생성을 담당할 Class 생성

        // public ConnectionFactory(String jdbcUrl, String username, String password) // 생성자로 연결 재료를 받는다.

        // public Connection getConnection() throws Exception {

        // return DriverManager.getConnection(jdbcUrl, username, password); } // 호출 시 Connection 리턴

        // DataLoaderListener에서 Dao에게 전달하는게 아닌, conFactory에게 전달한다.

        // ConnectionFactory conFactory = new ConnectionFactory(jdbcUrl, username, password); 

        // context.put("boardDao", new BoardDaoImpl(conFactory)); // 생성한 con을 Dao에게 전달한다.

        // public BoardDaoImpl(ConnectionFactory conFactory) // Dao는 생성자로 ConnectionFactory를 받는다.

        // Connection con = conFactory.getConnection(); // 연결시에는 conFactory에게 Connection을 받아 사용한다.

v40_1 -> Connection을 스레드에 보관 // ThreadLocal을 사용하여 스레드에 값 보관하기

        // Connection을 Dao마다 가지고 있으면, 트랜잭션을 사용할 수 없다는 단점이 있다.

        // 그러나 DB연결 Connection을 각 Dao에서 가지고 있어, 묶어 처리를 할 수 가 없다.

        // 여러 개의 데이터 변경(insert/update/delete) 작업을 한 단위로 묶으려면 같은 Connection을 사용해야 한다.

        // commit()/rollback()은 커넥션 객체에 대해 실행하기 때문이다. // 트랜잭션은 각 커넥션 별로 관리된다.

        // 스레드가 실행하는 데이터 변경 작업을 한 단위로 묶으려면

        // 그 스레드가 수행하는 데이터 변경 작업은 같은 커넥션으로 실행해야 한다.

        // DAO의 메서드가 실행될 때 사용하는 커넥션은 스레드에서 꺼낸다. // Connection을 Thread에 보관한다.

        // ConnectionFactory // ThreadLocal connectionLocal = new ThreadLocal<>();

        // 스레드에 값을 보관하는 일을 할 도구 ThreadLocal 준비.

        // Connection con = connectionLocal.get(); // Connection을 생성하기 전에, Connection이 들어있나 확인한다.

        // 있다면 return con; // 없다면 만들고, connectionLocal.set(con); connectionLocal 에 담은 후 return con;

        // 현재 스레드풀(ExecutorService)을 이용하여 스레드를 관리하고 있다.

        // 스레드를 사용한 후에 스레드를 버리지 않고 풀에 보관했다가 다음 클라이언트 요청에 재사용한다.

        // DAO가 사용하는 Connection 객체는 스레드에 보관한다.

        // DAO의 메서드(예: findAll(), insert() 등)에서 Connection을 사용한 후에 닫는다.

        // 따라서 스레드에 보관한 Connection은 DAO 작업이 끝난 후 닫힌 상태가 된다.

        // 다음 스레드를 재사용할 때 Connection은 닫힌 상태이기 때문에 DAO가 작업할 때 오류가 발생한다.

        // 클라이언트에게 응답을 완료한 후에 스레드에 보관된 Connection 객체를 제거한다.

        // 스레드가 재사용돼도 Connection 객체가 없기 때문에 ConnectionFactory는 새 Connection을 만들어 리턴한다.

        // public void removeConnection() // Connection을 삭제하는 메서드 추가

        // Connection con = connectionLocal.get(); // 스레드에 보관된 Connection 객체를 제거한다.

        // connectionLocal.remove(); // Connection이 있다면 삭제한다.

        // connectionLocal.get() // 이 메서드는 자동으로 자신을 실행시킨 쓰레드를 알 수 있다.

        // 따라서 자신을 실행시킨 쓰레드의 connectionLocal.remove()를 실행하여 con을 제거하는 것이다.

     // context.put("connectionFactory", conFactory); // DataLoaderListener에서 context에 connectionFactory를 담는다.

        // ConnectionFactory conFactory = (ConnectionFactory) context.get("connectionFactory"); // ServerApp

        // conFactory.removeConnection(); // executorService.submit 안에 사용된 Connection은 제거되게 코드를 추가

v40_2 -> Connection을 스레드에 보관 // Proxy 설계 기법(design pattern) 사용 Connection의 close()를 커스터마이징

       // Thread의 갯수를 제한해두어야 하는 이유 // Thread를 생성하면 Stack 메모리에 생성이 되는데 

       // Thread가 추가 될 때마다 JVM Stack 메모리가 기본크기로 생성이 된다.

       // 따라서 추가 될 때마다 메모리를 많이 차지한다. 

       // 이유를 얘기할 때 추가될 때마다 메모리를 많이 차지해서이다.라고 단순히 말하면 안된다. // 틀린건 아님

       // Thread가 생성 될 때 JVM Stack 메모리가 기본 크기로 생성되기 때문에 메모리를 많이 차지해서이다.라고 하자.

       // 가장 바쁜 시간에 Thread가 몇개까지 차오르는 지 등 지표를 보고 Thread의 제한 갯수를 판단한다. // 실무에서.

       // Connection을 사용 후 꼭 닫아야 하는 이유 // Thread에서 Connection을 사용하고 닫지 않으면,

       // Connection은 Thread에 연결된 상태로 주소를 잃어버린 상태가 된다.

       // Thread는 Connection이 연결되어 있기 때문에, 추가적인 Connection을 받지 않는다. // 추가 Thread를 생성

       // Thread를 생성하다보면 Stack메모리가 부족해져서 StackOverFlow 오류가 발생한다.

       // 추가적인 Connection을 받을 수 없는 상태가 된다.

       // 단, DBMS에서 연결이 오랫동안 되지 않는 Connection은 자체적으로 Connection을 끊어버린다.

       // 그 시간을 지정하는 등 DBMS에 대한 작업은 실제 엔지니어(DBA = Database Administrator)들이 한다.

       // 여기서 발생하는 문제는, 언제는 정상적으로 연결이 되고, 언제는 정상적으로 연결이 되지 않는다.

       // 단지 사용자가 많아 Server가 감당할 수 있는 수준을 벗어난 오류가 아니다.

       // 정상적이라면 Server가 수용할 수 있는 연결량인데, 사용 후 Connection을 제대로 닫지 않아 생기는 문제이다.

        // 한 스레드에서 Connection 을 여러 번 사용할 때 문제 발생 // "/photoboard/list" 명령을 실행시

        // LessonDao.findByNo()가 실행된 후에 커넥션이 닫히기 때문에, // LessonDao에서 Connection 사용 후 닫음

        // PhotoBoardDao.findAllByLessonNo()를 실행하면 오류가 발생한다. // PhotoBoardDao에서 사용 불가능.

        // try문이 이용돼서 자동으로 close를 호출하기 때문에 닫힌다.

        // 같은 스레드에서 커넥션을 여러 번 사용할 경우에 오류가 발생한다.

        // 해결 방법은 Connection을 사용하고 난 후에 닫지 않게 한다.

        // 방법 1. try-with-resources 블럭 밖에 Connection 변수를 둔다.

        // 기존의 표준 코딩 방식에 어긋난다. // 자원을 사용했으면 닫도록 코딩하는 게 일반적이다.

        // 스레드에서 커넥션을 여러 번 사용하는 경우라고 해서 특별하게 코딩한다면 유지보수 하기가 어렵다.

        // 방법 2. Connection 객체를 커스터마이징 한다.

        // close()가 호출될 때 진짜로 닫지는 않는다. // 기존 표준 코딩에 어긋나지 않는다.

        // 자원을 사용했으면 닫도록 코딩하는 규칙을 준수하고 있다. // 다만 중간에서 '조작질'을 할 뿐이다.

        // public class ConnectionProxy implements Connection // sql 패키지 생성 - ConnectionProxy 생성

        // Connection interface를 구현하는 클래스를 생성한다. // Connection은 구현해야되는 Class가 많다.

        // public<T> T unwrap(Class<T> iface) throws SQLException { return origin.unwrap(iface); }

        // return이 필요한 메서드의 경우 origin.upwrap(기존파라미터)를 이용해서 구현한다.

        // 이클립스에서 해당 기능을 제공한다. // 자동으로 만들어주는

       // 마우스 오른쪽 클릭 - source - Generate Delegate Methods 클릭 // 구현 필요한 메서드를 체크 후 Generate.

       // close()를 오버라이딩 한다. // close()가 호출돼도 아무 일이 일어나지 않게 하기 위해 내용을 넣지 않는다.

       // public void realClose() throws SQLException { origin.close(); } // 실제 close를 담당할 realClose()를 추가한다.

       // ConnectionFactory에서 ConnectionProxy를 생산하게끔 한다 // 오버라이딩 한 close()를 사용하게 하기 위함.

       // con = new ConnectionProxy(DriverManager.getConnection(jdbcUrl, username, password));

       // ServerApp에 realClose() 기능을 추가해야 한다. // 실제 Client의 작업이 끝났을 때 connection을 닫도록 한다.

       // ConnectionProxy con = (ConnectionProxy) conFactory.removeConnection(); // con.realClose();

       // Dao에서 Connection을 사용 후 닫는 것은 try문 때문에 벗어나면서 Close() 호출하여 어쩔 수 없는 부분이였다.

       // Connection을 다이렉트로 사용하지 않고, Close()를 오버라이딩한 Proxy를 거쳐 Close()를 호출하게 한다.

       // 오버라이딩 된 Close()는 아무런 일을 하지 않으니 실제로 Connection은 계속 사용이 가능한 상태이다.

       // 따라서 다른 Dao에서도 사용이 가능하다. // 사용후 Thread에 반납하는 식 // Client의 연결이 끝나고,

       // ServerApp에서 realClose()를 호출하여 실제 Connection을 끊도록 한다.

v40_3 -> Connection을 스레드에 보관 // 트랜잭션 적용하기

        // 원래는 Dao에서 Connection을 별도로 가지고 있었다.

        // 별도로 Dao가 Connection을 가지고 있으면 트랜잭션을 사용할 수 없다. 

        // 따라서, PhotoBoardAddServlet가 PhotoBoardDao를 호출하고, PhotFileDao를 호출하기 때문에

        // PhotoBoardAddServlet에서 ConnectionFactory에서 생성한 Connection을 받아,

        // ConnectionFactory의 생성 이유 // Connection의 생성 방법이 달라 질 수 있어, 별도의 Class에서 생성을 담당.

        // 호출할때 Connection을 같이 넘겨준다면, PhotoBoardDao와 PhotFileDao는 같은 Connection을 사용한다.

       // PhotoBoardAddServlet, PhotoBoardUpdateServlet, PhotoBoardDeleteServlet // 트랜잭션을 적용한다.

       // 생성자를 통해 ConnectionFactory conFactory를 받는다.

       // Connection con = conFactory.getConnection(); // 넘겨받은 conFactory를 통해 Connection을 전달 받는다.

       // con.setAutoCommit(false); // 트랜잭션을 사용하기 위해 AutoCommit을 false로 바꾼다.

       // con.commit(); // con.rollback(); // 처리 여부에 따라 commit rollback하도록 적용한다.

       // con.setAutoCommit(true); // 작업이 끝난 후 다시 AutoCommit을 true로 돌려놓는다.

v40_4 -> 트랜잭션 제어 코드 캡슐화 // 

       // PlatformTransactionManager // sql 패키지에 트랜잭션 제어 코드를 관리하는 Class 추가

       // public PlatformTransactionManager(ConnectionFactory conFactory) // 생성자로 ConnectionFactory 받는다.

       // public void beginTransaction() throws Exception { Connection con = conFactory.getConnection();

       // con.setAutoCommit(false); }

       // conFactory.getConnection을 이용해 현재 사용하는 커넥션을 꺼내고, 트랜잭션을 할 것이니, AutoCommit false.

       // public void commit() throws Exception { Connection con = conFactory.getConnection();

       // con.commit(); con.setAutoCommit(true); }

       // commit을 하여 사용을 다 완료 했기 때문에, AutoCommit을 true로 바꿔준다.

       // rollback() // 메서드도 Commit()과 같게 추가.

       // PhotoBoardAddServlet, PhotoBoardDeleteServlet, PhotoBoardUpdateServlet

       // 생성자에 ConnectionFactory대신 PlatformTransactionManager를 넘겨 받는다.

       // txManager.beginTransaction(); // txManager.commit(); // txManager.rollback(); // 직관적으로 코드 변경

       // DataLoaderListener // PlatformTransactionManager 객체 준비

       // PlatformTransactionManager txmanager = new PlatformTransactionManager(conFactory);

       // context.put("transactionManager", txmanager);

       // ServerApp // PlatformTransactionManager 객체를 Servlet에 전달

       // PlatformTransactionManager txManager = (PlatformTransactionManager) context.get("transactionManager");

 // servletMap.put("/photoboard/update", new PhotoBoardUpdateServlet(txManager, photoBoardDao, photoFileDao));

       // txManager를 생성자에 넣는다.

v41_1 -> Connection Pool 도입하기 // 

       // 현재 클라이언트 요청을 처리할 때마다, DBMS에 연결하고 작업이 끝나면 DBMS에 연결을 끊는다.

       // 연결하는데 시간이 소요된다. // 소켓 연결, 사용자 인증(Authentication), 사용권한 인증(Authorization)

       // 사용자 인증 (Authentication) // ex. Log-in 과정 // 사용권한 인증(Authorization) // ex. 카페 등급제한 

       // DBMS에 연결할 때마다, 위와 같은 과정을 계속 거쳐서 시간이 너무 많이 소요가 되는 단점이 있다.

       // 또한 DBMS 작업이 끝나면 사용했던 Connection은 Garbage 객체가 된다. // Garbage가 대량으로 생성된다.

       // Garbage가 대량으로 생성이 된다는 것은 메모리가 효율적이지 않다. // 메모리 낭비

       // ConnectionFactory에서, Connection 객체를 준비하고, Connection을 재사용하게 한다.

       // Connection을 재사용한다? // 생성한 객체는 사용 후 Close()하지 않는다. // 재 사용을 위해 보관해둔다.

       // 여러 DAO가 동시에 사용할 수 있도록 일정한 갯수의 Connection을 보유한다. // Thread에 적용한 그거 적용

       // Flyweight 디자인 패턴 적용 // Flyweight 디자인 패턴의 응용 기법인 Pooling 기법을 사용한다. 

       // Connection을 재사용한다. // 연결 소요시간을 줄이고 garbage 생성을 줄인다. // 메모리 낭비를 줄인다.

       // DataSource // util 패키지에 ConnectionFactory를 sql패키지로 가져와 DataSource로 이름을 변경한다.

       // JDBC API에서는 connection 객체를 생성하고 관리하는 역할자를 DataSource로 정의해두었다.

       // getConnection() // ArrayList<Connection> conList = new ArrayList<>(); // 커넥션을 관리할 ArrayList를 생성

       // if (conList.size() > 0) { con = conList.remove(0); }

       // conList에 con이 보관되어 있다면 사용할 것이니 conList에서 지우고 con에 담는다. // remove의 리턴값은 0번째

       // removeConnection() // conList.add(con); // 사용이 완료된 con을 다시 conList에 담는다.

       // public void clean() { while (conList.size() > 0) { try {

       // ((ConnectionProxy) conList.remove(0)).realClose(); } catch (Exception e) { } } } 

       // clean()은 모든 conList의 connection을 제거하고 진짜 닫는다.

       // (ConnectionProxy) conList.remove(0)).realClose(); 해석 // List에서 con을 지우고 remove의 리턴값 활용

       // return된 con이 (ConnectionProxy)의 con이라고 명시적 형변환 후, realClose()를 통해 실제로 닫음.

       // PlatformTransactionManager // 생성자의 변수명을 dataSource로 변경한다.

       // BoardDaoImpl // DaoImpl들 모두 변수명을 dataSource로 변경한다.

       // DataLoaderListener // 변수명을 dataSource로 변경한다. // context.put도 고친다.

       // contextDestroyed 사용 // 애플리케이션이이 종료될 때 모든 DB 커넥션을 명시적으로 끊어준다. 

       // DBMS는 timeout 될 때까지 기다릴 필요 없이, 클라이언트와 연결된 스레드를 즉시 해제 시킬 수 있다.

       // DataSource dataSource = (DataSource) context.get("dataSource"); // dataSource.clean(); // 꺼내서 clean처리.

       // ServerApp // 변수명을 dataSource로 변경한다. // context.get도 고친다.

       // dataSource.removeConnection(); // realClose부분을 변경한다.

v41_2 -> 트랜잭션 관리자를 사용하는 코드 캡슐화 // 

       // 41_2처럼 트랜잭션의 코드를 감추기 위함

       // TransactionTemplate // sql 패키지에 트랜잭션 관리자 코드를 사용하는 Class 추가

       // 이 Class의 목적은, PlatformTransactionManager // 트랜잭션의 패턴이 beginTransaction() 실행 후,

       // 결과에 따라 commit()을 하거나, rollback()을 하는 구조이다.

       // 따라서 반복적으로 실행하는 코드를 정의하여 보다 간결하고 직관적이게 코드를 개선하기 위함이다.

       // 반복 코드 안에서 특별하게 수행할 작업은 파라미터로 받는다. // TransactionCallback action

       // 파라미터로 받은 객체를 실행 코드의 틀(템플릿) 안에서 실행한다. // action.doInTransaction();

       // public TransactionTemplate(PlatformTransactionManager txManager) // 생성자

       // public Object execute(TransactionCallback action) throws Exception { // 메서드

       // txManager.beginTransaction(); try { Object result = null; result = action.doInTransaction(); 

       // txManager.commit(); return result; } catch (Exception e) { txManager.rollback(); throw e; } }

       // 코드의 결과가 어떻게 나올 지 모르기 때문에, Object result를 선언한다.

       // action.doInTransaction(); 의 결과를 result에 담고, commit()을 실행하고 result를 리턴한다.

       // 작업 중 오류가 발생했다면, rollback()을 실행하고 예외를 리턴한다.

       // public interface TransactionCallback { // TransactionTemplate의 파라미터에 들어가는 인터페이스이다.

       // 이 메서드를 구현하는 클래스는 이 메서드에 트랜잭션으로 묶어서 다룰 작업을 기술해야 한다.

       // 트랜잭션을 사용하여야 하는 객체를 TransactionCallback을 구현하는 객체로 선언하여 작업한다.

       // Object doInTransaction() throws Exception; // TransactionCallback의 메서드

       // 

v42_1 -> 사용자 로그인 기능 추가 

       // $ update lms_member set pwd=password('1111') // 테스트하기 위해 모든 회원의 암호를 '1111'로 초기화

       // default Member findByEmailAndPassword(String email, String password) throws Exception { return null; }

       // MemberDao에 새로 추가하는 것 이기 때문에 default로 다른 기능(json, objectfile 등)에 영향이 없게끔 한다.

       // public Member findByEmailAndPassword(String email, String password) throws Exception { 

       // Connection con = dataSource.getConnection(); // Statement stmt = con.createStatement();

       // ResultSet rs = stmt.executeQuery( "select member_id, name, email, pwd, tel, photo" 

       //  + " from lms_member" + " where email='" + email  + "' and pwd=password('" + password + "')")

       // password를 넘길때에는 password( )안에 씌워서 넘겨야 한다. // SQL 내부적으로 처리함.

       // MemberDaoImpl에 메서드 추가 // email과 password를 넘겨 받고, SQL문으로 해당 member를 받는다.

       // public class LoginServlet implements Servlet { // Client에게 이메일과 암호를 입력받는 Servlet을 준비한다.

       // Member member = memberDao.findByEmailAndPassword(email, password);

       // 입력받은 이메일과 암호를 findByEmailAndPassword()에게 넘겨 리턴 받는다.

       // servletMap.put("/auth/login", new LoginServlet(memberDao)); // ServerApp에 Servlet을 추가한다.

v42_2 -> SQL 삽입 공격과 자바 시큐어 코딩 // 사용자 로그인 기능 추가

       // DB 프로그래밍의 핵심은 JDBC API를 사용하여 SQL문을 실행하는 것이다.

       // SQL 문은 보통 입력한 값을 가지고 작성하는데, 여기서 보안 문제가 발생한다.

       // SQL을 잘 아는 사용자가 입력값에 SQL 문법을 포함시켜서 내부 데이터를 조회한다거나 변경할 수 있다.

       // PreparedStatement문을 사용하여 SQL 삽입 공격을 대비한다.

       // PreparedStatement문의 경우 값을 입력하는 자리에 ?를 두어, SQL 삽입 공격을 하더라도 값으로 처리한다.

       // public int insert(Board board) throws Exception { Connection con = dataSource.getConnection();

       // PreparedStatement stmt = con.prepareStatement( "insert into lms_board(conts) values(?)")) {

       // stmt.setString(1, board.getTitle()); return stmt.executeUpdate(); }

       // BoardDaoImpl를 비롯한 모든 DaoImpl의 Statement를 PreparedStatement로 바꾸어 준다.

반응형

+ Recent posts