728x90

자바에서 비동기 프로그래밍을 하기 위해 알아야 하는 Future 인터페이스에 대해 알아보자.

 

Method reference

:: 연산자를 이용해서 함수에 대한 참조를 간결하게 포현한 것이다.

package org.example;

import java.util.function.Consumer;
import java.util.stream.Stream;

public class Main {

    public static class Student {
        private final String name;

        public Student(String name) {
            this.name = name;
        }

        public boolean compareTo(Student student) {
            return student.name.compareTo(name) > 0;
        }

        public String getName() {
            return name;
        }
    }

    public static void print(String name) {
        System.out.println(name);
    }

    public static void main(String[] args) {
        var target = new Student("f");

        Consumer<String> staticPrint = Main::print;

        Stream.of("a", "b", "k", "z")
                .map(Student::new)
                .filter(target::compareTo)
                .map(Student::getName)
                .forEach(staticPrint);
    }
}

위의 코드를 예시로 들면

method reference: target::compareTo

static method reference: Main::print

instance method reference: Student::getName

constructor method reference: Student::new

이렇게 해당된다.

 

ExecutorService

쓰레드 풀을 이용하여 비동기적으로 작업을 실행하고 관리해준다.

쓰레드를 생성하고 관리하는 작업이 필요하지 않기 때문에, 코드를 간결하게 유지가 가능하다.

public interface ExecutorService extends Executor {
    void shutdown();

    <T> Future<T> submit(Callable<T> task);

    void execute(Runnable command);
}

이렇게 구성이 되어 있으며 각 메서드는 다음과 같이 동작한다.

execute: Runnable 인터페이스를 구현한 작업을 쓰레드 풀의 쓰레드에서 비동기적으로 실행한다.

submit: Callable 인터페이스를 구현한 작업을 쓰레드 풀에서 비동기적으로 실행하고, 해당 작업의 결과를 Future<T> 객체로 반환한다.

shutdown: ExecutorService를 종료하며, 더 이상의 task를 받지 않는다.

 

Executors를 사용하여 ExecutorService를 생성한다.

  • newSingleThreadExecutor: 단일 쓰레드로 구성된 쓰레드 풀을 생성, 한 번에 하나의 작업만 실행
  • newFixedThreadPool: 고정된 크기의 쓰레드 풀을 생성. 크기는 인자로 주어진 n과 동일
  • newCachedThreadPool: 사용가능한 쓰레드가 없다면 생성, 있다면 재사용하며 쓰레드가 일정시간 사용되지 않으면 회수
  • newScheduledThreadPool: 스케줄링 기능을 갖춘 고정 크기의 쓰레드 풀을 생성. 주기적이거나 지연이 발생하는 작업을 실행
  • newWorkStealingPool: work steal 알고리즘을 사용하는 ForkJoinPool을 생성

 

 

Future

이제 Future 인터페이스에 대하여 자세히 살펴보자.

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

Future 인터페이스는 이렇게 구성되어 있다.

 

  • isDone: task가 완료되었다면, true 반환
  • isCancelled: task가 cancel에 의해 취소된 경우, true 반환
  • get: 결과를 구할 때까지 thread가 계속 block(future가 오래 걸린다면 thread가 blocking 상태 유지), 이 문제를 해결하기 위해 timeout의 인자를 받는 메서드가 존재
  • cancel: future의 작업을 취소하며, 취소할 수 없는 상황이면 false를 반환한다. mayInterruptIfRunning이 false라면 시작하지 않은 작업만 취소

 

이렇게 Future에 대해서 알아보았는 데, cancel로 정지시키는 거 말고는 future를 컨트롤 할 수가 없다.

또한 반환된 결과를 get으로 기다린 후 접근하기 때문에 비동기로 작업하기가 어렵다.

728x90

https://seungkyu-han.tistory.com/109

 

동기, 비동기란 무엇일까?

스프링을 공부하기 전에 항상 나오는 말이 있다. 스프링은 동기, node 서버는 비동기라는 말을 굉장히 많이 들었던 것 같다. 처음에 공부할 때는 외우고만 있다가 해당 내용을 운영체제에서 공부

seungkyu-han.tistory.com

저번에 작성한 동기, 비동기와 연결이 되는 글이다.

 

앞서 작성했던 프로그램 A와 프로그램 B의 공통점은 둘 다 호출하는 main 함수가 getValue 함수가 시행되는 동안 Blocking 상태가 된다는 것이다.

각각 프로그램이 실행되는 모습을 그려보면 다음과 같다.

프로그램 A
프로그램 B

 

모두 메인 함수가 Blocking 되는 것을 볼 수 있다.

그렇기 때문에 프로그램 A는 동기이면서 Blocking, 프로그램 B는 비동기이면서 Non-blocking 프로그램이다.

 

그럼 과연 이런 모델들만 있을까?

물론 아니다.

다음 프로그램들을 보자.

package org.example;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ProgramC {

    public static void myLog(String string){
        System.out.println(Thread.currentThread() + " -> " +string);
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        myLog("Start main");

        var count = 1;
        Future<Integer> result = getValue();
        while (!result.isDone()){
            myLog("Waiting for result, count: " + count++);
            Thread.sleep(1000);
        }

        var nextValue = result.get() + 1;
        myLog(String.valueOf(nextValue == 1));

        myLog("Finish main");
    }

    public static Future<Integer> getValue(){
        var executor = Executors.newSingleThreadExecutor();
        try{
            return executor.submit(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    myLog("Start getValue");
                    try{
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }

                    var result = 0;
                    try {
                        return result;
                    }finally {
                        myLog("Finish getResult");
                    }
                }
            });
        }finally {
            executor.shutdown();
        }
    }
}

 

해당 코드에서는 준비가 되기 전까지 계속 메인에서 코드를 실행하게 된다.

 

그리고 결과를 보면 메인 스레드 외에도 다른 스레드가 실행되는 것을 볼 수 있다.

 

프로그램 C

메인 스레드는 getValue 스레드가 동작하는 중간에도 계속 동작하는 것을 볼 수 있다.

 

해당 프로그램은 동기이면서 Non-blocking 프로그램이다.

 

마지막으로 비동기이면서 Non-blocking 프로그램은 다음과 같을 것이다.

caller 스레드는 반환값이 필요가 없기에 바로 종료될 것이며, callee 본인의 일만 본인의 스레드에서 하게 될 것이다.

package org.example;

import java.util.concurrent.Executors;

public class ProgramD {
    public static void myLog(String string){
        System.out.println(Thread.currentThread() + " -> " +string);
    }

    public static void main(String[] args) {
        myLog("Start main");
        getValue(new Function<Integer>() {
            @Override
            public void accept(Integer integer) {
                var nextValue = integer + 1;
                myLog(String.valueOf(nextValue == 1));
            }
        });
        myLog("Finish main");
    }

    public static void getValue(Function<Integer> callback){
        var executor = Executors.newSingleThreadExecutor();
        try{
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    myLog("Start getValue");
                    try{
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }

                    var value = 0;
                    try{
                        callback.accept(value);
                    }finally {
                        myLog("Finish getResult");
                    }
                }
            });
        }finally {
            executor.shutdown();
        }
    }
}

 

이렇게 메인 스레드는 시작하자마자 종료가 되고, 남은 스레드 혼자 실행이 되고 있는 것을 볼 수가 있다.

728x90

스프링을 공부하기 전에 항상 나오는 말이 있다.

스프링은 동기, node 서버는 비동기라는 말을 굉장히 많이 들었던 것 같다.

처음에 공부할 때는 외우고만 있다가 해당 내용을 운영체제에서 공부하고 그때서야 이해할 수 있게 되었다.

 

이번에도 공부하기 전에 동기와 비동기에 대해서 정리하고 가고자 한다.

 

Caller와 Callee

프로그래밍에서 굉장히 많이 봤던 용어일 것이다.

caller는 말 그대로 다른 함수를 호출하는 함수이고, callee는 그때에 호출당하는 함수이다.

 

함수형 인터페이스

1개의 추상 메서드를 가지고 있는 인터페이스 함수를 1급 객체로 사용하는 것을 말한다.

자바에서는 람다 표현식을 이 함수형 인터페이스에서만 사용 가능하다.

package org.example;

@FunctionalInterface
public interface Function<T> {

    void accept(T t);
}

 

해당 함수형 인터페이스를 사용해보도록 하자.

package org.example;

public class Main {

    public static void main(String[] args) {
        var function = getFunction();
        function.accept(1);

        var functionAsLambda = getFunctionAsLambda();
        functionAsLambda.accept(1);

        functionHandler(function);
    }

    public static Function<Integer> getFunction(){
        Function<Integer> returnValue = new Function<Integer>() {
            @Override
            public void accept(Integer integer) {
                myLog("value in interface: " + integer);
            }
        };
        return returnValue;
    }

    public static Function<Integer> getFunctionAsLambda(){
        return integer -> myLog("value in lambda: " + integer);
    }

    public static void functionHandler(Function<Integer> function){
        myLog("functionHandler");
        function.accept(1);
    }


    public static void myLog(String string){
        System.out.println(Thread.currentThread() + " -> " +string);
    }
}

 

이 코드를 실행해보면 

 

이때 모두 메인 스레드에서 실행이 되는 것을 볼 수 있다.

 

함수 호출 관점

 

A 프로그램

package org.example;

public class programA {

    public static void myLog(String string){
        System.out.println(Thread.currentThread() + " -> " +string);
    }

    public static void main(String[] args) {
        myLog("start main");
        var value = getValue();
        var nextValue = value + 1;
        myLog(String.valueOf(value == nextValue));
        myLog("Finish main");
    }

    public static int getValue(){
        myLog("start getValue");
        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){
            throw new RuntimeException(e);
        }

        var value = 0;
        try{
            return value;
        }finally {
            myLog("Finish getResult");
        }
    }
}

 

이 모델의 실행을 그림으로 표현하면 다음과 같다.

main 함수는 getValue의 실행이 끝날 때까지 기다린 후 해당 데이터를 가지고 작업을 한다.

 

이것과는 다른 B 프로그램이다.

package org.example;

public class programB {

    public static void myLog(String string){
        System.out.println(Thread.currentThread() + " -> " +string);
    }

    public static void main(String[] args) {
        myLog("start main");
        getValue(integer -> {
            var nextValue = integer + 1;
            myLog(String.valueOf(nextValue == 1));
        });

        myLog("Finish main");
    }

    public static void getValue(Function<Integer> function){
        myLog("Start getValue");
        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){
            throw new RuntimeException(e);
        }

        var value = 0;
        function.accept(value);
        myLog("Finish getValue");
    }
}

 

해당 프로그램은 main에서 직접 실행하는 것이 아니라 getValue에서 실행하도록 한다.

 

이 프로그램을 그림으로 나타내면 다음과 같다.

 

두 프로그램의 차이점은
A 프로그램은 다음 코드를 위해 callee의 반환값이 필요하다.

B 프로그램은 다음 코드를 위해 callee의 반환값이 필요하지 않고 callee가 결과를 이용해서 callback을 수행한다.

 

이러면 A 프로그램을 동기, B 프로그램을 비동기라고 생각을 하면 된다.

 

728x90

TCP 기반으로 서버와 클라이언트를 작성해보자.

 

TCP로 설정하기 위해서는 socket함수에 SOCK_STREAM의 값을 넣어주면 된다.

TCP는 데이터 전송 중에 확인 과정을 거치기 때문에 상대방이 받지 못했다면 일정 시간 후에 재 전송함으로 신뢰성을 보장한다.

이 부분은 앞에서도 많이 말했기 때문에 쉽게 기억할 것이다.

 

이제 TCP 서버가 기본적으로 호출하는 함수들을 보자.

 

Server

socket() -> bind() -> listen() -> accept() -> read()/write() -> close()

 

socket(): 소켓을 생성한다.

bind(): 생성한 소켓에 주소를 할당한다.

listen(): 해당 소켓을 연결 가능 상태로 만든다.

accept(): 연결 요청이 오면 해당 연결을 허용해준다.

read()/write(): 해당 함수를 이용하여 데이터를 송수신한다.

close(): 연결을 종료한다.

 

하나씩 더 자세히 살펴보자.

 

listen

#include <sys/type.h>

int listen(int sock, int backlog);

- sock

연결 가능 상태로 만들고자 하는 소켓의 파일 디스크립터

- backlog

연결요청 대기 큐의 크기, 만약 5를 전달한다면 클라이언트의 연결 요청을 5개까지 대기시킬 수 있다.

 

accept

#include <sys/socket.h>

int accept(int sock, struct sockaddr * addr, socklen_t * addrlen);

- sock

서버 소켓의 파일 디스크립터

- addr

연결 요청이 온 클라이언트의 주소 정보를 담을 구조체

- addrlen

addr에 전달된 주소의 변수 크기를 바이트 단이로 전달한다. 크기 정보를 변수에 저장한 후에 변수의 주소값을 저장하게 된다.

return: 생성된 소켓의 파일 디스크립터, -1(fail)

 

새로운 소켓을 하나 더 생성하고 그 소켓을 이용해서 데이터의 송수신을 진행한다.

 

 

Client

이번에는 listen과 accept를 사용하지 않는 클라이언트를 확인해보자.

socket() -> connect() -> read()/write() -> close()

 

socket(): 소켓을 생성한다.

connect(): 서버에 연결을 요청한다.

read()/write(): 연결된 서버와 데이터를 송수신한다.

close(): 소켓을 닫는다.

 

당연히 connect는 서버에서 listen 상태이어야 가능하다.

 

이 함수들을 이용하여 여러 클라이언트들과 통신하는 iterative 서버를 만들어보자.

#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;
    int str_len;

    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size;

    char message[] = "Hello, I'm Seungkyu";

    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");
    

    for(int i = 0; i < 5; i++){
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
        if(clnt_sock == -1) error_handling("accept() error");
        printf("Connected client %d, client_sock: %d\n", i + 1, clnt_sock);

        while((str_len = read(clnt_sock, message, BUFSIZ)) != 0) write(clnt_sock, message, str_len);

        close(clnt_sock);
        printf("close client socket: %d\n", clnt_sock);
    }

    close(serv_sock);
    return 0;
}

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

이렇게 서버에서 반복문으로 5번까지  accept가 가능하게 만들어준다.

 

#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 sock;
    struct sockaddr_in serv_addr;
    char message[30];
    int str_len;

    if(argc!=3){
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    sock = socket(PF_INET, SOCK_STREAM, 0);
    if(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=inet_addr(argv[1]);
    serv_addr.sin_port=htons(atoi(argv[2]));

    if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) error_handling("connect() error!");

    while(1) {
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, BUFSIZ, stdin);
        if(!strcmp(message,"q\n") || !strcmp(message,"Q\n")) break;
        write(sock, message, strlen(message));
        str_len=read(sock, message, BUFSIZ-1);
        message[str_len]=0;
        printf("Message from server: %s", message);
    }
    close(sock);
    return 0;
}

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

이렇게 하면 해당 클라이언트를 5번까지 받아 줄 수 있다.

 

728x90

3주차에는 주소체계와 TCP 기반 서버/클라이언트에 대하여 공부한다.

 

  • 인터넷 주소

인터넷 주소는 당연히 인터넷 상에서 컴퓨터끼리 구분하기 위해 사용되는 주소이다.

4바이트 주소체계인 IPv4와 16바이트 주소체계인 IPv6가 존재한다.

네트워크 주소와 호스트 주소로 나뉘는데, 이름 그대로 네트워크 주소를 이용해서 네트워크를 찾고 호스트 주소를 이용해서 해당 네트워크에서 호스트를 검색한다.

클래스 A 1 Byte (네트워크 ID) 1 Byte(호스트 ID) 1 Byte(호스트 ID) 1 Byte(호스트 ID)
클래스 B 1 Byte (네트워크 ID) 1 Byte (네트워크 ID) 1 Byte(호스트 ID) 1 Byte(호스트 ID)
클래스 C 1 Byte (네트워크 ID) 1 Byte (네트워크 ID) 1 Byte (네트워크 ID) 1 Byte(호스트 ID)
클래스 D 멀티캐스트 IP 주소

 

클래스 A의 첫 번재 바이트 범위는 0이상     127이하  -> 클래스 A의 첫 번째 비트는 항상 0으로 시작

클래스 B의 첫 번째 바이트 범위는 128이상 191이하  -> 클래스 B의 첫 두 비트는 항상 10으로 시작

클래스 C의 첫 번째 바이트 범위는 192이상 223이하 -> 클래스 C의 첫 세 비트는 항상 110으로 시작

 

첫 번째 바이트 정보만 참조해도 IP주소의 클래스 구분이 가능하므로, 네트워크 주소와 호스트 주소의 경계를 구분할 수 있다.

 

  • Port

IP는 컴퓨터끼리 구분하기 위해 사용된다.

그러면 한 컴퓨터 내의 프로그램끼리는 어떻게 구분을 할까?

바로 Port를 사용해서 구분한다.

Port 번호는 소켓을 구분하는 용도로 사용이 된다.

둘 이상의 포트가 하나의 프로그램에 할당될 수도 있기는 하다.

2의 16제곱인 0 ~ 65535의 수를 포트로 지정을 할 수 있지만, 그 중에서 0 ~ 1023은 이미 용도가 지정되어 있는 포트들이라 사용을 피하는 것이 좋다.

 

  • 주소 표현을 위한 구조체
struct sockaddr_in {
	__uint8_t       sin_len;
	sa_family_t     sin_family;
	in_port_t       sin_port;
	struct  in_addr sin_addr;
	char            sin_zero[8];
};

sin_family: 주소체계 정보를 저장한다.

주소체계 의미
AF_INET IPv4 인터넷 프로토콜
AF_INET6 IPv6 인터넷 프로토콜
AF_LOCAL 로컬 통신을 위한 유닉스 프로토콜의 주소체계

sin_port: 16비트 Port 번호를 저장한다.

 

sin_addr: 32비트의 IP주소 정보를 저장한다.

in_addr 구조체는 그냥 32비트 정수 자료형이다.

 

sin_zero: 특별한 의미를 지니지 않으며, 0으로 채운다.

struct sockaddr_in의 크기를 struct_sockaddr에 맞춰주기 위해 만든 멤버이다.

 

이 sockaddr_in은 bind 함수의 인자로 전달을 하는데 bind 함수의 매개변수 타입은 sockaddr이기 때문에 형 변환이 필요하다.

sockaddr도 같은 16바이트이다.

구조체를 살펴보면

struct sockaddr {
	__uint8_t       sa_len;         /* total length */
	sa_family_t     sa_family;      /* [XSI] address family */
	char            sa_data[14];
};

이렇게 작성이 되어 있다.

sockaddr에 IPv4의 정보를 담기가 불편해서 동일한 바이트 크기를 가지는 sockaddr_in 구조체를 정의하고 sin_zero를 통해서 구조체의 크기를 맞춘 것이다.

 

  • 바이트 순서와 네트워크 바이트 순서

컴퓨터구조 시간에 빅 엔디안과 리틀 엔디안에 대해서 공부 했을 것이다.

 

- 빅 엔디안(Big Endian)

상위 바이트의 값을 작은 번지수에 저장하는 방식

 

- 리틀 엔디안(Little Endian)

상위 바이트의 값을 큰 번지수에 저장하는 방식

 

우리가 쓰는 컴퓨터가 빅 엔디안을 쓸 수도 있고, 리틀 엔디안을 쓸 수도 있다.

하지만 네트워크 바이트 순서는 빅 엔디안이 기준이다.

데이터를 전송 할 때는 바이트 단위로 데이터를 전송하기 때문에 상관 없지만 sockaddr_in 구조체의 내용을 전송할 때는 빅 엔디안으로 바꾸어 주어야 한다.

 

그럴 때는 아래의 함수들을 사용한다.

#include <stdint.h>
#include <netinet/in.h>

unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);

h는 호스트를 의미하고, n은 네트워크를 의미한다.

htons를 예로 설명하면 호스트에서 네트워크로 short를 변경한다는 의미이다.

만약 현재 내 컴퓨터가 Big Endian 컴퓨터라면 원래의 값을 리턴해주기 때문에 본인의 컴퓨터를 신경쓰지 않고 저 코드들을 사용해주면 된다.

 

#include <stdio.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]){
    
    unsigned short host_port = 0x1234;
    unsigned short net_port;
    unsigned long host_addr = 0x12345678;
    unsigned long net_addr;

    net_port = htons(host_port);
    net_addr = htons(host_addr);

    printf("Host ordered port: %#x \n", host_port);
    printf("Network ordered port : %#x \n", net_port);
    printf("Host ordered address: %#lx \n", host_addr);
    printf("Network ordered address: %#lx \n", net_addr);

    return 0;
}

해당 코드를 실행해보았을 때

Host ordered port: 0x1234 
Network ordered port : 0x3412 
Host ordered address: 0x12345678 
Network ordered address: 0x7856

이런 식으로 순서가 바뀌어 출력되게 된다면 해당 컴퓨터는 리틀 엔디안임을 의미한다.

 

우리는 IP주소를 적을 때 111.111.111.111 처럼 문자열로 작성을 하게 된다.

이렇게만 작성을 하고 우리는 수로 변환해주지 않았는 데, 이 때 inet_addr() 함수를 사용하게 된다.

#include <arpa/inet.h>

in_addr_t inet_addr(const char * string);

해당 문자열을 32비트 정수형으로 변환해준다.

 

#include <stdio.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]){
    char *addr1 = "111.111.111.111";
    char *addr2 = "123.123.1.1";

    unsigned long conv_addr = inet_addr(addr1);
    if(conv_addr == INADDR_NONE) printf("Error!!!\n");
    else printf("Network ordered integer addr:  %#lx\n", conv_addr);

    conv_addr = inet_addr(addr2);
    if(conv_addr == INADDR_NONE) printf("Error!!!\n");
    else printf("Network ordered integer addr:  %#lx\n", conv_addr);

    return 0;
}

이렇게 코드를 실행해보면

Network ordered integer addr:  0x6f6f6f6f
Network ordered integer addr:  0x1017b7b

우리가 필요로 하는 32비트로 값을 얻게 된다.

 

이번에는 반환하는 것이 아니라 구조체에 저장하는 함수이다.

#include <arpa/inet.h>

int inet_aton(const char * string, struct in_addr * addr);

-string

변환할 IP 주소 정보를 담고 있는 문자열

- addr

변환된 정보를 저장할 in_addr 구조체 변수의 주소 값 전달

return: 1(success), 0(fail)

 

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>

void error_handling(char *message);

int main(int argc, char *argv[]){
    struct sockaddr_in addr_inet;

    if(inet_aton(argv[1], &addr_inet.sin_addr)) printf("Network ordered integer addr: %#x\n", addr_inet.sin_addr.s_addr);
    else error_handling("Conversion error");

    return 0;
}

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

이 코드를 사용해보면 &addr_inet에 데이터가 들어가게 된다.

 

이번에는 반대로 정수형태를 문자열로 바꾸는 함수이다.

#include <arpa/inet.h>

char *inet_ntoa(struct in_addr adr);

- adr

변환할 정수 값

return: 변환된 문자열의 주소 값, -1(fail)

#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]){
    struct sockaddr_in addr1, addr2;
    char *str_ptr;
    char str_arr[20];

    addr1.sin_addr.s_addr=htonl(0x1020304);
    addr2.sin_addr.s_addr=htonl(0x1010101);

    str_ptr = inet_ntoa(addr1.sin_addr);
    strcpy(str_arr, str_ptr);
    printf("Dotted-Decimal notation1: %s \n", str_ptr);

    str_ptr = inet_ntoa(addr2.sin_addr);
    strcpy(str_arr, str_ptr);
    printf("Dotted-Decimal notation2: %s \n", str_ptr);

    return 0;
}

실행해보면 문자열로 출력이 되는 것을 볼 수 있다.

 

이제 우리가 인터넷 주소를 초기화 했던 코드들을 살펴보도록 하자.

struct sockaddr_in addr;
//IP 주소를 문자열로 선언, 보통은 받아옴
char *serv_ip = "111.111.111.111";
//PORT 번호를 문자열로 선언, 이것도 보통은 받아옴
char *serv_port = "1204";
//memset을 이용하여 현재 구조체의 모든 값을 0으로 초기화
memset(&addr, 0, sizeof(addr));
//주소 체계를 지정, 보통은 IPV4로 지정
addr.sin_family = AF_INET;
//문자열로 작성된 IP 주소를 입력
addr.sin_addr.s_addr = inet_addr(serv_ip);
//문자열로 작성된 port 번호를 입력
addr.sin_port=htons(atoi(serv_port));

이제 주소 초기화 한 함수들을 이해할 수 있을 것이다.

 

728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -

www.inflearn.com

 

저번에 만들었던 서블릿들을 이제 스프링 부트에 맞게 바꾸어 보도록 하자.

스프링은 기존에 작성했던 서블릿과는 다르게 @annotation을 많이 활용한다.

 

우선 저번에 만들었던 Form을 이렇게 수정했다.

package hello.servlet.web.springmvc.version1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class SpringStudentFormControllerV1 {

    @RequestMapping("/springmvc/v1/students/new-form")
    public ModelAndView process(){
        return new ModelAndView("new-form");
    }
}

@Controller를 달아서 스프링에서 컨트롤러로 인식하도록 한다, 그리고 메서드에 @RequestMapping으로 URL을 mapping 한다.

return은 저번에 작성했던 것과 비슷하게 ModelAndView 클래스에 생성자로 뷰 path를 넘겨주면 된다.

 

Save 부분도

package hello.servlet.web.springmvc.version1;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class SpringStudentSaveControllerV1 {

    private StudentRepository studentRepository = StudentRepository.getInstance();

    @RequestMapping("/springmvc/v1/students/save")
    public ModelAndView process(HttpServletRequest request, HttpServletResponse response){
        String studentName = request.getParameter("studentName");
        int year = Integer.parseInt(request.getParameter("year"));
        
        Student student = new Student(studentName, year);
        studentRepository.save(student);
        
        ModelAndView modelAndView = new ModelAndView("save-result");
        modelAndView.addObject("student", student);
        return modelAndView;
    }
}

이렇게 Mapping을 해주고 ModelAndView를 사용해서 Path 입력해주고 객체를 넣어주면 된다.

 

List도 같은 방식으로 작성한다.

package hello.servlet.web.springmvc.version1;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

@Controller
public class SpringStudentListControllerV1 {

    private StudentRepository studentRepository = StudentRepository.getInstance();

    @RequestMapping("/springmvc/v1/students")
    public ModelAndView process(){
        List<Student> students = studentRepository.findAll();

        ModelAndView modelAndView = new ModelAndView("students");
        modelAndView.addObject("students", students);
        return modelAndView;
    }
}

 

이제 여기서 리펙토링을 해보도록 하자.

어차피 mapping은 메서드에 하나씩 들어가게 된다.

그러면 한 컨트롤러에 작성할 수도 있지 않을까?

 

물론 된다.

한 컨트롤러에 작성하고 다른 주소들을 Mapping 해주면 된다.

package hello.servlet.web.springmvc.version2;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

@Controller
@RequestMapping("/springmvc/v2/students")
public class SpringStudentControllerV2 {

    private StudentRepository studentRepository = StudentRepository.getInstance();

    @RequestMapping("/new-form")
    public ModelAndView newForm(){
        return new ModelAndView("new-form");
    }

    @RequestMapping("/save")
    public ModelAndView save(HttpServletRequest request, HttpServletResponse response){
        String studentName = request.getParameter("studentName");
        int year = Integer.parseInt(request.getParameter("year"));
        
        Student student = new Student(studentName, year);
        studentRepository.save(student);
        
        ModelAndView modelAndView = new ModelAndView("save-result");
        modelAndView.addObject("student", student);
        return modelAndView;
    }
    
    @RequestMapping
    public ModelAndView students(){
        List<Student> students = studentRepository.findAll();
        
        ModelAndView modelAndView = new ModelAndView("students");
        modelAndView.addObject("students", students);
        return modelAndView;
    }
}

이렇게 메서드들의 이름만 분리해서 작성할 수 있다.

 

이제 마지막으로 @RequestParam까지 이용해보자.

이걸 이용하면 request에서 getParam을 쓸 필요가 없어진다.

바로 파라미터에서 가져올 수 있게 된다.

package hello.servlet.web.springmvc.version3;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@Controller
@RequestMapping("/springmvc/v3/students")
public class SpringStudentControllerV3 {

    private StudentRepository studentRepository = StudentRepository.getInstance();

    @GetMapping("/new-form")
    public String newForm(){
        return "new-form";
    }

    @PostMapping("/save")
    public String save(
            @RequestParam("studentName") String studentName,
            @RequestParam("year") int year,
            Model model){
        Student student = new Student(studentName, year);
        studentRepository.save(student);

        model.addAttribute("student", student);
        return "save-result";
    }

    @GetMapping
    public String students(Model model){
        List<Student> students = studentRepository.findAll();
        model.addAttribute("students", students);
        return "students";
    }
}

이렇게 ModelAndView를 리턴하는게 아니라 String을 리턴하면 자동으로 해당 Path를 찾아가게 된다.

Mapping에 Get, Post를 설정할 수 있으며 Model에 값을 추가하여 ModelAndView처럼 동작하게 할 수 있다.

'Spring > 스프링' 카테고리의 다른 글

스프링 16일차  (0) 2023.03.29
스프링 15일차  (0) 2023.03.28
스프링 14일차  (0) 2023.03.26
스프링 13일차  (0) 2023.03.25
스프링 12일차  (0) 2023.02.15
728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -

www.inflearn.com

 

저번에 만들었던 서블릿에 컨트롤러를 추가해보자.

 

현재 만들어둔 컨트롤러는 공통된 부분이 많고 하는 일도 비슷하다.

그럴 때는 FrontController를 생성해준다.

이 FrontController가 요청을 받고 그 요청에 맞는 컨트롤러를 호출해주는 역할을 한다.

 

우선 프론트 컨트롤러 자체를 도입해보자.

우선 다형성을 이용해 컨트롤러 인터페이스를 만들어보자.

package hello.servlet.web.frontcontroller.version1;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public interface ControllerV1 {
    void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

이렇게 만들고 각 컨트롤러들이 이 인터페이스를 구현하게 해주면 요청에 따른 컨트롤러는 모두 준비가 된다.

 

이제 FrontController를 만들어준다.

package hello.servlet.web.frontcontroller.version1;

import hello.servlet.web.frontcontroller.version1.controller.StudentFormControllerV1;
import hello.servlet.web.frontcontroller.version1.controller.StudentListControllerV1;
import hello.servlet.web.frontcontroller.version1.controller.StudentSaveControllerV1;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {
    private Map<String, ControllerV1> controllerMap = new HashMap<>();
    
    public FrontControllerServletV1(){
        controllerMap.put("/front-controller/v1/students/new-form", new StudentFormControllerV1());
        controllerMap.put("/front-controller/v1/students/save", new StudentSaveControllerV1());
        controllerMap.put("/front-controller/v1/students", new StudentListControllerV1());
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ControllerV1 controller = controllerMap.get(req.getRequestURI());
        if(controller == null){
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        
        controller.process(req, resp);
    }
}

이렇게 Map에 컨트롤러들을 담아준 후 URI를 확인하여 거기에 맞는 컨트롤러로 보내준다.

 

이제 계속 리팩토링을 진행해보자.

모든 컨트롤러에서 중복되는 부분이 있다.

이 부분을 모아주어야 깔끔하고, 후에 수정하기도 쉽다.

이런 상태에서 중간에 MyView를 추가하여 dispatcher.forward()를 맡길 예정이다.

package hello.servlet.web.frontcontroller;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public class MyView {

    private String viewPath;

    public MyView(String viewPath){
        this.viewPath = viewPath;
    }

    public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewPath);
        requestDispatcher.forward(request, response);
    }
}

이렇게 생성자로 viewPath를 입력받아서 render를 이용하여 jsp로 넘겨준다.

 

나머지 코드들은 저 부분을 빼서 다시 작성한다고 생각하면 된다.

package hello.servlet.web.frontcontroller.version2;

import hello.servlet.web.frontcontroller.MyView;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public interface ControllerV2 {

    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
package hello.servlet.web.frontcontroller.version2.controller;

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.version2.ControllerV2;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public class StudentFormControllerV2 implements ControllerV2 {

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        return new MyView("/WEB-INF/views/new-form.jsp");
    }
}
package hello.servlet.web.frontcontroller.version2.controller;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.version2.ControllerV2;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public class StudentSaveControllerV2 implements ControllerV2 {

    private StudentRepository studentRepository = StudentRepository.getInstance();

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String studentName = request.getParameter("studentName");
        int year = Integer.parseInt(request.getParameter("year"));

        Student student = new Student(studentName, year);
        studentRepository.save(student);

        request.setAttribute("student", student);

        return new MyView("/WEB-INF/views/save-result.jsp");
    }
}
package hello.servlet.web.frontcontroller.version2.controller;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.version2.ControllerV2;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.List;

public class StudentListControllerV2 implements ControllerV2 {

    private StudentRepository studentRepository = StudentRepository.getInstance();

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Student> students = studentRepository.findAll();
        request.setAttribute("students", students);

        return new MyView("/WEB-INF/views/students.jsp");
    }
}

그러고 FrontController는 이렇게 작성을 해준다.

package hello.servlet.web.frontcontroller.version2;

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.version2.controller.StudentFormControllerV2;
import hello.servlet.web.frontcontroller.version2.controller.StudentListControllerV2;
import hello.servlet.web.frontcontroller.version2.controller.StudentSaveControllerV2;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {

    private Map<String, ControllerV2> controllerMap = new HashMap<>();

    public FrontControllerServletV2(){
        controllerMap.put("/front-controller/v1/students/new-form", new StudentFormControllerV2());
        controllerMap.put("/front-controller/v1/students/save", new StudentSaveControllerV2());
        controllerMap.put("/front-controller/v1/students", new StudentListControllerV2());
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ControllerV2 controller = controllerMap.get(req.getRequestURI());
        if(controller == null){
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        MyView view = controller.process(req, resp);
        view.render(req, resp);
    }
}

request와 response에 데이터를 받고 view로 경로를 받은 후에 실행해주는 것이다.

 

다음 리펙토링이다.

컨트롤러에서는 request와 response가 꼭 필요하지 않다.

어차피 데이터를 넘길거면 담아서 넘기는 게 아니라 그냥 넘기면 되기 때문이다.

그렇기 때문에 컨트롤러에서 서블릿 기술을 제외해보자.

 

스프링처럼 Model을 만들어 데이터를 넘기도록 한다.

이렇게 Controller에서 Model에 데이터만 담아서 가져오는 것이다.

package hello.servlet.web.frontcontroller;

import lombok.Getter;
import lombok.Setter;

import java.util.HashMap;
import java.util.Map;

@Getter
@Setter
public class ModelView {
    
    private String viewName;
    private Map<String, Object> model = new HashMap<>();

    public ModelView(String viewName, Map<String, Object> model) {
        this.viewName = viewName;
    }
}

이렇게 Map으로 key, value로 데이터들을 저장하도록 만든다.

그러고 경로를 저장하기 위해서 viewName을 사용한다.

 

이제 ModelView를 리턴하도록 코드들을 바꾸어보자.

package hello.servlet.web.frontcontroller.version3;

import hello.servlet.web.frontcontroller.ModelView;

import java.util.Map;

public class StudentFormControllerV3 implements ControllerV3{

    @Override
    public ModelView process(Map<String, String> paramMap) {
        return new ModelView("new-form");
    }
}

이렇게 Model에 jsp의 경로와 데이터들을 넣어준다.

package hello.servlet.web.frontcontroller.version3;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import hello.servlet.web.frontcontroller.ModelView;

import java.util.Map;

public class StudentSaveControllerV3 implements ControllerV3{
    
    private StudentRepository studentRepository = StudentRepository.getInstance();

    @Override
    public ModelView process(Map<String, String> paramMap) {
        String studentName = paramMap.get("studentName");
        int year = Integer.parseInt(paramMap.get("year"));
        
        Student student = new Student(studentName, year);
        studentRepository.save(student);
        
        ModelView modelView = new ModelView("save-result");
        modelView.getModel().put("student", student);
        return modelView;
    }
}
package hello.servlet.web.frontcontroller.version3;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import hello.servlet.web.frontcontroller.ModelView;

import java.util.List;
import java.util.Map;

public class StudentListControllerV3 implements ControllerV3{

    private StudentRepository studentRepository = StudentRepository.getInstance();

    @Override
    public ModelView process(Map<String, String> paramMap) {
        List<Student> students = studentRepository.findAll();

        ModelView modelView = new ModelView("students");
        modelView.getModel().put("students", students);
        
        return modelView;
    }
}

 

이제 이렇게 작동할 FrontController를 작성한다.

package hello.servlet.web.frontcontroller.version2;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.version2.controller.StudentFormControllerV2;
import hello.servlet.web.frontcontroller.version2.controller.StudentListControllerV2;
import hello.servlet.web.frontcontroller.version2.controller.StudentSaveControllerV2;
import hello.servlet.web.frontcontroller.version3.ControllerV3;
import hello.servlet.web.frontcontroller.version3.StudentFormControllerV3;
import hello.servlet.web.frontcontroller.version3.StudentListControllerV3;
import hello.servlet.web.frontcontroller.version3.StudentSaveControllerV3;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {

    private Map<String, ControllerV3> controllerMap = new HashMap<>();

    public FrontControllerServletV3(){
        controllerMap.put("/front-controller/v3/students/new-form", new StudentFormControllerV3());
        controllerMap.put("/front-controller/v3/students/save", new StudentSaveControllerV3());
        controllerMap.put("/front-controller/v3/students", new StudentListControllerV3());
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ControllerV3 controller = controllerMap.get(req.getRequestURI());
        if(controller == null){
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        Map<String, String> paramMap = createParamMap(req);
        ModelView modelView = controller.process(paramMap);

        String viewName = modelView.getViewName();
        MyView view = viewer(viewName);
        view.render(modelView.getModel(), req, resp);
    }

    private MyView viewer(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }

    private Map<String, String> createParamMap(HttpServletRequest req) {
        Map<String, String> paramMap = new HashMap<>();
        req.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName, req.getParameter(paramName)));

        return paramMap;
    }
}

여기에 MyView에 파라미터가 3개 들어가는 생성자가 호출 되었으니, 그에 맞게 생성자를 추가해준다.

 

 

'Spring > 스프링' 카테고리의 다른 글

스프링 17일차  (0) 2023.03.31
스프링 15일차  (0) 2023.03.28
스프링 14일차  (0) 2023.03.26
스프링 13일차  (0) 2023.03.25
스프링 12일차  (0) 2023.02.15
728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -

www.inflearn.com

 

간단한 학생 관리 웹을 만들어보자.

학생의 정보에는 학번, 이름, 입학년도가 있다.

 

우선 학생의 클래스를 작성해보자.

전에도 사용했던 Lombok을 사용한다.

package hello.servlet.domain.student;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class Student {
    private Long studentId;
    private String name;
    private String year;
}

학생들의 정보를 저장할 저장소이다.

 

package hello.servlet.domain.student;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class StudentRepository {
    private static Map<Long, Student> store = new HashMap<>();
    private static long STUDENT_ID_SEQUENCE = 0L;

    private static final StudentRepository instance = new StudentRepository();

    public static StudentRepository getInstance(){
        return instance;
    }

    private StudentRepository() {}

    public Student save(Student student){
        student.setStudentId(++STUDENT_ID_SEQUENCE);
        store.put(student.getStudentId(), student);
        return student;
    }

    public Student findById(Long id){
        return store.get(id);
    }

    public List<Student> findAll(){
        return new ArrayList<>(store.values());
    }
    
    public void clearStore(){
        store.clear();
    }
}

저장소는 싱글톤으로 생성했고, 저장과 검색 메서드들을 추가했다.

 

command + shift + T로 테스트 코드를 작성해보자.

package hello.servlet.domain.student;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

class StudentRepositoryTest {

    StudentRepository studentRepository = StudentRepository.getInstance();

    @AfterEach
    void afterEach(){
        studentRepository.clearStore();
    }

    @Test
    void save(){
        //given
        Student student = new Student("hyunttai", 2022);

        //when
        Student savedStudent = studentRepository.save(student);

        //then
        Student findStudent = studentRepository.findById(savedStudent.getStudentId());
        Assertions.assertEquals(savedStudent, findStudent);
    }

    @Test
    void findAll(){
        //given
        Student student1 = new Student("hyeontae", 2022);
        Student student2 = new Student("hyeonttai", 2022);

        //when
        studentRepository.save(student1);
        studentRepository.save(student2);
        List<Student> result = studentRepository.findAll();

        //then
        Assertions.assertEquals(result.size(), 2);
        org.assertj.core.api.Assertions.assertThat(result).contains(student1, student2);
    }
}

@AfterEach로 각 테스트마다 저장소를 초기화 해주었다.

문제없이 작성했다면 테스트를 통과 했을 것이다.

 

이제 servlet으로 HTML form을 응답하도록 코드를 작성해보자.

package hello.servlet.web.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "studentFormServlet", urlPatterns = "/servlet/students/new-form")
public class StudentFormServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        resp.setCharacterEncoding("utf-8");

        PrintWriter writer = resp.getWriter();
        writer.write("<!DOCTYPE html>\n" +
                "<html>\n" +
                "<head>\n" +
                "    <meta charset=\"UTF-8\">\n" +
                "    <title>Title</title>\n" +
                "</head>\n" +
                "<body>\n" +
                "<form action=\"/servlet/students/save\" method=\"post\">\n" +
                "    studentName: <input type=\"text\" name=\"studentName\" />\n" +
                "           year:      <input type=\"text\" name=\"year\" />\n" +
                " <button type=\"submit\">전송</button>\n" + "</form>\n" +
                "</body>\n" +
                "</html>\n");
    }
}

response 객체에 ContentType을 html로해서 html로 응답한 모습이다.

 

action에 save를 달아주었기 때문에 이 요청을 받을 save도 작성을 해야한다.

package hello.servlet.web.servlet;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "studentSaveServlet", urlPatterns = "/servlet/students/save")
public class StudentSaveServlet extends HttpServlet {

    private StudentRepository studentRepository = StudentRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String studentName = req.getParameter("studentName");
        int year = Integer.parseInt(req.getParameter("year"));

        Student student = new Student(studentName, year);
        studentRepository.save(student);

        PrintWriter writer = resp.getWriter();

        writer.write("<html>\n" +
                "<head>\n" +
                " <meta charset=\"UTF-8\">\n" + "</head>\n" +
                "<body>\n" +
                "<ul>\n" +
                "    <li>studentId="+student.getStudentId()+"</li>\n" +
                "    <li>name="+student.getStudentName()+"</li>\n" +
                " <li>year="+student.getYear()+"</li>\n" + "</ul>\n" +
                "<a href=\"/index.html\">메인</a>\n" + "</body>\n" +
                "</html>");
    }
}

그러고 http://localhost:8080/servlet/students/new-form 해당 페이지에 접근하면 잘 작동하는 것을 볼 수 있다.

 

이번엔 하나씩 조회하는 것이 아니라 모든 학생들을 조회하는 기능을 만들어보자.

package hello.servlet.web.servlet;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

@WebServlet(name = "studentListServlet", urlPatterns = "/servlet/students")
public class StudentListServlet extends HttpServlet {

    private StudentRepository studentRepository = StudentRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        resp.setCharacterEncoding("utf-8");

        List<Student> students = studentRepository.findAll();

        PrintWriter writer = resp.getWriter();

        writer.write("<html>");
        writer.write("<head>");
        writer.write("    <meta charset=\"UTF-8\">");
        writer.write("    <title>Title</title>");
        writer.write("</head>");
        writer.write("<body>");
        writer.write("<a href=\"/index.html\">메인</a>");
        writer.write("<table>");
        writer.write("    <thead>");
        writer.write("    <th>studentId</th>");
        writer.write("    <th>studentName</th>");
        writer.write("    <th>year</th>");
        writer.write("    </thead>");
        writer.write("    <tbody>");
        for (Student student : students) {
            writer.write("    <tr>");
            writer.write("        <td>" + student.getStudentId() + "</td>");
            writer.write("        <td>" + student.getStudentName() + "</td>");
            writer.write("        <td>" + student.getYear() + "</td>");
            writer.write("    </tr>");
        }
        writer.write("    </tbody>");
        writer.write("</table>");
        writer.write("</body>");
        writer.write("</html>");
    }
}

이렇게 for-each문을 이용해서 반복되는 html들을 찍어주었다.

이렇게 잘 출력되는 것을 볼 수 있다.

하지만 이렇게 자바코드 안에 HTML을 넣어서 작업하는 방법은 너무 힘들다.

 

그렇기에 HTML에 자바코드를 작성하는 JSP로 페이지를 만들어보자.

신입생을 등록하는 jsp는 html과 크게 다르지 않다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>신입생 등록</title>
</head>
<body>
<form action = "/jsp/students/save.jsp" method="post">
    studentName: <input type = "text" name="studentName">
    year: <input type="text" name="age">
    <button type="submit">전송</button>
</form>

</body>
</html>

 

여기서 응답을 하는 save.jsp는

<%@ page import="hello.servlet.domain.student.StudentRepository" %>
<%@ page import="hello.servlet.domain.student.Student" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
  StudentRepository studentRepository = StudentRepository.getInstance();

  String studentName = request.getParameter("studentName");
  int year = Integer.parseInt(request.getParameter("year"));

  Student student = new Student(studentName, year);
  studentRepository.save(student);

%>
<html>
<head>
    <title>신입생 등록 완료</title>
</head>
<body>
<ul>
  <li>studentId=<%=student.getStudentId()%></li>
  <li>studentName=<%=student.getStudentName()%></li>
  <li>year=<%=student.getYear()%></li>
</ul>

<a href="/index.html">메인</a>

</body>
</html>

이렇게 자바코드와 html이 합쳐져 있다.

윗 부분에 자바 코드들을 작성하고 html의 body에서 데이터들을 가져오는 것이다.

이렇게 보면 기존과 크게 차이가 없어 보이지만, html에 자바코드를 사용해서 반복문으로 값을 출력하면 훨씬 작업하기 편해진다.

 

학생들의 목록을 출력해주는 JSP이다.

<%@ page import="hello.servlet.domain.student.StudentRepository" %>
<%@ page import="hello.servlet.domain.student.Student" %>
<%@ page import="java.util.List" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    StudentRepository studentRepository = StudentRepository.getInstance();
    List<Student> students = studentRepository.findAll();
%>
<html>
<head>
    <title>학생 조회</title>
    <meta charset="UTF-8">
</head>
<body>
<a href="/index.html">메인</a>
<table>
    <thead>
    <th>studentId</th>
    <th>studentName</th>
    <th>year</th>
    </thead>
    <tbody>
    <%
        for(Student student: students){
            out.write("<tr>\n");
            out.write("<td>" + student.getStudentId() + "</td>\n");
            out.write("<td>" + student.getStudentName() + "</td>\n");
            out.write("<td>" + student.getYear() + "</td>\n");
            out.write("</tr>\n");
        }
    %>
    </tbody>
</table>

</body>
</html>

이렇게 html 안에서 자바코드를 사용하여 html 코드들을 반복해서 출력하는 것으로 코드가 훨씬 보기 좋고 편해졌다.

하지만 현재의 JSP는 너무 많은 역할을 담당하고 있다.

그렇기 때문에 Model, View, Controll의 MVC 패턴에 맞추어 작성하도록 해야한다.

Model은 컨트롤러에서 뷰로 넘기는 데이터로 뷰가 출력하는 데에 필요한 데이터들을 가지고 있다.

View는 Model에 있는 데이터를 사용해서 화면을 보여주는 역할을 한다.

Controller는 HTTP 요청을 받아 파라미터를 보고, 로직을 실행한다. 그러고 Model에 데이터를 담아 View에 넘긴다.

 

여기서 Controller에 로직들을 둘 수 있지만, 역할을 나누기위해 서비스를 만들어 그 서비스를 호출하여 실행하는 역할을 한다.

요청부터 응답까지의 과정을 살펴보면

이렇게 MVC를 이용하여 요청에 응답을 한다.

그러면 여기서 로직을 수행하는 부분은 서블릿으로, 뷰를 보여주는 부분은 JSP로 작성을 한다.

 

package hello.servlet.web.servletmvc;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

@WebServlet(name = "mvcStudentFormServlet", urlPatterns = "servlet-mvc/students/new-form")
public class MvcStudentFormServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String viewPath="/WEB-INF/views/new-form.jsp";
        RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
        dispatcher.forward(req, resp);
    }
}

 

이렇게 작성을 하면 urlPattern으로 mapping 한 주소에 접속할 때 dispatcher.forward를 통해 viewPath에 있는 JSP를 열어주게 된다.

여기서 /WEB-INF를 만들어야 하는데, 이 경로 안에 있는 JSP는 외부에서 호출할 수 없고 서버 내부에서만 호출이 가능하다.

 

이제 새로 열릴 new-form.jsp를 만들어준다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>신입생 등록</title>
    <meta charset="UTF-8">
</head>
<body>
<form action="save" method="post">
    studentName: <input type="text" name="studentName"/>
    year: <input type="text" name="year"/>
    <button type="submit">전송</button>
</form>
</body>
</html>

action을 save로 잡아주었으니 이제 save를 작성한다.

전송을 하면 아래의 서블릿에서 로직이 실행이 된다.

 

request를 보고 파라미터들을 이용하여 값을 가져온 후 다시 request.setAttribute()를 이용하여 데이터를 넣어준 후 jsp로 넘겨준다.

package hello.servlet.web.servletmvc;

import hello.servlet.domain.student.Student;
import hello.servlet.domain.student.StudentRepository;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

@WebServlet(name = "mvcStudentSaveServlet", urlPatterns = "/servlet-mvc/students/save")
public class MvcStudentSaveServlet extends HttpServlet {
    private StudentRepository studentRepository = StudentRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String studentName = req.getParameter("studentName");
        int year = Integer.parseInt(req.getParameter("year"));
        
        Student student = new Student(studentName, year);
        studentRepository.save(student);
        
        req.setAttribute("student", student);
        
        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
        dispatcher.forward(req, resp);
    }
}

 

이제 결과를 출력할 jsp를 작성해준다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>신입생 등록 완료</title>
    <meta charset="UTF-8">
</head>
<body>

<ul>
  <li>studentId=${student.studentId}</li>
  <li>studentName=${student.studentName}</li>
  <li>year=${student.year}</li>
</ul>

<a href="/index.html">메인</a>

</body>
</html>

${}를 이용하면 attribute에 있는 데이터를 조회할 수 있다.

 

 

'Spring > 스프링' 카테고리의 다른 글

스프링 17일차  (0) 2023.03.31
스프링 16일차  (0) 2023.03.29
스프링 14일차  (0) 2023.03.26
스프링 13일차  (0) 2023.03.25
스프링 12일차  (0) 2023.02.15

+ Recent posts