목표

목표.

이번 작품의 목표는 다량의 데이터가 들어왔을 때 두 개의 서버에서 나누어서 처리하는 분산 처리 서버 구현이었다.

원래 계획했던 작품은 웹페이지로 클라이언트에게 이미지 파일이 압축된 zip 파일을 받고, 메인 서버에서 zip 파일을 압축 해제하며 파일을 byte로 서브 서버에게 전송한다. 그러면 서브 서버가 ocr을 진행한 후 DB에 저장하는 작품이었다.

시스템 아키텍쳐
BARAM_21년도_후반기_제안서_28기_19김다현.hwp
1.36MB

 

문제점

문제점 1.

web으로 클라이언트의 요청을 받으려했는데 그러면 MAIN server를 spring을 이용해 web server로 구현해야 했었다. spring을 사용하기 때문에 java를 사용했다. 근데 sub server는 C로 구현해서 통신이.... 너무 어려웠다.

이미지 하나의 전송까지는 성공했지만 2개부터 잘 되지 않았다..ㅠㅠ

문제점 2.

sub server는 AWS에서 무료로 제공하는 서비스를 이용했다. 근데 AWS에서 무료로 제공하는 서버의 성능이 너무 좋지 않았다. 어느 정도였냐면 tesseract를 사용하기 위해서는 opencv를 설치해야했는데 opencv의 컴파일이 되지 않았다. tesseract 진행 후 DB 구현하는 걸 구현은 해두었는데 서버에 올릴 수 없었다.

 

결과

결과.

그래서 결과적으로는 3개의 서버를 모두 C 서버로 구현해서 소켓통신을 했다.

결과물로는 클라이언트와 로드밸런싱과 DB 저장 정도? 계획했던 작품에 비해서는 결과가 제대로 나오지는 않았지만 load balancing과 socket 통신을 구현해볼 수 있는 기회였다.

설명.

AWS에서 2개의 서버를 대여한 후 아래의 "server.c" 코드(데이터 수신 및 DB 저장)를 각각 서버에 넣어주고 실행파일을 만든 후 서버를 실행시켜준다.

main server의 9999 port를 이용해서 로드밸런서를 구현했으므로 client는 main server의 9999 포트에 접속해서 이미지를 전송한다. 이때 파일 전송은 스레드로 구현해서 속도를 향상시켰다.

클라이언트는 같은 주소로 파일을 전송하지만 서버 두 개가 데이터를 번갈아서 받는 모습을 볼 수 있다. round robbin으로 로드밸런싱을 구현했기 때문에 순차적으로 데이터를 전송받는 것이다.

전송이 끝나면 이미지들이 DB에 저장된다.

 

코드

최종 코드.

https://github.com/ekgus2222/multiserver

 

GitHub - ekgus2222/multiserver: testset

testset. Contribute to ekgus2222/multiserver development by creating an account on GitHub.

github.com

결과 코드는 여기서!

 

 

[HAProxy] TCP 로드밸런싱
https://hyunisland.tistory.com/71

 

[HAProxy] TCP 로드밸런싱

/* * ubuntu 18.04 - 서버 3개 & 클라이언트 * HA-Proxy version : 1.8.40 */ 총 서버는 3개를 준비했다. 하나의 클라이언트에서 HAProxy 서버에 접속하면 HAProxy 서버가 연결된 2개의 서버에 로드밸런싱을 해준..

hyunisland.tistory.com

TCP 로드밸런싱은 여기서!

 

 

 

추가 코드.

[socket 통신] java client & c server 간 이미지 전송
https://hyunisland.tistory.com/68

 

[Tesseract + DB]

OCR + Tesseract
DB 결과

#include<string>
#include<tesseract/baseapi.h>
#include<leptonica/allheaders.h>
#include<opencv4/opencv2/opencv.hpp>

#include "/usr/include/mysql/mysql.h"
#include <string.h>
#include <stdio.h>


void main(int argc, char *argv[])
{
    char* impath = argv[1]; // path 설정
    string outText, imPath = impath;
    Mat im = cv::imread(imPath, IMREAD_COLOR);
    tesseract::TessBaseAPI *api = new tesseract::TessBaseAPI();

    api->Init(NULL, "eng", tesseract::OEM_LSTM_ONLY);
    api->SetPageSegMode(tesseract::PSM_AUTO);
    api->SetImage(im.data, im.cols, im.rows, 3, im.step);
    outText = string(api->GetUTF8Text());
    cout<<outText;
    api->End();

    MYSQL       *connection=NULL, conn;
    MYSQL_RES   *sql_result;
    MYSQL_ROW   sql_row;
    int       query_stat; 

    char text[20];
    strcpy(text, outText.c_str());
    char time[80] = "230";
    char query[255];
    
    mysql_init(&conn);

    connection = mysql_real_connect(&conn, DB_HOST,
                                    DB_USER, DB_PASS,
                                    DB_NAME, 3306,
                                    (char *)NULL, 0);

    if (connection == NULL)
    {
        fprintf(stderr, "Mysql connection error : %s", mysql_error(&conn));
        return;
    }

    // query_stat = mysql_query(connection, "select * from text");
    // if (query_stat != 0)
    // {
    //     fprintf(stderr, "Mysql query error : %s", mysql_error(&conn));
    //     return 1;
    // }
    
    // sql_result = mysql_store_result(connection);
    
    // printf("%-11s %-11s %-11s\n", "id", "텍스트", "시간");
    // while ( (sql_row = mysql_fetch_row(sql_result)) != NULL )
    // {
    //     printf("%-11s %-11s %-11s\n", sql_row[0], sql_row[1], sql_row[2]);
    // }

    mysql_free_result(sql_result);

    printf("input text : %s\n", text);
    //fgets(text, 12, stdin);
    CHOP(text);

    printf("input time : %s\n", time);
    //fgets(time, 80, stdin);
    CHOP(time);

    printf("\n");

    sprintf(query, "insert into text(text,time) values ('%s', '%s')",
                   text, time);

    query_stat = mysql_query(connection, query);
    if (query_stat != 0)
    {
        fprintf(stderr, "Mysql query error : %s", mysql_error(&conn));
        return;
    }
    else {
	printf("insert ok\n");
    }

    mysql_close(connection);
}

/*

* ubuntu 18.04 - 서버 3개 & 클라이언트

* HA-Proxy version : 1.8.40

*/

 

 

총 서버는 3개를 준비했다.

하나의 클라이언트에서 HAProxy 서버에 접속하면 HAProxy 서버가 연결된 2개의 서버에 로드밸런싱을 해준다.

roundrobbin 방식을 사용함!

1. HAProxy 설치

sudo add-apt-repository ppa:vbernat/haproxy-1.8
sudo apt-get update
sudo apt-get install haproxy

2. HAProxy Load Balancing 설정

sudo vi /etc/haproxy/haproxy.cfg

40 : Local_Server의 bind에는 HAProxy IP를 적어준다. 이 IP를 통해서 51줄과 52줄의 server에 접속하게 되는 것이다.

41 : 소켓통신이므로 mode는 tcp로 설정한다. 만약 web으로 하고 싶다면 http로 바꾸면 된다

52, 53 : 접속할 server의 IP 주소와 포트를 적어주면 된다.

3. 서버 설정

나는 AWS의 EC2 서버를 2개 할당받아서 사용했다.

1달 무료라서! 그리고 외부 접속이 가능해서 사용했다. 따로 서버호스팅 사이트를 써도 무관하다.

클라이언트에는 다음과 같은 코드를 만들고 실행파일을 만들어준다.

sudo vi client.c

/* client.c */

#include<stdio.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<unistd.h>
#include<string.h>

int main(int argc, char* argv[])
{
    int my_sock;
    struct sockaddr_in serv_addr;
    int str_len;
    if(argc != 3)
    {
        printf("%s <IP> <PORT>\n", argv[0]);
        exit(1);
    }
    my_sock = socket(PF_INET,SOCK_STREAM,0); //1번
    if(my_sock == -1)
        printf("socket error \n");
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_addr.sin_port=htons(atoi(argv[2]));

    if(connect(my_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1) //2번
        printf("connect error\n");
    char message[15];
    str_len = read(my_sock,message,sizeof(message)-1); //3번
    if(str_len==-1)
        printf("read error\n");
    printf("서버에서 : %s \n", message);
    close(my_sock); //4번
    return 0;
}
gcc client.c -o client

HAProxy를 통해 접근될 서버 2개에 다음과 같은 코드를 넣어준 뒤 실행파일을 만들어준다.

sudo vi server.c

/* server.c */

#include<stdio.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<unistd.h>
#include<string.h>

int main(int argc, char* argv[])
{
    int serv_sock;
    int clint_sock;

    struct sockaddr_in serv_addr;
    struct sockaddr_in clint_addr;
    socklen_t clnt_addr_size;

    if(argc != 2)  
    {
        printf("%s <port>\n", argv[0]);
        exit(1);
    }
    serv_sock = socket(PF_INET, SOCK_STREAM,0);
    if(serv_sock == -1)
        printf("socket error\n");

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(atoi(argv[1]));

    if(bind(serv_sock,(struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) 
        printf("bind error\n");
    if(listen(serv_sock,5)==-1) 
        printf("listen error\n");

    clnt_addr_size = sizeof(clint_addr);
    clint_sock = accept(serv_sock,(struct sockaddr*)&clint_addr,&clnt_addr_size); 
    if(clint_sock == -1)
        printf("accept error\n");

    char message[] = "hello client (server 1)"; //또는 server 2
    write(clint_sock, message, sizeof(message));
    close(serv_sock); //6번
    close(clint_sock);
    return 0;
}
gcc server.c -o server

4. HAProxy 실행

HAProxy 서버에서 HAProxy를 실행해준 뒤 상태를 확인한다. 아래 사진처럼 나오면 성공

systemctl start haproxy.service
netstat -nltp|grep 9999
systemctl status haproxy

5. 실행

우선 서버 2개를 실행시켜준다. 꼭 서버 먼저!

./server 9999

그다음 client를 실행시켜주는데 이때 IP와 포트는 2번에서 설정해준대로 설정한다.

./client 127.0.0.1 9999

같은 IP로 접속했는데도 다른 결과가 출력되는 모습을 볼 수 있다.

+ 참고 블로그

// socket 통신
https://m.blog.naver.com/cy2003k/222060153639

// HAProxy
https://kimtaekhan.tistory.com/4
https://tecadmin.net/how-to-setup-haproxy-load-balancing-on-ubuntu-linuxmint/

JAVA CLIENT

//java client
package hello.FileUpload;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;

public class JavaSocketClient{
    public static void main(String[] args) throws IOException {
        JavaSocketClient cm = new JavaSocketClient();
        cm.run();
    }

    void run() throws IOException {
        //소켓 생성
        Socket socket = new Socket();
        //서버쪽 주소 생성. ip는 localhost, 포트는 9999. 필요에 맞게 바꾸기
        SocketAddress address = new InetSocketAddress("127.0.0.1", 9999);
        //주소에 해당하는 서버랑 연결
        socket.connect(address);

        System.out.println("CONNECT OK");


        try {
        	//받기
            receive_my(socket);
            System.out.println("recieve ok");

			//보내기
            send_Image(socket);
        } catch (IOException e) {
            e.printStackTrace();
        }


    }

    public static void send_Image(Socket socket) throws IOException {
        try{
            DataOutputStream DOS = new DataOutputStream(socket.getOutputStream());
            File fp = new File("/home/dahyeon/123.png");
            FileInputStream fs = new FileInputStream(fp);
            if(fs == null)
                return;
            BufferedInputStream br_file = new BufferedInputStream(fs);
            int temp = 0;
            byte[] data = new byte[256];
            while((temp=br_file.read(data))!=-1){
                DOS.write(data, 0, temp);
            }
            DOS.flush();
            DOS.close();
            br_file.close();
            fs.close();
        }catch (Exception ex){
            System.out.println(ex);
        }finally{
            try{
                if(socket!=null) socket.close();
            }catch (Exception ex){}
        }

    }

    public static void send(Socket socket) throws IOException {
        //Person 객체 생성. 인자로 3 넣어줌.
        Person person = new Person(3);

        //생성한 person 객체를 byte array로 변환
        byte[] data = toByteArray(person);
        //서버로 내보내기 위한 출력 스트림 뚫음
        OutputStream os = socket.getOutputStream();
        //출력 스트림에 데이터 씀
        os.write(data);
        //보냄
        os.flush();
    }

    public static void receive_my(Socket socket) throws IOException {
        //수신 버퍼의 최대 사이즈 지정
        int maxBufferSize = 20;
        //버퍼 생성
        byte[] recvBuffer = new byte[maxBufferSize];
        //서버로부터 받기 위한 입력 스트림 뚫음
        InputStream is = socket.getInputStream();
        //버퍼(recvBuffer) 인자로 넣어서 받음. 반환 값은 받아온 size
        int nReadSize = is.read(recvBuffer);

        //System.out.println(recvBuffer);

        //받아온 값이 0보다 클때
        if (nReadSize > 0) {
            //받아온 byte를 Object로 변환
            //Person receivePerson = toObject(recvBuffer, Person.class);
            String data = new String(recvBuffer);
            //확인을 위해 출력
            System.out.println(data);
        }
    }

    public static void receive(Socket socket) throws IOException {
        //수신 버퍼의 최대 사이즈 지정
        int maxBufferSize = 256;
        //버퍼 생성
        byte[] recvBuffer = new byte[maxBufferSize];
        //서버로부터 받기 위한 입력 스트림 뚫음
        InputStream is = socket.getInputStream();
        //버퍼(recvBuffer) 인자로 넣어서 받음. 반환 값은 받아온 size
        int nReadSize = is.read(recvBuffer);

        //받아온 값이 0보다 클때
        if (nReadSize > 0) {
            //받아온 byte를 Object로 변환
            //Person receivePerson = toObject(recvBuffer, Person.class);
            String data = new String(recvBuffer);
            //확인을 위해 출력
            System.out.println(data);
        }
    }

    public static byte[] toByteArray (Object obj)
    {
        byte[] bytes = null;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(obj);
            oos.flush();
            oos.close();
            bos.close();
            bytes = bos.toByteArray();
        }
        catch (IOException ex) {
            //TODO: Handle the exception
        }
        return bytes;
    }

    public static <T> T toObject (byte[] bytes, Class<T> type)
    {
        Object obj = null;
        try {
            ByteArrayInputStream bis = new ByteArrayInputStream (bytes);
            ObjectInputStream ois = new ObjectInputStream (bis);
            obj = ois.readObject();
        }
        catch (IOException ex) {
            //TODO: Handle the exception
        }
        catch (ClassNotFoundException ex) {
            //TODO: Handle the exception
        }
        return type.cast(obj);
    }
}

class Person implements Serializable {
    int age;

    Person(int age) {
        this.age= age;
    }
}

 

 

 

이거는 여러 코드를 짜집기해서 헷갈릴 수 있지만 중요한 부분은 아래 부분이다. 

아래에서 쓰인 함수들만 사용하였고 send_Image(socket); 함수가 이미지를 보낼 때 쓰는 함수이다.

    void run() throws IOException {
        //소켓 생성
        Socket socket = new Socket();
        //서버쪽 주소 생성. ip는 localhost, 포트는 9999. 필요에 맞게 바꾸기
        SocketAddress address = new InetSocketAddress("127.0.0.1", 9999);
        //주소에 해당하는 서버랑 연결
        socket.connect(address);

        System.out.println("CONNECT OK");


        try {
            //받기
            receive_my(socket);
            System.out.println("recieve ok");
			
            //이미지 전송
            send_Image(socket);
        } catch (IOException e) {
            e.printStackTrace();
        }


    }

 

 

 

 

 

C SERVER

/* server.c */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void error_handling(char *message);

int main(int argc, char *argv[])
{
	int serv_sock;
	int clnt_sock;
	char buf[256];
	struct sockaddr_in serv_addr;
	struct sockaddr_in clnt_addr;
	socklen_t clnt_addr_size;

	char message[]="Hello World!";
	
	if(argc!=2){
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
	
	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	if(serv_sock == -1)
		error_handling("socket() error");
			
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family=AF_INET;
	serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_addr.sin_port=htons(atoi(argv[1]));
	
	if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1 )
		error_handling("bind() error"); 
	
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");
	
	clnt_addr_size=sizeof(clnt_addr);  
	clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size);
	if(clnt_sock==-1)
		error_handling("accept() error");  

	write(clnt_sock, message, sizeof(message));
	printf("test message send\n");
	int nbyte = 256;
    size_t filesize = 0, bufsize = 0;
    FILE *file = NULL;

    file = fopen("aurora.png"/* 새로 만들 파일 이름 */, "wb");

    bufsize = 256;

    while(/*filesize != 0*/nbyte!=0) {
        nbyte = recv(clnt_sock, buf, bufsize, 0);

        fwrite(buf, sizeof(char), nbyte, file);		
        //nbyte = 0;
    }
 

	fclose(file);
	close(clnt_sock);	
	close(serv_sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

 

우선 Hello World! 라는 문자를 보낸 후 파일을 수신 받는다.

파일을 수신 받은 후에는 aurora.png 파일에 저장한다.

 

 

 

작동

- 서버 실행

9999 포트로 서버를 실행시켰다.

 

java 코드를 실행시키면

 

 

 

더보기

java와 C가 통신하려면 Byte를 이용해야 한다.

EC2 인스턴트 생성

1. 계정생성

일단 AWS에 들어가서 계정을 생성한다.

이때 선택은 그냥 기본값으로 하면 된다.

주의할 점은 카드를 등록해야하는데 카드를 확인하기 위해서 100원이 결제됐다가 취소된다!

그 다음 [콘솔에 로그인] 버튼 누르면 된다.

 

 

2. 인스턴트 생성

위 사진처럼 나오면 [인스턴스 시작] 버튼 누르고

 

 

여기서는 프리 티어 사용 가능 마크 중에서 아무거나 선택하면 된다.

나는 Ubuntu 20.04를 선택해서 사용 중이다.

 

 

선택하면 여러 가지 창이 나오는데 여러 설정들을 진행할 수 있다. 단 잘못 설정할 경우 과금될 수 있기 때문에 조심!

내가 한 설정은 4. 스토리지 추가에서 크기를 30GB로 바꾸고 보안 그룹 구성을 바꿨다. 보안 그룹 구성은 나중에 바꿀 수 있다. 이 설명은 뒤에서 다시 언급할 예정이라 그냥 안바꾸고 진행해도 된다.

 

 

 

설정을 끝낸 후 [검토 및 시작] 버튼을 누르면 인스턴스가 생성된다.

아래 블로그들을 참고해서 진행했다.

 

https://brunch.co.kr/@topasvga/17

 

16.(시작) AWS무료 서버 1대 받기

AWS 가입하기, 서버 1대 받기 | 클라우드 서비스에서 서버를 1대 받아보자. 현재 제일 유명하고 편한 서비스가  AWS ,  아마존 웹서비스이다. 모든 게 무료는 아니다.몇만원이 나오는 경우가 있다.

brunch.co.kr

https://developer88.tistory.com/295

 

AWS FreeTier 와 사용량 확인하는 방법 #EC2 #RDS

AWS를 시작하게 되면서, 가장 먼저 알아두어야 할 것이 바로 프리티어와 그 사용량입니다. AWS는 1년동안 무료로 사용할 수 있는 양을 정해서, 그 양을 벗어나면 과금을 하고 있기 때문인데요. 초

developer88.tistory.com

 

 

GUI 설치

이렇게 시작하고 싶은 인스턴스에서 우클릭을 한 후 연결을 눌러서 실행하면 되는데 그냥 콘솔 창만 떠서 사용하기가 너무 어려웠다. 그래서 GUI를 설치했다. 

 

설치를 하려면 우선 putty랑 TightVNC Viewer가 필요하다.

https://mozi.tistory.com/191

 

[AWS] EC2 인스턴스 Putty 로 접속하기

리눅스 환경이라면, ssh -i <파일> <서버IP> 옵션을 줘서 간단하게 접속할 수 있습니다. 윈도우 환경에서는 그럴수 없으므로 Putty 를 사용하여 EC2 인스턴스에 접속합니다. 접속하는 방법을 알아보겠

mozi.tistory.com

putty는 이 블로그를 참고해서 설치하면 된다.

 

 

다른 과정들은 아래의 블로그에서 따라하면 된다.

https://medium.com/@ggomma/%EC%9C%88%EB%8F%84%EC%9A%B0%EC%97%90%EC%84%9C-ubuntu-aws-ec2-gui-%EC%9D%B4%EC%9A%A9-ee2567a85d8f

 

윈도우에서 Ubuntu (AWS EC2) GUI 이용

윈도우에서 AWS EC2에 올린 Ubuntu를 GUI로 접속할 일이 있었다.

medium.com

나도 이 블로그를 따라했는데 헷갈리는 부분이 몇개 있어서 그 부분만 추가적으로 포스팅해보고자 한다.

 

 

 

1. 우선 맨~처음 sudo apt-get update를 입력하는 창을 띄우는 방법은 시작하고자 하는 인스턴트에서 우클릭 -> 연결을 누른다.

 

 

2. IP 알아내기

1번과 같은 과정을 거치면 아래와 같은 창이 뜨는데 여기서 퍼블릭 IP 주소 라고 쓰인 게 PuTTY의 Destination에 입력해야 할 IP 주소이다.

 

 

3. 5091 포트 열어주기

1번 과정처럼 우클릭 -> 보안 -> 보안 그룹 변경 에 들어가면 보안 그룹 ID가 보일 것이다. 이걸 잘 기억해두고 (기억 못하면 다시 확인하면 된다) 뒤로가기 버튼을 눌러서 인스턴트 창으로 들어온다.

 

인스턴트 창 왼쪽에 메뉴들이 쭉 있을텐데 여기서 [네트워크 및 보안] -> [보안 그룹] 에 들어간다.

 

 

그러면 아래와 같은 창이 뜰텐데 아까 기억해뒀던 보안 그룹 ID를 선택해서 우클릭->인바운드 규칙 편집을 누른다.

 

 

이런 창이 뜨면 유형, 포트범위는 동일하게 설정하고 소스 부분은 하나는 Anywhere-IPv4, 하나는 Anywhere-IPv6로 하면 소스 옆 칸의 파란색 표시 (ex. 0.0.0.0/0)이 자동으로 생성된다. 이렇게 하고 규칙 저장 하면 5091 포트 열어주기 끝!

 

 

 

 

4. 마무리

마지막으로 TightVNC창을 열어야하는데 이때 주의할 점은 PuTTY에서 Load -> Open을 누르면 뜨는 콘솔창이 열려있어야 한다는 점!

콘솔창이 열린 상태에서 TightVNC 창을 열면 GUI가 열린다.

** zip 파일은 아직 못 올림 ** -> 추후 수정 예정 -> 11/3 올릴 수 있음

https://goodteacher.tistory.com/351

 

[SpringBoot]file upload /download 처리

SpringBoot에서의 file upload와 download 처리 웹 애플리케이션을 작성하다 보면 파일을 업로드 하거나 다운로드 하는 일은 매우 빈번한 일이다. 이번 포스트에서는 SpringBoot를 이용해서 file을 upload 및

goodteacher.tistory.com

참고한 블로그는 위 블로그다!

중간중간 생략된 부분이 있어서 그 부분을 추가해서 포스팅한다.

 

 

 

구축 환경

windows 10

IntellJ IDEA Community Edition 2021.2.2

JDK 11

 

 

초기 설정

https://start.spring.io/

우선 여기 들어가서 spring 파일을 다운받아야 한다.

 

위 사진처럼 설정한 후 GENERATE를 누르면 zip 파일이 다운로드 된다.

설정 시에 Group과 Artifact은 자유로 설정해도 된다.

또 Spring Boot 버전은 뒤에 영어가 없는 버전 중에서 제일 최신 버전을 다운받는다! 뒤에 괄호가 있고 영어가 써져있는 버전은 미배포 버전이다.

 

 

zip 파일을 압축 해제한 후 IntelliJ에서 열어준다.

 

 

 

IntelliJ

1. 초기 화면

초기 화면은 아래와 같다. 오른쪽 밑에 저런 창이 뜨면 Load를 눌러준다.

 

 

2. Setup SDK

기본 java 파일을 열면 아래 사진의 파란색 창이 뜨는데 Setup SDK를 눌러서 11을 선택해준다.

(java 선택 버전인듯)

 

 

 

3. 기타 setting

File -> Setting -> Build,Ex~ -> Gradle에서 Build and run using과 Run tests using을 다음과 같이 바꿔준다.

정확히는 잘 모르지만 빠르게 바꿔준다고 들었다!

 

 

 

코드 작성

0. 파일 경로

 

 

1. 파일 정보 설정

application.properies 에 다음과 같은 내용을 적어둔다. 파일이 업로드될 장소, 사이즈 등등을 설정하는 것이다.

spring.servlet.multipart.location=C:\\Temp\\upload
spring.servlet.multipart.max-file-size=10MB
spring.servlet.mulltipart.max-request-size=50MB

 

 

2. static -> index.html

기본 화면을 index.html을 이용해서 작성한다.

 

https://www.w3schools.com/css/tryit.asp?filename=trycss_default

 

Tryit Editor v3.7

body { background-color: lightblue; } h1 { color: white; text-align: center; } p { font-family: verdana; font-size: 20px; } My First CSS Example This is a paragraph.

www.w3schools.com

이 사이트를 이용하면 내가 쓴 html이 어떤 식으로 창에 띄워지는지 참고할 수 있다!

 

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title> Insert title here</title>
</head>
<body>
    <h1>file upload</h1>
    <form method="post" action="/upload" enctype="multipart/form-data">
        <input type="file" name="uploadfile" multiple="multiple">
        <input type="submit">
    </form>
</body>
</html>

 

요런 화면을 만드는 코드이다.

 

 

3. templates -> result.html

파일을 업로드하면 뜨는 화면이다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>upload fiile list</h1>
    <ul th:each="file: ${files}">
            <li ><span th:text="${file.fileName}"></span>
                <a href="#" th:href="${'/download?uuid='+file.uuid+'&fileName='+file.fileName}">[download]</a>
    </ul>
</body>
</html>

 

 

4. controller 작성

controller package를 만들어서 (폴더가 아니고 package!) uploadcontroller.java 파일을 만든 후 다음과 같은 코드를 작성한다.

 

코드를 약간만 설명하자면 html 파일에서 /upload 이렇게 입력하면

@PostMapping("/upload")와 짝지어진 부분이 실행된다.

실행을 마친 후 return에는 다음으로 갈 html 파일을 적어둔다.

 

"제출" 버튼을 누르면 upload 함수로, upload 함수가 처리되면 result 화면을 띄운다.

 

 

이 부분이 가장 어려웠다. 왜냐면 계속 whitelabel error page 404 가 떴기 때문이다..

어떤 부분이 문제인지 알기 위해 함수에다 log를 돌렸는데 log조차 뜨지 않았다. 아예 Controller가 먹통이었던 것이다.

그러면 package를 확인해야한다.

package가 아래처럼 경로가 되어 있어야지 인식이 된다.

나는 수정 전 package controller였다.

 

https://cceeun.tistory.com/183

 

[SpringBoot] Controller가 동작하지 않는 오류 | Mapping된 URL, Value를 찾지 못하는 경우 | @ComponentScan | Whit

스프링부트 프로젝트를 신규로 생성한 후에 static 경로에 index.html을 넣고 서버를 실행시키면 localhost:8080/ 주소에서 index.html 화면이 나온다. 그리고 각각 다른 화면들을 보여주기 위해 @GetMapping을

cceeun.tistory.com

위 블로그를 읽어보면 이해가 잘 될 것이다.

package hello.FileUpload.controller;

import hello.FileUpload.FileDto;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Controller
public class uploadcontroller {

    @PostMapping("/upload")
    public String upload(@RequestParam MultipartFile[] uploadfile, Model model)
            throws IllegalStateException, IOException{
        List<FileDto> list = new ArrayList<>();
        System.out.printf("------------------abc");
        for(MultipartFile file:uploadfile){
            if(!file.isEmpty()){
                System.out.printf("------------------abc");
                //UUID를 이용해 unique한 파일 이름을 만들어준다
                FileDto dto = new FileDto(UUID.randomUUID().toString(),
                        file.getOriginalFilename(),
                        file.getContentType());
                list.add(dto);

                File newFileName = new File(dto.getUuid()+"_"+dto.getFileName());
                //전달된 내용을 실제 물리적인 파일로 저장해준다.
                file.transferTo(newFileName);
            }
        }
        model.addAttribute("files",list);
        return "result";
    }

    @Value("${spring.servlet.multipart.location}")
    String filePath;

    @GetMapping("/download")
    public ResponseEntity<Resource> download(@ModelAttribute FileDto dto) throws IOException{
        Path path = Paths.get(filePath+"/"+dto.getUuid()+"_"+dto.getFileName());
        String contentType = Files.probeContentType(path);
        //header를 통해서 다운로드 되는 파일의 정보를 설정한다.
        HttpHeaders headers = new HttpHeaders();
        headers.setContentDisposition(ContentDisposition.builder("attachment").filename(dto.getFileName(), StandardCharsets.UTF_8).build());
        headers.add(HttpHeaders.CONTENT_TYPE,contentType);

        Resource resource = (Resource) new InputStreamResource(Files.newInputStream(path));
        return new ResponseEntity<>(resource,headers, HttpStatus.OK);
    }

}

 

 

5. 결과

(1.)에서 지정해준 경로에 웹페이지를 통해 업로드한 파일이 올라와있는 모습을 볼 수 있다.

BARAM_21년도_후반기_제안서_28기_19김다현.hwp
1.36MB
BARAM_21년도_후반기_제안서_28기_19김다현.pptx
0.75MB

 

+ Recent posts