Skip to content

Conversation

@shinminkyoung1
Copy link

완료 작업 목록

웹 서버 7단계 - 이미지 업로드 구현

  • Multipart 파서 구현HttpRequest에서 multipart/form-data 바디 파싱 로직 추가
  • 파일 저장 로직: 업로드된 이미지 파일을 서버 로컬 디렉토리(예: ./uploads)에 저장
  • DB 스키마 확장ARTICLE 테이블에 이미지 파일 경로(imagePath) 컬럼 추가
  • 정적 이미지 서빙/uploads/ 경로로 들어오는 GET 요청에 대해 실제 이미지 파일 응답
  • 마이페이지 연동: 프로필 수정 폼 구현 및 User 테이블에 프로필 이미지 경로 추가
  • 뷰 템플릿 수정index.html 및 mypage.html에서 저장된 경로를 이용해 이미지 출력

주요 고민과 해결 과정

  1. 현재 BufferReader를 사용하고 있는데, Multipart 데이터는 바이너리 데이터이기에 텍스트 전용인 BufferReader로 읽으면 파일 데이터가 깨지는 문제 발생

→ 헤더는 BufferReader로 읽고, 바디(파일 데이터 포함)는 InputStream에서 바이트 단위로 직접 읽도록 구조 변경

private List<MultipartPart> multipartParts = new ArrayList<>();

private void parseBody(InputStream in) throws IOException {
        byte[] body = in.readNBytes(contentLength);
        String contentType = headers.get("Content-Type");

        if (contentType != null && contentType.contains("multipart/form-data")) {
            String boundary = contentType.split("boundary=")[1];
            this.multipartParts = HttpRequestUtils.parseMultipartBody(body, boundary);

            for (MultipartPart part : multipartParts) {
                if (!part.isFile()) {
                    params.put(part.name(), new String(part.data(), StandardCharsets.UTF_8));
                }
            }

        } else {
            String bodyStr = new String(body, StandardCharsets.UTF_8);
            this.params.putAll(HttpRequestUtils.parseParameters(bodyStr));
        }
    }
public HttpRequest(InputStream in) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));

        String line = br.readLine();
        if (line == null) return;

        // Request Line 파싱
        String[] tokens = line.split(" ");
        if (tokens.length >= 3) {
            this.method = tokens[0];
            this.url = tokens[1];
            this.protocol = tokens[2];
            this.path = HttpRequestUtils.parsePath(this.url);
            this.queryString = HttpRequestUtils.parseQueryString(this.url);
            this.params = HttpRequestUtils.parseParameters(this.queryString);
        }

        // 나머지 헤더 정보 읽음
        while ((line = br.readLine()) != null && !line.isEmpty()) {
            Pair pair = HttpRequestUtils.parseHeader(line);
            if (pair != null) {
                headers.put(pair.key, pair.value);

                if ("Content-Length".equalsIgnoreCase(pair.key)) {
                    this.contentLength = Integer.parseInt(pair.value);
                }

                // 쿠키 파싱
                if ("Cookie".equalsIgnoreCase(pair.key)) {
                    this.cookies = HttpRequestUtils.parseCookies(pair.value);
                }
            }
        }

        if (contentLength > 0) {
            parseBody(in);
        }
    }
  1. 파일명 중복 방지와 저장 경로 설정을 어떻게 해결하는가

    → 여러 사용자가 동시에 image.jpg라는 파일을 올려도 덮어씌워지지 않도록 UUID를 활용해 고유한 이름 부여

    @Override
        public void process(HttpRequest request, HttpResponse response) {
            String sessionId = request.getCookie("sid");
            User loginUser = SessionManager.getLoginUser(sessionId, userDao);
    
            if (loginUser == null) {
                response.sendRedirect("/login");
                return;
            }
    
            String title = request.getParameter("title");
            String contents = request.getParameter("contents");
            String writer = loginUser.userId();
            String imagePath = null;
    
            for (MultipartPart part : request.getMultipartParts()) {
                if (part.isFile() && "image".equals(part.name()) && part.data().length > 0) {
                    imagePath = saveUploadedFile(part);
                }
            }
            
            ...
            
            
        private String saveUploadedFile(MultipartPart part) {
            String uploadDir = Config.STATIC_RESOURCE_PATH + "/uploads";
            File dir = new File(uploadDir);
            if (!dir.exists()) {
                dir.mkdir(); // 폴더 없으면 생성
            }
    
            String fileName = UUID.randomUUID().toString() + "_" + part.fileName();
            File file = new File(dir, fileName);
    
            try (FileOutputStream fos = new FileOutputStream(file)) {
                fos.write(part.data());
                logger.debug("File saved successfully: {}", file.getAbsolutePath());
                return "/uploads/" + fileName;
            } catch (IOException e) {
                logger.debug("File save error: {}", e.getMessage());
                return null;
            }
        }
  2. 스트림 읽기 충돌 에러 발생

[webserver.HttpResponse] - Exception during transfer: Broken pipe
14:43:04.687 [ERROR] [pool-1-thread-2] [webserver.HttpResponse] - Exception during transfer: Broken pipe
  • 상황 분석

    HttpRequest 생성자에서 BufferdReader를 생성하여 헤더를 읽으면, BufferedReader을 위해 바디 데이터의 일부까지 미리 읽어 자신의 내부 버퍼에 넣음

    그 상태에서 in.readNBytes(contentLength)를 호출하면, 이미 버퍼로 들어간 앞부분 데이터는 건너뛰게 되어 데이터 불일치가 발생하고 소켓 연결이 꼬임

    → HttpRequest를 바이트 단위로 직접 제어하도록 수정

    ⇒ 스트림 읽기 방식 변경

    InputStream에서 1바이트씩 읽어 한 줄(String)을 만듦

    BufferedReader의 버퍼링 문제 해결 위함

    HttpRequest.java

    public HttpRequest(InputStream in) throws IOException {
    
            String line = readLine(in);
            if (line == null) return;
    
            // Request Line 파싱
            String[] tokens = line.split(" ");
            if (tokens.length >= 3) {
                this.method = tokens[0];
                this.url = tokens[1];
                this.protocol = tokens[2];
                this.path = HttpRequestUtils.parsePath(this.url);
                this.queryString = HttpRequestUtils.parseQueryString(this.url);
                this.params = HttpRequestUtils.parseParameters(this.queryString);
            }
    
            // 나머지 헤더 정보 읽음
            while ((line = readLine(in)).isEmpty()) {
                Pair pair = HttpRequestUtils.parseHeader(line);
                if (pair != null) {
                    headers.put(pair.key, pair.value);
    
                    if ("Content-Length".equalsIgnoreCase(pair.key)) {
                        this.contentLength = Integer.parseInt(pair.value);
                    }
    
                    // 쿠키 파싱
                    if ("Cookie".equalsIgnoreCase(pair.key)) {
                        this.cookies = HttpRequestUtils.parseCookies(pair.value);
                    }
                }
            }
    
            if (contentLength > 0) {
                parseBody(in);
            }
        }
        
        private String readLine(InputStream in) throws IOException {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b;
            while ((b = in.read()) != -1) {
                if (b == '\r') {
                    int next = in.read();
                    if (next == '\n') break;
                    baos.write(b);
                    baos.write(next);
                } else if (b == '\n') {
                    break;
                } else {
                    baos.write(b);
                }
            }
            return baos.toString(StandardCharsets.UTF_8);
        }

    정리)

    • 바이너리 스트림 제어

      BufferedReader를 제거하고 InputStream을 직접 제어하여 헤더는 텍스트로, 바디는 순수 바이트로 읽는 readLine과 parseBody 로직을 구현

    • 멀티파트 파서 구현

      multipart/form-data 규격에 맞춰 boundary를 기준으로 데이터를 쪼개 텍스트와 파일을 구분하는 로직을 HttpRequestUtils에 추가

AI에 도움받은 부분

  • 커밋 메시지 작성
  • 프로필 이미지 수정 시 DB에는 수정되었지만 화면에 렌더링 되지 않음의 트러블 슈팅

→ AppConfig에서 마이페이지에 대한 정적 파일 연결을 제외하지 않아서 발생했던 문제였음

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant