Posts 학원 #89일차: 로그인과 사진 업로드 구현
Post
Cancel

학원 #89일차: 로그인과 사진 업로드 구현

PMS 프로젝트

이전 버전: 로그인 기능 구현 복습

이전에는handler 패키지의 LoginCommand에서 로그인 기능을 구현했다. 로그인을 구현하기 위해서는 세션을 다뤄야 한다. 세션은 클라이언트 전용 정보를 다루는 클라이언트 전용 보관소이다. 로그인 기능을 구현하기 전에는 모든 정보를 context 맵에 저장하였지만 context모든 클라이언트가 공유하기 때문에 클라이언트 전용 정보를 다루는 Session이 필요했다.

이때 클라이언트와 서버 간의 프로토콜을 변경했다. 클라이언트는 무조건 요청을 할 때 세션 아이디를 요청 명령 앞에 보내고, 서버는 응답을 할 상대 클라이언트의 세션 아이디를 응답 앞에 보내도록 만들었다. 클라이언트 요청을 받을 때 서버는 세션 아이디를 분석해 세션을 생성하고 보관하였다. 그리고 이 세션을 request 안에 보관하였다. 그러면 Commandexcute(Request request) 메서드의 파라미터를 통해 request 객체를 받아 그 안에 있는 session을 꺼내 쓰면 되었다.

ServerApp

1
Request request = new Request(requestLine, context, out, in, sessionId);
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class ServerApp {
  
	//..
  public static void main(String[] args) {
    ServerApp server = new ServerApp();

    // 리스너(옵저버/구독자) 등록
    server.addApplicationContextListener(new AppInitListener());
    server.addApplicationContextListener(new DataHandlerListener());
    server.addApplicationContextListener(new RequestMappingListener());

    server.service(8888);
  }

  private static void handleClient(Socket clientSocket) {
    InetAddress address = clientSocket.getInetAddress();
    System.out.printf("클라이언트(%s)가 연결되었습니다.\n",
        address.getHostAddress());

    try (Socket socket = clientSocket; // try 블록을 떠날 때 close()가 자동 호출된다.
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        PrintWriter out = new PrintWriter(socket.getOutputStream())) {

      // 클라이언트가 보낸 세션 아이디에 따라 세션 보관소를 준비한다.
      String sessionId = prepareSession(in.readLine());

      // 클라이언트가 보낸 요청을 읽는다.
      String requestLine = in.readLine();

      if (requestLine.equalsIgnoreCase("stop")) {
        stop = true; // 서버의 상태를 멈추라는 의미로 true로 설정한다.
        out.println("SessionID=xxx");
        out.println("서버를 종료하는 중입니다!");
        out.println();
        out.flush();
        return;
      }

      // 커맨드나 필터가 사용할 객체를 준비한다.
      Request request = new Request(requestLine, context, out, in, sessionId);

      // context 맵에 보관된 필터 체인을 꺼낸다.
      FilterChain filterChain = (FilterChain) context.get("filterChain");

      // 필터들의 체인을 실행한다.
      // => 필터 체인을 따라가면서 중간에 삽입된 필터가 있다면 실행할 것이다.
      // => 마지막 필터에서는 클라이언트가 요청한 명령을 처리할 것이다.
      if (filterChain != null) {
        // 클라이언트 응답 첫 줄에 세션 ID를 출력한다.
        out.printf("SessionID=%s\n", sessionId);
        filterChain.doFilter(request);
      }

      // 응답의 끝을 알리는 빈 문자열을 보낸다.
      out.println();
      out.flush();

    } catch (Exception e) {
      System.out.println("클라이언트와의 통신 오류!");
      e.printStackTrace();
    }

    System.out.printf("클라이언트(%s)와의 연결을 끊었습니다.\n",
        address.getHostAddress());
  }

  private static String prepareSession(String sessionInfo) {

    String[] values = sessionInfo.split("=");

    // 클라이언트에서 자신의 세션 아이디를 보내왔다면,
    if (values.length == 2 && context.get(values[1]) != null) {
      // 기존에 서버에서 발급한 세션 아이디를 그대로 리턴한다.
      return values[1];
    }

    // 세션 아이디가 없다면,
    // 클라이언트에게 새 세션 아이디를 부여한다.
    String sessionId = UUID.randomUUID().toString();

    // 새 세션을 위한 보관소를 생성한다.
    HashMap<String,Object> sessionMap = new HashMap<>();

    // 필터나 커맨드가 사용할 수 있도록 context 맵에 저장한다.
    context.put(sessionId, sessionMap);

    return sessionId;
  }
}

멀티파트 파일 업로드 처리하기

MemberAddServlet

1
2
3
@MultipartConfig(maxFileSize = 1024 * 1024 * 10)
@WebServlet("/member/add")
public class MemberAddServlet extends HttpServlet {

MultipartConfig를 위와 같이 클래스 위에 붙여주었다. maxFileSize의 맨 뒤 값은 최대 10MB까지 허용한다는 뜻이다.

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {

    ServletContext ctx = request.getServletContext();
    MemberService memberService =
        (MemberService) ctx.getAttribute("memberService");

    // 클라이언트가 POST 요청할 때 보낸 데이터를 읽는다.
    //request.setCharacterEncoding("UTF-8");

    Member member = new Member();
    member.setName(request.getParameter("name"));
    member.setEmail(request.getParameter("email"));
    member.setPassword(request.getParameter("password"));
    member.setTel(request.getParameter("tel"));

    // <input type="file"...> 입력 값 꺼내기
    Part photoPart = request.getPart("photo");

    // 회원 사진을 저장할 위치를 알아낸다.
    // => 컨텍스트루트/upload/파일
    // => 파일을 저장할 때 사용할 파일명을 준비한다.
    String filename = UUID.randomUUID().toString();
    String saveFilePath = ctx.getRealPath("/upload/" + filename);

    // 해당 위치에 업로드된 사진 파일을 저장한다.
    photoPart.write(saveFilePath);

    // DB에 사진 파일 이름을 저장하기 위해 객체에 보관한다.
    member.setPhoto(filename);

    // 회원 사진의 썸네일 이미지 파일 생성하기
    generatePhotoThumbnail(saveFilePath);

    response.setContentType("text/html;charset=UTF-8");
    PrintWriter out = response.getWriter();

    out.println("<!DOCTYPE html>");
    out.println("<html>");
    out.println("<head>");
    out.println("<meta http-equiv='Refresh' content='1;url=list'>");
    out.println("<title>회원등록</title></head>");
    out.println("<body>");

    try {
      out.println("<h1>회원 등록</h1>");

      memberService.add(member);

      out.println("<p>회원을 등록하였습니다.</p>");

    } catch (Exception e) {
      out.println("<h2>작업 처리 중 오류 발생!</h2>");
      out.printf("<pre>%s</pre>\n", e.getMessage());

      StringWriter errOut = new StringWriter();
      e.printStackTrace(new PrintWriter(errOut));
      out.println("<h3>상세 오류 내용</h3>");
      out.printf("<pre>%s</pre>\n", errOut.toString());
    }

    out.println("</body>");
    out.println("</html>");
  }

Content-Type 헤더에 지정한 구분자를 사용하여 각 파트를 분리한 다음 데이터를 읽는다. 문제는 기존에 제공하는 getParameter()로는 멀티파트 형식으로 전송된 데이터를 읽을 수 없다.

다음과 같이 멀티파트 형식으로 전송된 데이터를 읽을 수 있다.

  • 개발자가 직접 멀티파트 형식을 분석하여 데이터를 추출한다.
  • 외부 라이브러리를 사용한다.
  • Servlet 3.0부터 제공하는 기능을 사용한다.

세 번째 방법을 사용하여 멀티파트 형식으로 전송된 데이터를 읽도록 하였다.

한편, 왜 UUID.randomUUID().toString() 메서드를 사용하여 랜덤한 파일 이름을 생성하여 저장했을까? 만약 사용자가 업로드한 파일의 이름을 그대로 사용한다면 다른 사용자가 똑같은 이름으로 파일을 업로드하였을 때 기존 파일을 덮어쓰게 되기 때문이다. 따라서 업로드한 파일명을 사용하지 않고 다른 이름으로 저장해야 한다. 이렇게 하면 한글 파일명도 넣을 수 있다.

html 파일에서는 다음과 같이 form을 만들었다.

1
2
3
<form action="add" metohd="post" enctype="multitype/form-data">
  <input type="file" name="photo">
</form>

다음과 같이 꺼내 쓸 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
out.printf("<tr>"
  + "<td>%d</td>"
  + "<td><a href='detail?no=%1$d'><img src='../upload/%s' alt='%2$s'>%s</a></td>"
  + "<td>%s</td>"
  + "<td>%s</td>"
  + "<td>%s</td>"
  + "</tr>\n",
member.getNo(),
member.getPhoto(),
member.getName(),
member.getEmail(),
member.getTel(),
member.getRegisteredDate());
This post is licensed under CC BY 4.0 by the author.

학원 #88일차: 서블릿 프로그래밍

HTML5 CSS3 웹 표준의 정석 #7장: 색상과 배경을 위한 스타일

Loading comments from Disqus ...