Day61 실습프로젝트(Proxy패턴, Server)
ProxyPattern
ProxyPattern 개념
- 실제 객체에 대한 대리자(Proxy) 역할을 하는 객체를 통해 그 객체에 대한 접근의 제어하는 패턴이다.
- 객체에 대한 접근 제어 : 객체에 대한 접근을 프록시가 제어하여, 불필요한 접근이나 권한이 없는 사용자의 접근을 방지할 수 있습니다.
- 실제 객체의 부담 감소 : 객체를 실제로 사용하기 전까지 지연 생성하거나, 필요한 때에만 객체를 생성하여 리소스를 절약할 수 있습니다.
- 추가 기능 구현 : 클라이언트 코드에 영향을 주지 않으면서 프록시에서 로깅, 캐싱, 트랜잭션 관리 등 부가 기능을 쉽게 추가할 수 있습니다.
ProxyPattern 구조
- RealSubject (실제 객체) :
- 실제 작업을 수행하는 객체로, Proxy 객체가 이 객체에 대한 접근을 대리하는 역할을 수행한다.
- Proxy (대리자) :
- RealSubject에 대한 참조를 가지고 있으며, RealSubject에 대한 요청을 대리 처리한다.
- 클라이언트가 RealSubject와 상호작용하는 것처럼 보이도록 한다.
- Subject (공통 인터페이스) :
- RealSubject와 Proxy가 공통으로 구현해야 하는 인터페이스 또는 추상 클래스이다.
- 이를 통해 클라이언트는 Proxy와 RealSubject를 동일하게 다룰 수 있다.
호출하기
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가 코드를 재설치 해야하므로 유지보수 측면에서 불리하다.
server-client S/W Architecture
- Server : 모든 기능을 수행 후 UI를 생성하여 Client에게 전송
- Client : 전송받은 UI 출력 및 프롬프트 전송
- 서버와 클라이언트는 데이터를 1회씩 주고 받는다.
클라이언트 만들기
- 클라이언트에 필요한 파일은 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 수정
- 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 라이센스를 따릅니다.