본문 바로가기
Programming Language 이해하기/Java 이해하기

Spring5 를 왜 사용해야되는건가?(with Tody Lee)

by simplify-len 2020. 8. 23.

우연히 재미있는 유튜브 동영상을 시청했습니다.

구글 알고리즘에 의해서 노출되었는데, 제목에 이끌러 재생 버튼을 눌렀습니다.


"자바9와 Spring5로 바라보는 Java의 변화와 도전" youtu.be/BFjrmj4p3_Y

 

해당 동영상에서 말하는 내용에 제가 느낀바 추가해서 중요하다고 생각되는 부분만 정리했습니다. 그리고 실제로 코드로 작성하는 행위까지 이어질 예정입니다.

Spring 5 가 2017년 9월 27일출시됐는데, 지금이 2020년이니까, 3년이 지난 시점에서 여전히 Spring5는 사용하는 곳보다 사용하지 않는 곳이 더 많습니다. 이 글을 쓰는 저 또한 Spring5를 사용해본 경험이 없습니다. 애노테이션 기반의 스프링 버전 4.x 를 베이스로 하는 SpringBoot만 사용해왔습니다.

스프링부트가 가져다 주는 API 개발 편의성은 Node.js의 express.js 와 견줘도 될 정도로 많은 발전이 되었다고 생각합니다. 

개발자라면 늘 고민해봐야 하는 부분으로 '프레임워크가 주는 편리함 뒤에 알아야 하는 것' 들을 명확하지 이해하지 못한 채 사용한 것은 아닐까? 라는 생각도 들게합니다.

 

이 동영상에서 토비님 께서는 스프링부트에서 제공되는 애노테이션으로 동작되는 코드에 대해 말합니다.


"자바가 죽을 것이다." 라고 말하는 순간마다 자바는 진화해왔고, 루비온레인즈와 같은 간결화된 프레임워크가 생겨나면서 자바의 위험이 다시한번 들이닥쳤다.

 그래서 자바도 무너질 것인가?

자바도 이에 맞쳐서

"애노테이션 기반의 메타프로그래밍과 영리한 디폴트로 무장한 관계의 적극 도입"

변화합니다.

지금도 저희가 가장 많이 쓰는 방식이죠.

import static org.springframework.http.ResponseEntity.*;

@RestController
@RequestMapping(value = "/v1/api")
public class ApprovalDraftController {

    private final ApprovalDraftService approvalDraftService;

    public ApprovalDraftController(ApprovalDraftService approvalDraftService) {
        this.approvalDraftService = approvalDraftService;
    }

    @GetMapping(value = "approval/draft/user/rank")
    ResponseEntity<ApprovalDraftChartModel> getDraftRankByUser(){

        List<ApprovalDraftUserModel> result = approvalDraftService.getDraftRankByUserCount();

        return ok(ApprovalDraftChartModel.of(new LinkedList<>(), new LinkedList<>(), result));
    }
...
}

위 코드는 관례에 의해서 많은 행위가 이루어집니다.

간단하게 살펴보면, @RestController 는 Json으로 통신하겠다는 내용이고. @RequestMapping 또한 value와 같은 API로 Request를 하곘다 라는 이야기입니다.

또한, JPA도 마찬가지죠. 

@Repository
public class ApprovalCompareWithDrawRepository {
	...
    private final MongoTemplate mongoTemplate;
    ...
}

 

즉, Annotation와 같은 관례로 우리는 간결한 코드를 짤 수 있다는 의미입니다. 

이런 상황에서 또다른 위기를 자바는 겪습니다. 바로 "함수형 프로그래밍과 비동기 논블록킹 개발의 도전"

 

토비님의 얼굴이 나오지만, 그래도 공유합니다. 감사하니까요...!

함수형 프로그래밍의 장점으로 우리는 많은 부분을 이야기합니다. 

- 대용량 비동기 분산 시스템 개발에 적합한 함수형 프로그래밍 필요성 대두

- 비동기 논블록킹에 최적화된 기술(NodeJS)

- 사이드 이펙트가 없는 코드

이런 장점을 가져가기 위해 자바는 

함수형 프로그래밍 스타일의 자바와 비동기 논블록킹 지원 서블릿, 스프링이 등장합니다.

자바8에서 이렇게 코딩합니다.

List<Integer> list = Arrays.asList(1,2,3,4);
list.stream().reduce(0, (a,b) -> a + b);

IntStream.iterate(1, a -> a+ 1).limit(10).reduce(0, (a,b) -> a+b );

2009년에 등장한 Servlet 3.0, 3.1 에 자바에도
- 비동기 웹 요청 처리 방식 지원
- 비동기 논블록킹 IO지원
- 쓰레드 활용 최적화

논블록킹 지원하는 Java

지금까지 살펴본 부분이 과거라면,

지금까지의 위기는 어떤 부분일까?

애노테이션과 메타프로그래밍, 관례의 범람

애노테이션의 한계와 부작용
: 애노테이션은 타입이 아니기 때문에, 정보를 알 수 있는 것이 아니다. 타입을 기반으로 컴파일될 것이다라는 검증할 수 있는 기반이 없다.

- 컴파일러에의해 검증 가능한 타입이 아님
- 코드의 행위를 규정하지 않음
- 상속, 확장 규칙의 표준이 없음.
- 이해하기 어려운 코드, 오해하기 쉬운 코드 가능성
- 테스트가 어렵거나 불가능함
- 커스터마이징하기 어려움

예시로

위 코드가 관례처럼 사용되고, 메타 애노테이션 즉, 애노테이션의 애노티에션이라 불리는 메타 애노테이션의 경우에도 명확한 기준이 없습니다. 

@Components 와 같은 메타애노테이션은 스프링이 그런 규칙을 만든거지, 자바에서 그런 규칙을 만든 것이 아닙니다.

각각의 애노테이션은 의미를 가지고 있는데, 어떻게 테스트 할 수 있는가?

심지어 애노테이션은 자바 문법을 파괴하는 행위까지 이루어집니다.

리플렉션과 런타임 바이트코드 생성의 문제

- 성능 저하
- 타입정보 제거
- 디버깅 어려움
- 툴이나 컴파일러에 의한 검증 불가

이런 문제들을 자바는 어떻게 해결하려고 하는가? 토비님께서는 "자바의 기본으로 돌아간다" 라는 말을 합니다.

즉, 함수형 스타일 프로그래밍이 도입된 업그레이드 된 자바의 기본으로 돌아갑니다.

이는 곧 스프링 5.0의 변화를 이끌었다고 합니다.

추후에는 더 이상의 서블릿은 의존성을 제거하라-!!

Spring5의 새로운 변화는 어떤 부분이 있을까?

- 독립형 애플리케이션
- 스프링 컨테이너 이용

과거 스프링5 이전에 우리의 코드는 

- 요청 매핑
- 요청 바인딩
- 핸들러 실행
- 핸들러 결과 처리(응답 생성)

으로 웹 요청을 처리했습니다. 이 부분의 기존의 애노테이션을 활용한 요청 코드는 생략하겠습니다.

 다만, 위 토비님의 말씀처럼 HandlerFunctionRouterFunction이 어떻게 해결했는지 살펴보겠습니다.

import reactor.core.publisher.Mono;

/**
 * Represents a function that handles a {@linkplain ServerRequest request}.
 *
 * @author Arjen Poutsma
 * @since 5.0
 * @param <T> the type of the response of the function
 * @see RouterFunction
 */
@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {

	/**
	 * Handle the given request.
	 * @param request the request to handle
	 * @return the response
	 */
	Mono<T> handle(ServerRequest request);

}

이 부분을 활용해 함수형으로 변경하면 아래와 같습니다.

HandlerFunction handlerFunction = new HandlerFunction() {
	@Override 
    public Mono handle(ServerRequest req) {
		String name = req.pathVariable("name");
		return ServerResponse.ok().syncBody("hello" + name);
	}
};

---

HandlerFunction handlerFunction = req -> {
	String name = req.pathVariable("name");
	return ServerResponse.ok().syncBody("hello" + name);
};

- 요청 매핑을 제외한
- 요청 바인딩
- 핸들러 실행
- 핸들러 결과 처리(응답 생성)

위 행위들을 순수한 자바 코드로 이뤄집니다.

위 코드의 특징으로

토비님께서는 아래와같이 말합니다.
- 애노테이션, 리플렉션 사용하지 않음
- 웹 요청을 처리하는 모든 작업을, 동작하는 자바 코드로 표현
- 람다식으로 작성 가능
- 독립적인 단위 테스트 가능
- 자유로운 확장, 추상화 기능
- POFO(Plain Old Functional Object)

그렇다면 요청 매핑은 어디서 하는 걸까? 살펴보지 못한 RouterFunction 에서 이루어집니다.

import reactor.core.publisher.Mono;

/**
 * Represents a function that routes to a {@linkplain HandlerFunction handler function}.
 *
 * @author Arjen Poutsma
 * @since 5.0
 * @param <T> the type of the {@linkplain HandlerFunction handler function} to route to
 * @see RouterFunctions
 */
@FunctionalInterface
public interface RouterFunction<T extends ServerResponse> {

	/**
	 * Return the {@linkplain HandlerFunction handler function} that matches the given request.
	 * @param request the request to route
	 * @return an {@code Mono} describing the {@code HandlerFunction} that matches this request,
	 * or an empty {@code Mono} if there is no match
	 */
	Mono<HandlerFunction<T>> route(ServerRequest request);
}

웹 요청을 받아서 해당 요청을 담당할 핸들러를 반환합니다.

RouterFunction router = new RouterFunction() {
		@Override
        public Mono<HandlerFunction> route(ServerRequest request) {

		return RequestPredicates.path("/hello/{name}").test(request) ? Mono.just(handlerFunction): Mono.empty();
	}
};

이렇게 만들어지는 함수형 웹 애플리케이션의 구성요소로는

- 핸들러 함수
- 라우터 함수
- HTTP 서버
- DI 컨테이너

독립형 컨테이너 / 스프링 컨테이너

HandlerFunction handlerFunction = req -> {
		String name = req.pathVariable("name");
		return ServerResponse.ok().syncBody("hello" + name);
};

RouterFunction router = new RouterFunction() {
		@Override 
		public Mono<HandlerFunction> route(ServerRequest request) {
		return RequestPredicates.path("/hello/{name}").test(request) ? Mono.just(handlerFunction): Mono.empty();
	}
};

HttpHandler httpHandler = RouterFunctions.toHttpHandler(router);

ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
HttpServer server = HttpServer.create();
server.handle(adapter).port(8080).host("localhost");

System.in.read();

 

이런 코드의 장점으로는 

- 간결한 코드( 람다식-메소드 상호 호환 - 메서드 레퍼런스)
- 핸들러를 람다식 대신 메소드로 작성할 수 있음
- 기능 종류에 따라 클래스-메소드로 구조화
- 함수의 조합

이러한 장점이 Spring5로 이끄는 역할을 했습니다.

댓글