728x90

CompletableFuture 클래스

우선 주요 메서드 먼저 살펴보고 가도록 하자.

public class CompletableFuture<T> implements Future<T>, CompletionStage<T>{
    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
        return asyncSupplyStage(ASYNC_POOL, supplier);
    }

	public static CompletableFuture<Void> runAsync(Runnable runnable) {
        return asyncRunStage(ASYNC_POOL, runnable);
    }
    
    public boolean complete(T value) {
        boolean triggered = completeValue(value);
        postComplete();
        return triggered;
    }
    
    public boolean isCompletedExceptionally() {
        Object r;
        return ((r = result) instanceof AltResult) && r != NIL;
    }
    
    public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs) {
        return andTree(cfs, 0, cfs.length - 1);
    }
    
    public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs) {
        int n; Object r;
        if ((n = cfs.length) <= 1)
            return (n == 0)
                ? new CompletableFuture<Object>()
                : uniCopyStage(cfs[0]);
        for (CompletableFuture<?> cf : cfs)
            if ((r = cf.result) != null)
                return new CompletableFuture<Object>(encodeRelay(r));
        cfs = cfs.clone();
        CompletableFuture<Object> d = new CompletableFuture<>();
        for (CompletableFuture<?> cf : cfs)
            cf.unipush(new AnyOf(d, cf, cfs));
        // If d was completed while we were adding completions, we should
        // clean the stack of any sources that may have had completions
        // pushed on their stack after d was completed.
        if (d.result != null)
            for (int i = 0, len = cfs.length; i < len; i++)
                if (cfs[i].result != null)
                    for (i++; i < len; i++)
                        if (cfs[i].result == null)
                            cfs[i].cleanStack();
        return d;
    }
}

 

 

supplyAsync

 

    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
        return asyncSupplyStage(ASYNC_POOL, supplier);
    }

 

이렇게 구성이 되어 있었다.

 

보면 알 수 있듯이, 파라미터를 받지 않고도 결과를 만들어서 다음 task에 전달해준다.

 

runAsync

 

	public static CompletableFuture<Void> runAsync(Runnable runnable) {
        return asyncRunStage(ASYNC_POOL, runnable);
    }

 

Runnable과 비슷하다.

값을 받지도, 값을 리턴하지도 않고 수행만 하게 된다.

 

complete

 

CompletableFuture가 완료되지 않았다면 주어진 값으로 채운다.

리턴되는 Boolean은 complete에 의해 상태가 바뀌었다면 true, 아니라면 false를 반환한다.

    public boolean complete(T value) {
        boolean triggered = completeValue(value);
        postComplete();
        return triggered;
    }

 

isCompletedExceptionally

 

CompletableFuture가 에러로 인해 중지가 되었는지 Boolean으로 반환하는 메서드이다.

var futureWithException = CompletableFuture.supplyAsync(() -> 1 / 0);

Thread.sleep(1000);

assert futureWithException.isDone();
assert futureWithException.isCompletedExceptionally();

 

이런식의 코드를 작성하여 확인 할 수 있다.

 

allOf

 

여러개의 CompletableFuture를 모아서 하나의 CompletableFuture로 변환할 수 있다.

모든 CompletableFuture가 완료되면 상태가 done으로 변경된다.

반환하는 값은 없기 때문에 각각의 값에 다시 접근하여 get으로 값을 가져와야 한다.

 

allOf를 테스트 해보기 위해 코드를 작성해서 확인해보자.

@Slf4j
public class A {

    @SneakyThrows
    public static void main(String[] args) {

        var startTime = System.currentTimeMillis();

        var firstFuture = waitAndReturn(100, 1);
        var secondFuture = waitAndReturn(500, 2);
        var thirdFuture = waitAndReturn(1000, 3);

        CompletableFuture.allOf(firstFuture, secondFuture, thirdFuture)
                .thenAcceptAsync(v -> {
                    try{
                        log.info("first: {}", firstFuture.get());
                        log.info("second: {}", secondFuture.get());
                        log.info("third: {}", thirdFuture.get());
                    }catch (Exception e){
                        throw new RuntimeException(e);
                    }
                }).join();

        var endTime = System.currentTimeMillis();

        log.info("time: {}", endTime - startTime);
    }

    public static CompletableFuture<Integer> waitAndReturn(int time, int value) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return value;
        });
    }
}

 

해당 코드를 실행하면 다음과 같이 나온다.

 

차례차례 실행이 된 100 + 500 + 1000이 아닌 1000에 가까운 값이 나오는 것을 볼 수 있다.

 

anyOf

 

allOf와는 다르게 가장 먼저 끝난 Future의 값을 제공해준다.

 

방금과 비슷한 코드를 실행해보면

@Slf4j
public class A {

    @SneakyThrows
    public static void main(String[] args) {

        var startTime = System.currentTimeMillis();

        var firstFuture = waitAndReturn(100, 1);
        var secondFuture = waitAndReturn(500, 2);
        var thirdFuture = waitAndReturn(1000, 3);

        CompletableFuture.anyOf(firstFuture, secondFuture, thirdFuture)
                .thenAcceptAsync(v -> {
                    try{
                        log.info("Hi FirstValue");
                        log.info("value: {}", v);
                    }catch (Exception e){
                        throw new RuntimeException(e);
                    }
                }).join();

        var endTime = System.currentTimeMillis();

        log.info("time: {}", endTime - startTime);
    }

    public static CompletableFuture<Integer> waitAndReturn(int time, int value) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return value;
        });
    }
}

 

 

가장 빨리 실행이 되는 Future만 가져오는 것을 볼 수 있다.

 

 

728x90

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

 

Future 인터페이스

자바에서 비동기 프로그래밍을 하기 위해 알아야 하는 Future 인터페이스에 대해 알아보자. Method reference :: 연산자를 이용해서 함수에 대한 참조를 간결하게 포현한 것이다. package org.example; import j

seungkyu-han.tistory.com

저번 내용을 읽어보면 도움이 될 것이다.

 

CompletionStage

public interface CompletionStage<T> {

    public <U> CompletionStage<U> thenApply(Function<? super T,? extends U> fn);

    public <U> CompletionStage<U> thenApplyAsync(Function<? super T,? extends U> fn);

    public CompletionStage<Void> thenAccept(Consumer<? super T> action);

    public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);

    public CompletionStage<Void> thenRun(Runnable action);

    public CompletionStage<Void> thenRunAsync(Runnable action);

    public <U> CompletionStage<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn);

    public <U> CompletionStage<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn);

    public CompletionStage<T> exceptionally(Function<Throwable, ? extends T> fn);
}

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

 

차례로 내려가면서 실행하기 때문에 각각 파이프 하나의 단계라고 생각하면 될 것이다.

저번 내용과 다르게, 결과를 직접적으로 가져올 수 없기 때문에 비동기 프로그래밍이 가능하게 된다.

또한 Non-blocking으로 프로그래밍하기 위해서는 별도의 쓰레드가 필요하다.

Completable은 내부적으로 FokJoinPool을 사용한다.

할당된 CPU 코어 - 1 에 해당하는 쓰레드를 관리하는 것이다.

이렇게 다른 쓰레드에서 실행이 되기 때문에 Non-blocking 프로그래밍도 가능하게 해준다.

 

CompletionStage와 함수형 인터페이스

저번에 배웠던 함수형 인터페이스와 관련하여 각각 CompletionStage와 연결된다.

Consumer - accept -> thenAccept(Consumer action) void 반환
Function - apply -> thenApply(Function fn) 다른 타입 반환
Function - compose -> thenCompose(Function fn) Completion의 결과값
Runnable - run -> thenRun(Runnable action) void 반환

 

thenAccept[Async]

  • Consumer를 파라미터로 받는다.
  • 이전 task로부터 값을 받지만, 값을 넘기지는 않는다.
  • 다음 task에게 null이 전달된다.
  • 값을 받아서 action만 하는 경우에 사용한다.
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

 

해당 Consumer를 파라미터로 받는다.

 

    public CompletionStage<Void> thenAccept(Consumer<? super T> action);
    public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);
    public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action,
                                                 Executor executor);

 

thenAccept도 하나만 있는 것이 아니라, 3개가 존재한다.

일단 [Async]에 대해서만 확인해보자.

 

@Slf4j
public class A {

    //future 종료된 후에 반환
    public static CompletionStage<Integer> finishedStage() throws InterruptedException {
        var future = CompletableFuture.supplyAsync(() -> {
            log.info("supplyAsync");
            return 1;
        });
        Thread.sleep(100);
        return future;
    }

    //future 종료되기 전에 반환
    public static CompletionStage<Integer> runningStage(){
        return CompletableFuture.supplyAsync(() -> {
            try{
                Thread.sleep(1000);
                log.info("I'm Running!");
            } catch (InterruptedException e){
                throw new RuntimeException(e);
            }
            return 1;
        });
    }

    public static void main(String[] args) throws InterruptedException {

        System.out.println("thenAccept");

        log.info("start thenAccept");
        CompletionStage<Integer> acceptStage = finishedStage();
        acceptStage.thenAccept(i -> {
            log.info("{} in thenAccept", i);
        }).thenAccept(i -> {
            log.info("{} in thenAccept2", i);
        });
        log.info("after thenAccept");

        System.out.println("thenAcceptAsync");

        log.info("start thenAcceptAsync");
        CompletionStage<Integer> acceptAsyncStage = finishedStage();
        acceptAsyncStage.thenAcceptAsync(i -> {
            log.info("{} in thenAcceptAsync", i);
        }).thenAcceptAsync(i -> {
            log.info("{} in thenAcceptAsync2", i);
        });
        log.info("after thenAccept");
    }
}

 

해당코드를 통해 알아보겠다.

종료된 Stage를 사용하여 thenAccept, thenAcceptAsync 2개를 확인해보았다.

 

일단 thenAccept는 반환값이 없기 때문에 2부터는 null이 찍히는 것을 볼 수 있다.

 

그리고 이 두개의 차이는 다음과 같다.

thenAccept는 Future가 완료된 상태라면, caller와 같은 쓰레드에서 실행이 된다.

그렇기 때문에 동기적으로 차례대로 시작된 것을 볼 수 있다.

thenAcceptAsync는 그냥 상태에 상관없이, 그냥 남는 쓰레드에서 실행이 된다.

 

해당 코드로 바꾸어 실행해보자.

        System.out.println("thenAccept Running");

        log.info("start thenAccept Running");
        CompletionStage<Integer> acceptRunningStage = runningStage();
        acceptRunningStage.thenAccept(i -> {
            log.info("{} in thenAccept Running", i);
        }).thenAccept(i -> {
            log.info("{} in thenAccept2 Running", i);
        });
        log.info("after thenAccept");

        Thread.sleep(2000);

        System.out.println("thenAcceptAsync");

        log.info("start thenAcceptAsync Running");
        CompletionStage<Integer> acceptAsyncRunningStage = runningStage();
        acceptAsyncRunningStage.thenAcceptAsync(i -> {
            log.info("{} in thenAcceptAsync Running", i);
        }).thenAcceptAsync(i -> {
            log.info("{} in thenAcceptAsync2 Running", i);
        });
        log.info("after thenAccept");

        Thread.sleep(2000);

 

해당 코드를 실행하면 다음과 같은 결과가 나온다.

 

위와는 다르게 Future가 종료되지 않았다면, thenAccept는 callee에서 실행이 되게 된다.

 

이를 통해, Async는 그냥 thread pool에서 가져와서 실행이 되며 Async가 아니면 stage의 상태에 따라 나뉘는 것을 볼 수 있다.

stage가 실행중이라면, 호출한 caller 쓰레드에서 실행이 된다.

stage가 실행중이 아니라면, 호출된 caller 쓰레드에서 실행이 되게 된다.

 

위에서 마지막에 있던 메서드 중에 파라미터가 하나 더 있던 메서드가 있었다.

executor를 넘겨주는 것인데, 해당 action이 실행될 쓰레드를 지정해주는 것이다.

@Slf4j
public class B {

    @SneakyThrows
    public static void main(String[] args) {
        var single = Executors.newSingleThreadExecutor();
        var fixed = Executors.newFixedThreadPool(10);

        log.info("start main");
        CompletionStage<Integer> stage = CompletableFuture.supplyAsync(() -> {
            log.info("supplyAsync");
            return 1;
        });

        stage
                .thenAcceptAsync(i -> {
                    log.info("{} in thenAcceptAsync", i);
                }, fixed).thenAcceptAsync(i -> {
                    log.info("{} in thenAcceptAsync", i);
                }, single);

        log.info("after thenAccept");
        Thread.sleep(2000);

        single.shutdown();
        fixed.shutdown();
    }
}

 

해당 코드를 실행해보면

이렇게 지정된 쓰레드에서 실행이 되는 것을 볼 수 있다.

 

thenApply[Async]

  • Function를 파라미터로 받는다.
  • 이전 task로부터 T 타입의 값을 받아서 가공하고 U 타입의 값을 반환한다.
  • 다음 task에게 반환했던 값이 전달된다.
  • 값을 변형해서 전달해야 하는 경우 유용하다.
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

 

해당 Function을 파라미터로 받는다.

 

    public <U> CompletionStage<U> thenApply(Function<? super T,? extends U> fn);

    public <U> CompletionStage<U> thenApplyAsync
        (Function<? super T,? extends U> fn);

    public <U> CompletionStage<U> thenApplyAsync
        (Function<? super T,? extends U> fn,
         Executor executor);

 

메서드가 다음과 같이 있다.

 

해당 메서드들을 하면서 확인해보자.

@Slf4j
public class C {
    public static void main(String[] args) throws InterruptedException {
        CompletionStage<Integer> stage = CompletableFuture.supplyAsync(() -> 1);

        stage.thenApplyAsync(value -> {
            var next = value + 1;
            log.info("in thenApplyAsync : {}", next);
            return next;
        }).thenApplyAsync(value -> {
            var next = "result: " + value;
            log.info("in thenApplyAsync2 : {}", next);
            return next;
        }).thenApplyAsync(value -> {
            var next = value.equals("result: 2");
            log.info("in thenApplyAsync3 : {}", next);
            return next;
        }).thenAcceptAsync(value -> {
            log.info("final: {}", value);
        });

        Thread.sleep(2000);
    }
}

 

해당 코드를 실행하면 다음과 같다.

 

thenApply는 다른 타입을 주고 받는 것이 가능한 것을 볼 수 있다.

 

thenCompose[Async]

  • Function를 파라미터로 받는다.
  • 이전 task로부터 T 타입의 값을 받아서 가공하고 U 타입의 CompletionStage를 반환한다.
  • 반환한 CompletionStage가 done 상태가 되면 값을 다음 task에 전달한다.
  • 다른 future를 반환해야 하는 경우 유용하다.
    public <U> CompletionStage<U> thenCompose
        (Function<? super T, ? extends CompletionStage<U>> fn);

    public <U> CompletionStage<U> thenComposeAsync
        (Function<? super T, ? extends CompletionStage<U>> fn);

    public <U> CompletionStage<U> thenComposeAsync
        (Function<? super T, ? extends CompletionStage<U>> fn,
         Executor executor);

 

그냥 Future를 반환하며, 해당 Future가 끝날 때까지 기다렸다가 준다는 것이다.

 

@Slf4j
public class D {

    public static CompletionStage<Integer> addValue(int number, int value){
        return CompletableFuture.supplyAsync(() -> {
            try{
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return number + value;
        });
    }

    public static void main(String[] args) throws InterruptedException {
        CompletionStage<Integer> stage = CompletableFuture.supplyAsync(() -> 1);

        stage.thenComposeAsync(value -> {
            var next = addValue(value, 1);
            log.info("in thenComposeAsync: {}", next);
            return next;
        }).thenComposeAsync(value -> {
            var next = CompletableFuture.supplyAsync(() -> {
                try{
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                return "result: " + value;
            });
            log.info("in thenComposeAsync: {}", next);
            return next;
        }).thenAcceptAsync(value -> log.info("{} in then AcceptAsync", value));

        Thread.sleep(2000);
    }
}

 

해당 코드를 실행해보면

 

다음과 같이 출력이 되는데, 마지막에 result: 2로 Future가 완료된 후의 값을 가져온 것을 볼 수 있다.

 

thenRun[Async]

  • Runnable을 파라미터로 받는다.
  • 이전 task로부터 값을 받지 않고 값을 반환하지 않는다.
  • 다음 task에게 null이 전달된다.
  • future가 완료되었다는 이벤트를 기록할 때 유용하다.

Runnable 인터페이스이다.

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

 

받는 것도 주는 것도 없는 것을 볼 수 있다.

 

당연히 큰 역할을 하기보다, 그냥 로그? 이벤트 기록 용으로 사용한다고 한다.

 

@Slf4j
public class E {

    public static void main(String[] args) throws InterruptedException {
        CompletionStage<Integer> stage = CompletableFuture.supplyAsync(() -> 1);

        stage.thenRunAsync(() -> log.info("in thenRunAsync"))
                .thenRunAsync(() -> log.info("in thenRunAsync2"))
                .thenAcceptAsync(value -> log.info("{} in thenAcceptAsync", value));

        Thread.sleep(2000);
    }
}

 

해당 코드를 실행해보면

 

그냥 주지도, 받지도 않는 것을 볼 수 있다.

 

exceptionally

  • Function을 파라미터로 받는다.
  • 이전 task에서 발생한 exception을 받아서 처리하고 값을 반환한다.
  • 다음 task에게 반환된 값을 전달한다.
  • future 파이프에서 발생한 에러를 처리할 때 유용하다.
    public CompletionStage<T> exceptionally
        (Function<Throwable, ? extends T> fn);

 

간단하게 해당 에러를 발생하는 코드를 만들어보면

@Slf4j
public class F {

    public static void main(String[] args) throws InterruptedException {
        CompletionStage<Integer> stage = CompletableFuture.supplyAsync(() -> 1);

        stage.thenApplyAsync(i -> {
            log.info("in then ApplyAsync");
            return i / 0;
        }).exceptionally(e -> {
            log.info("{} in exceptionally", e.getMessage());
            return 0;
        }).thenAcceptAsync(value -> {
            log.info("{} in thenAcceptAsync", value);
        });

        Thread.sleep(2000);
    }
}

 

이러한 결과가 나오는 것을 볼 수 있다.

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

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

 

스프링 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