Search

'개발/spring'에 해당되는 글 2건

  1. 2021.03.13 Node.js vs Spring Boot (5)
  2. 2021.03.05 Spring 테이블 칼럼이 아닌 필드 데이터 받아오기

Node.js vs Spring Boot

개발/spring 2021. 3. 13. 22:32 Posted by 아는 개발자

 

현재 서버 애플리케이션 플랫폼의 큰 두 축은 Spring Boot 와 Node.js 인것 같다. 각각의 플랫폼마다 고유한 장점이 있을 텐데 정작 나는 '일하고 있는 곳에서 사용중이다', '요새 이게 트렌드라고 한다'는 이유로 본질을 망각한채 공부만 해왔던 것 같다. 그래서 이번 포스트에서는 spring boot와 nodejs 각각의 장점과 단점을 늦었지만 다뤄 보고자 한다.

 

Node.js

 

Node.js하면 자바스크립트로 짤 수 있는 서버 애플리케이션을 가장 먼저 떠오르는데 사실 Node js는 Non-blocking I/O를 처리하는데 최적화된 플랫폼이다. Non-blocking I/O는 다른 작업이 처리되는 걸 기다리는 도중에 다른 작업을 하는 것을 말하며 이러한 형태는 짧은 시간에 여러 작업을 처리할 수 있어 효율적이다. 다른 언어에서도 이런 형태로 구현은 가능하지만 코드가 너무 지저분해지고 구현이 어려운 단점이 있었는데 Node js에서는 비동기식 함수를 통해 코드 상에서 이 작업을 구현하기 간편하게 만들어줬다. 실제로 최근에 만든 사이드프로젝트에서 Non-blocking I/O를 구현하는게 정말 간편했다. 그리고 내부적으로는 하나의 Thread를 이용해서 구현했기 때문에 메모리를 크게 잡아 먹지도 않아 효율적이다. 똑같은 애플리케이션을 돌려도 다른 프레임워크보다 Node.js가 소모하는 메모리의 크기가 적다.

 

단점은 JavaScript 언어를 사용한다는 점이다. JavaScript가 배우기는 참 쉬워서 적은 시간을 투자하고 금방 숙달을 할 수 있으나 프로젝트 규모가 커지면 커질수록 Type Safe 하지 못하는 점이 한계점으로 작용한다. 언어가 Type Safe 하지 못하면 내가 짠 코드가 별것도 아닌 에러로 런타임에 죽을 수도 있다. 대부분 이 에러는 Java나 C언어 를 사용했다면 빌드 중에 발생하는 컴파일 에러 종류인데 JavaScript는 빌드하는 과정이 없기 때문에 실행 전에 잡아 주질 못한다. 구현하고 서버 실행까지 매우 빠르다고 좋아 할지 모르나 이 사이에 컴파일 오류는 없을 지 꼼꼼히 봐야한다. 그리고 Type Safe하지 못해서 IDE에서 자동 완성이 잘 되지 않는다. 프로젝트가 커지면 커질수록 리팩토링을 하거나 기존 코드를 써먹어서 확장해야 할 때 자동완성 기능이 핵심인데 JavaScript를 쓰면 자동완성이 잘 안돼서 큰 애를 먹게 된다. 프론트엔드 프레임워크 React에서는 TypeScript를 도입해서 어느정도 보완하고 있는데 Node.js에서도 TypeScript를 도입하는 시도가 있다고 들었는데 어느 정도 진행됐는지 모르겠다. 

 

Spring Boot

 

SpringBoot는 Java로 만든 서버 애플리케이션이다. Java는 유구한 역사를 가지고 있고 지금도 많이 사용되는 언어라 스프링부트를 사용하면 Java 언어에서 있는 기능을 그대로 사용할 수 있다. Java를 개발해본 사람들은 쉽게 Spring Boot에 적응 할 수 있다. 그리고 역사가 오래 됐기 때문에 개발하는데 필요한 왠만한 라이브러리는 모두 Spring Boot에 다 있다. 안드로이드 개발자가 사용한 자바 라이브러리들은 모두 Spring에서도 찾을 수 있다고 볼 수 있고 추가로 서버 개발자들이 어려움을 겪는 데이터베이스 관리도 스프링부트에서는 JPA라는 라이브러리를 통해 간소화 해둬서 손쉽게 다룰 수 있다. 그리고 Java이기 때문에 TypeSafe 하다. 리팩토링하거나 확장 할 때 IDE를 이용해서 수정할 점을 빠르게 체크 할 수 있는데 프로젝트 규모가 커지고 안정성이 중요해지는 시점부터는 큰 장점으로 다가온다. 내부적으로는 Multi Threading을 지원하는 구조로 짜여있어서 길고 반복적인 업무를 처리할 때 효율적이다. 많은 양의 컴퓨팅이 필요한 경우 잘 써먹으면 좋다.

 

한번 써보신 분들은 알겠지만 Spring Boot는 러닝 커브가 존재한다. Node.js는 처음 배우는 사람도 하루만에 서버 구동하고 api도 하나 만들 수 있는데 Spring Boot를 공부하면 Service, Controller, Repository 에 대해서 알아야하고 각 컴포넌트는 어떤식으로 채워야하는지 공부가 필요해 해야 할 게 많다. Spring Boot에서는 좋은 구조를 유도하기 위해 이런 형태의 디자인을 권장하는데 초심자한테는 러닝 커브가 좀 있다. 그리고 boilerplate 코드가 많다. 스프링에서 권장하는 구조랑 라이브러리들을 사용하려면 이런 저런 코드를 만들어야 하는데 처음에는 어려우나 숙달되면 귀찮아진다. 그래도 안쓰는 것 보다 낫긴 하지만. 내부적으로는 메모리를 좀 많이 쓴다. Multi thread 환경이기 때문에 여러개의 Thread를 띄우다 보니까 어쩔 수 없이 생긴 문제인 것 같다. 

 

728x90

'개발 > spring' 카테고리의 다른 글

Node.js vs Spring Boot  (5) 2021.03.13
Spring 테이블 칼럼이 아닌 필드 데이터 받아오기  (0) 2021.03.05
  1. 지나가는 나그네 2021.03.19 18:50  댓글주소  수정/삭제  댓글쓰기

    Node.js를 프레임워크라고 하지 않습니다. javascript runtime 입니다.
    그래서 spring boot 와 비교하기 위해서는 node 에서 동작하는 다른 프레임워크와 비교해야 합니다.

  2. Werewolf 2021.04.17 11:21  댓글주소  수정/삭제  댓글쓰기

    요즘 많이 쓰이는 대부분의 Node.js 기반 프레임워크는 타입스크립트를 지원하고 있습니다. 이 중에서도 Nest.js 의 경우 프레임워크 자체가 타입스크립트로 개발되었고 스프링과 매우 흡사한 구조를 가지고 있기 때문에 기존의 스프링 개발자 분들이나 혹은 Node.js 개발자 분 중 스프링 프레임워크로 넘어갈 걸 고려하는 개발자 분들에게 매우 좋은 선택인 것 같습니다.

    어떻게 보자면 스프링 프레임워크가 혹독한 다이어트를 거쳐 체중을 감량하고 이벤트 루프 기반 비동기 I/O를 기본으로 지원하도록 변형된 버전을 Nest.js라고 보면 될 것 같습니다. 당근마켓 등 스타트업 몇 곳에서 Nest.js를 활용하고 있고 근래의 Node.js 기반 프레임워크 중에서는 나름 대세(?)라고 해도 과언이 아닐 것 같네요.

Spring으로 쿼리를 만들다보면 여러개의 테이블을 조인한 쿼리에서 다른 테이블 칼럼의 값까지 읽어올 필요가 있다. 예를 들면 글정보를 받아 오는 api가 있는데 내가 그 글을 좋아요 했는지, 안했는지 유무까지 알려주는 요구 사항의 경우 두 개의 테이블을 조인해야한다.

 

쿼리문을 짜면 다음과 같다. tb_post에 있는 모든 필드를 가져오고, 좋아요 유무는 liked 필드 이름으로 받아오는 것으로 뒀다.

select tb_post.*, when tb_post_user_like.post_id > 0 then true else false end as liked from tb_post
left join tb_post_like on (tb_post.post_id = tb_post_user_like.post_id and tb_post_like.user_id = :findUserId)
where tb_post.post_id = :postId

 

JPA에서 제공하는 CrudRepository 로는 이미 있는 테이블의 칼럼을 매핑해서 받아오는데 반해 이 방법은 임의로 liked라는 새 칼럼을 생성한 것이기 때문에 테이블과 1:1 매핑된 엔티티 클래스에서 했던 것처럼 칼럼을 자동으로 매핑하는게 안되고 다른 방법을 써야한다. 열심히 구글 검색을 해본 결과 세가지 방법을 발견했다. 

 

1. Object형태로 받아오기 

Query문에서 리턴 값을 Object로 받아오면 모든 필드 값의 리턴을 받아 올 수 있다. 가장 직관적이고 쉬운 방법이다.

 

@Query(value = "select tb_post.*, case when tb_post_like.post_id > 0 then true else false end as liked from tb_post \n" +
        "from tb_post \n" +
        "left join tb_post_like on (tb_post.post_id = tb_post_like.post_id and tb_post_like.user_id = :findUserId)\n" +
        "where tb_post.post_id = :postId", nativeQuery = true)
fun findPostById(@Param("postId") postId: Long, @Param("findUserId") findUserId: Long) : List<Array<Any>>

하지만 이렇게 읽어오면 아래 그림처럼 필드 값이 생략돼서 날라오게 돼서 알아보기가 힘들다. 쿼리문에서 칼럼 필드 순서를 지정하는 방법으로 처리할 수 있으나 알아보기가 힘들어서 관리하기가 어려운 단점이 있어 추천하지 않는다. 위와 같은 형태로 읽어오는 클래스를 만든다면 더더욱 쓰지 않는게 좋다.

 

 

2. JPA New 명령어 

 

JPA 쿼리의 New 명령어를 사용하면 리턴 값을 클래스로 줄 수 있다. 단 이 방법은 native query 문을 사용하지 못하고 jpa 쿼리를 사용해야 한다는 점이다. limit을 쓰는 구문에서는 사용할 수 없다.

 

data class Post(postId: Long, postTitle: String, postContent: String, liked:Boolean)

@Query(value = "select new com.package.Post(tb_post.post_id, tb_post.post_title, tb_post.post_content, case when tb_post_like.post_id > 0 then true else false end) from tb_post from tb_post left join tb_post_like on (tb_post.post_id = tb_post_like.post_id and tb_post_like.user_id = :findUserId) where tb_post.post_id = :postId")
fun findPostById(@Param("postId") postId: Long, @Param("findUserId") findUserId: Long) : List<Post>

 

3. SetQueryResultSetMapping 

 

쿼리에서 읽어온 컬럼 필드를 클래스에 매핑 해줄 수 있는 어노테이션이다. ConstructorResult 어노테이션에서 칼럼 필드 값을 읽어와서 Post 클래스의 생성자로 만들 수 있다. 하단의 NamedNativeQuery 어노테이션에서는 쿼리의 이름을 정하고, ConstructorResult에서 참조하는 필드의 형태로 읽어 올 수 있도록 Select 문을 만들어 주고 사용할 mapping을 SqlResultSetMapping에서 지정한 이름과 동일한 값을 입력한다. 이렇게 하면 이 쿼리는 자동으로 Post 클래스 값을 출력하는 쿼리가 된다.

 

@SqlResultSetMapping(
        name = "PostMapping",
        classes = [
                ConstructorResult(
                        targetClass = Post::class,
                        columns = [
                                ColumnResult(name = "post_id", type = Long::class),
                                ColumnResult(name = "post_title", type = String::class),
                                ColumnResult(name = "post_content", type = String::class),
                                ColumnResult(name = "liked", type = Boolean::class)
                        ]
                )
        ]
)
@NamedNativeQueries(value = [
    NamedNativeQuery(name = "findPostByIdBaseOnUser", query = "select tb_post.post_id, tb_post.post_title, tb_post.post_content, case when tb_post_like.post_id > 0 then true else false end as liked from tb_post \n" +
            "left join tb_post_like on (tb_post.post_id = tb_post_like.post_id and tb_post_like.user_id = :findUserId)\n" +
            "where tb_post.post_id = :postId resultSetMapping = "PostMapping")
])
@Entity
data class Post(
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        val postId: Long,
        @Column
        val postTitle: String,
        @Column
        val postContent: String,
        @Column
        val liked: Boolean = false
): Serializable

 

아래의 코드로 쿼리 호출이 가능하다. 앞서 설저한 NamedNativeQuery의 이름 값과 동일하게 넣는다. 초반에 보일러플레이트 코드가 많지만 Post 클래스를 지속적으로 사용하고자 한다면 필드 값을 참조 할 수 있기 때문에 관리가 더 편리하다. 

 

@Service
class PostService {
    @PersistenceContext
    lateinit var entityManager: EntityManager

    fun postById(postId: Long, findUserId: Long): Post? {
        return entityManager.createNamedQuery("findPostByIdBaseOnUser", Post::class.java)
                .setParameter("postId", postId)
                .setParameter("findUserId", findUserId)
                .resultList
                .firstOrNull()

    }
}
728x90

'개발 > spring' 카테고리의 다른 글

Node.js vs Spring Boot  (5) 2021.03.13
Spring 테이블 칼럼이 아닌 필드 데이터 받아오기  (0) 2021.03.05