포스트

Day61 실습프로젝트(Proxy패턴, Server)

ProxyPattern

ProxyPattern 개념

  • 실제 객체에 대한 대리자(Proxy) 역할을 하는 객체를 통해 그 객체에 대한 접근의 제어하는 패턴이다.
  • 객체에 대한 접근 제어 : 객체에 대한 접근을 프록시가 제어하여, 불필요한 접근이나 권한이 없는 사용자의 접근을 방지할 수 있습니다.
  • 실제 객체의 부담 감소 : 객체를 실제로 사용하기 전까지 지연 생성하거나, 필요한 때에만 객체를 생성하여 리소스를 절약할 수 있습니다.
  • 추가 기능 구현 : 클라이언트 코드에 영향을 주지 않으면서 프록시에서 로깅, 캐싱, 트랜잭션 관리 등 부가 기능을 쉽게 추가할 수 있습니다.

ProxyPattern 구조

  • RealSubject (실제 객체) :
    • 실제 작업을 수행하는 객체로, Proxy 객체가 이 객체에 대한 접근을 대리하는 역할을 수행한다.
  • Proxy (대리자) :
    • RealSubject에 대한 참조를 가지고 있으며, RealSubject에 대한 요청을 대리 처리한다.
    • 클라이언트가 RealSubject와 상호작용하는 것처럼 보이도록 한다.
  • Subject (공통 인터페이스) :
    • RealSubject와 Proxy가 공통으로 구현해야 하는 인터페이스 또는 추상 클래스이다.
    • 이를 통해 클라이언트는 Proxy와 RealSubject를 동일하게 다룰 수 있다.

image

호출하기

newProxyInstance

  • newProxyInstance의 매개변수로 클래스로더, 인터페이스목록, 함수호출 관리자이다
  • 클래스로더(ClassLoader): 클래스를 메모리에 로딩하는 일을 할 객체의 주소를 준다.
  • 인터페이스 목록(Class[]{interface}): 자동 생성할 클래스가 구현해야 하는 인터페이스 목록
  • 호출관리자(invocationHandler()): 구현체에서 해야할 일을 설정한다.
1
2
3
4
5
6
7
8
Proxy.newProxyInstance(
    this.class.getClassLoader(), // 클래스를 메모리에 로딩하는 일을 할 객체
    new Class[] {Interface.class}, // 자동 생성할 클래스가 구현해야 하는 인터페이스 목록
    new InvocationHandler(){
      @Override
      public Object invoke(Object proxy, Method method, Object[] args)        
    }
);

InvocationHandler

  • proxy : 클라이언트가 메서드를 호출할때 실제 객체처럼 메서드를 호출할 수 있도록 대리 역할을 한다.
  • method : 클라이언트가 호출한 메서드
  • args : 클라이언트가 호출한 메서드의 파라미터 정보
1
2
3
4
5
6
7
8
9
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

InvocationHandler invocationHandler = new InvocationHandler() {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    return null;
  }
}


method 값 호출하기, args 호출하기

  • 클라이언트가 인터페이스의 메서드를 호출하면, Proxy로 생성된 객체에 메서드가 전달되어 invoke메서드에서 수행된다.
  • 인터페이스의 구현체로 역할을 하려면 Invoke()메서드에 적적한 코드를 작성하면된다.


Dao객체 만들기

Dao구조 파악하기

  • Dao의 기능은 sqlSession을 통해 mapper에 SQL을 전달하는 역할을 한다.
  • SQL문은 객체 리스트를 받는 selectList, int값을 받는 insert,update,delete와 객체를 받는 selectOne이 있다.
1
2
3
4
5
selectList()
insert()
update()
delete()
selectOne()


DaoFactory 설계

  • Dao인터페이스에는 매개변수가 0~2개가 있다.
  • return 타입에 따라 수행하는 메서드가 다르다.
  • invoke의 함수가 길기 때문에
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Dao설계
//UserDao, BoardDao, ProjectDao가 있으므로 T로 받는 메서드를 생성
private <T> T createObject(Class<T> daoType){
  return new Proxy.newProxyInstance(
      this.getClass().getClassLoader(),
      new Class[]{daoType},
      this::invoke    
  );
} 

//리턴타입과 매개변수가 같으면 메서드 레퍼런스 사용가능
private Object invoke(Object proxy, Method method, Object[] args){
  //매개변수를 담는다.
  
  //리턴타입에 따라 수행할 메서드를 나눈다.
}


invoke메서드 수정

매개변수 갯수 설정

  • 매개변수
    • 0개 : list()를 수행하는 sqlSession으로 parameter에 null값을 넣는다.
    • 1개 : 조건을 검색하는 where절을 수행하는 sqlSession으로 parameter에 args[0]을 넣는다.
    • 2개 : 2개의 값을 넘기는 sqlSession으로 mapper에서는 map을 통해 받기 때문에 Map<String, Object>로 넘긴다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Object paramValue = null;
if (args != null){
  if (agrs.length ==1{
    paramValue = args[0];
  } else {
    Parameter[] params = method.getParameters();
    HashMap<String,Object> map = new HashMap<>();
    for (int i = 0; i < args.length; i++){
      Param anno = params.getAnnotation(Param.class);
      map.put(anno, args[i]);
      }
    paramValue = map;
  }
}
  • Annotation
    • mapper에 사용할 property와 args[n]과 다르기 때문에 Map을 넘긴다 하더라도 get하지 못한다.
    • Annotation을 사용하면 Runtime중에 매개변수명에 태그 값을 가져올 수 있다.
    • Dao에 Annotaion을 적용할 메서드 파라미터에 태그를 붙이면 된다.
1
2
3
4
5
6
7
8
9
10
11
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Param{
  String value();
}

//Dao 적용예시
void updateViewCount(@Param("no") int boardNo, @Param("count") int count)


ReturnType에 따른 SqlSession 설정

  • ReturnType
    • List 타입 : sqlSession.selectList를 호출한다.
    • int, void, boolean : sqlSession.insert를 호출한다.
    • User, Board, Project : sqlSession.selectOne를 호출한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Class<?> returnType = method.getReturnType();

if (returnType == List.class){
  return sqlSession.selectList("sql.method", paramValue);
} else if (returnType == int.class || returnType == void.class || returnType == boolean.class ){
  int count = sqlSession.insert("sql.method", paramValue);
  if (returnType == boolean.class){
    return count > 0;  
  } else if (returnType == void.class){
    return null  
  } else{
    return count;  
  }
} else {
  return sqlSession.selectOne("sql.method", paramValue);
}


서버와 클라이언트

서버와 클라이언트로 분리하기

현재 S/W Architecture

  • myapp-client와 DBMS와 통신을 한다.
  • 기능 변경 시 모든 client가 코드를 재설치 해야하므로 유지보수 측면에서 불리하다.

    image

  • server-client S/W Architecture

  • Server : 모든 기능을 수행 후 UI를 생성하여 Client에게 전송
  • Client : 전송받은 UI 출력 및 프롬프트 전송
  • 서버와 클라이언트는 데이터를 1회씩 주고 받는다.

image

클라이언트 만들기

  • 클라이언트에 필요한 파일은 ClientApp파일과 Prompt파일만 있으면 된다.
  • ClinetApp : 서버로부터 보내온 데이터를 출력하고 Prompt에서 입력 받아 데이터를 전송한다.
  • Prompt : 키보드에서 받는 데이터를 저장하는 역할을 한다.(기존의 Prompt와 동일)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 void execute() {
    String host = Prompt.input("서버? ");
    int port = Prompt.inputInt("포트번호? ");

    try (Socket socket = new Socket(host, port);
        DataInputStream in = new DataInputStream(socket.getInputStream());
        DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {

      while (true) {
        String message = in.readUTF();

        if (message.equals(GOODBYE)) {
          System.out.println(message.substring(0, message.indexOf(GOODBYE)));
          System.out.println("종료합니다.");
          break;
        }
        System.out.println(message);

        String input = Prompt.input("");
        out.writeUTF(input);
        out.flush();
      }
    } catch (Exception e) {
      System.out.println("실행오류");
      e.printStackTrace();
    }
    Prompt.close();
  }


서버 만들기

Prompt 수정

  • prompt는 키보드에서 입력받는것이 아니라, in.readUTF로 읽어온다.
  • cmd창에 결과를 출력하는 것이 아니라 out.writeUTF()로 내보낸다.
  • 클라이언트와 상호 1회씩 read와 write를 주고 받기 때문에 1회 내보내기 전까지 메세지를 전체 저장해야한다.
  • StringWriter 객체를 PrintWriter에 데코레이터로 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.PrintWriter;
import java.io.StringWriter;

StringWriter strWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(strWriter);

public void print(String str) {
  printWriter.print(str);
}

public void printf(String format, Object... args) {
  printWriter.printf(format, args);
}

public void println(String str) {
  printWriter.println(str);
}


ServerApp 수정

  • 서버에서는 로그인 기능을 수행해야한다.
  • 소켓과 연결하는 기능을 수행해야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private void execute() throws Exception {

    for (ApplicationListener listener : listeners) {
      try {
        if (!listener.onStart(appCtx)) {
          System.out.println("종료합니다.");
          return;
        }
      } catch (Exception e) {
        System.out.println("리스너 실행 중 오류 발생!");
        e.printStackTrace();
      }
    }

    ServerSocket serverSocket = new ServerSocket(8888);
    System.out.println("서버 실행 중...");

    while (true) {
      service(serverSocket.accept());
    }

    // 애플리케이션이 종료될 때 리스너에게 알린다.
//    for (ApplicationListener listener : listeners) {
//      try {
//        listener.onShutdown(appCtx);
//      } catch (Exception e) {
//        System.out.println("리스너 실행 중 오류 발생!");
//      }
//    }
  }
  • Thread를 생성하여 멀티쓰레드 기능을 구현해야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private void service(Socket socket) {
    new Thread(() -> {
      try {
        Prompt prompt = new Prompt(socket, appCtx);

        prompt.println("[프로젝트 관리 시스템]");
        String email = prompt.input("이메일?");
        String password = prompt.input("암호?");

        UserDao userDao = (UserDao) appCtx.getAttribute("userDao");
        User loginUser = userDao.findByEmailAndPassword(email, password);
        if (loginUser == null) {
          prompt.println("이메일 또는 암호가 맞지 않습니다!");
          prompt.print("<[goodbye!]>");
          prompt.end();
          prompt.close();
          return;
        }

        // 로그인 정보를 보관해 둔다.
        prompt.setAttribute("loginUser", loginUser);

        appCtx.getMainMenu().execute(prompt);
        prompt.print("<[goodbye!]>");
        prompt.end();
        prompt.close();
      } catch (Exception e) {
        System.out.println("실행 오류!");
        e.printStackTrace();
      }
    }).start();
  }


  • menu에서 사용하는 System.out.의 기능은 Prompt 객체로 이관한다.
  • Prompt 객체를 사용하기 위해 매개변수로 Prompt를 받는다
1
2
3
4
5
6
7
public interface Menu {

  String getTitle();

  void execute(Prompt prompt);
}


Command 수정

  • menuItem에서 Prompt를 전달하여 리프트리에서 Prompt를 사용하게 한다.
  • AppContext도 Prompt에서 관리 하므로 ctx 필드도 삭제한다.
  • 인터페이스에 맞게 수정한다.
1
2
3
4
public interface Command {

  void execute(String menuName, Prompt prompt);
}
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.