semtax의 개발 일지

스프링부트로 게시판 만들기 2 : 스프링 부트 와 JPA 연동 본문

개발/Java

스프링부트로 게시판 만들기 2 : 스프링 부트 와 JPA 연동

semtax 2020. 1. 23. 12:13
반응형

개요

 

이번 포스팅에서는, 스프링부트의 ORM(Object Relation Mapping) 라이브러리인 Spring-data-jpa를 이용하여 실제 데이터베이스에 값을 넣고 빼고, 수정하고, 읽어오는법(CRUD)을 익혀보도록 하겠다.

 

 

테이블(Relation) 과 객체의 불일치

 

이번 섹션은 제 개인적인 견해가 일부 들어가있을수 있으므로, 꼭 다른 사람 의견도 같이 들어서 옳은것을 취사선택 해주시기 바랍니다.

 

 

평소에 웹 개발을 하다가 보면, 사용자의 요구사항에 맞게 다양한 형식의 데이터를 받고, 가공해서 데이터베이스(특히 RDBMS)에 저장하는 작업을 많이한다.

 

 

결국 개발시에는, (자바기준으로) 데이터를 받아서 객체에 담고 데이터가 담긴 객체(VO, DTO)의 값을 꺼내서 SQL 쿼리에 매핑한다음에(DAO가 이 역할을 해준다) DBMS에 쿼리문을 이용해 데이터를 넣어주는 작업이 매우매우 많이 반복이된다. 이렇게 되다보니, 결국 백엔드 개발자가 하는일은 DBA에게 쿼리문을 받아서 테이블구조에 맞춰서 객체를 최대한 똑같이 만들고, SQL과 객체를 매핑하는 매핑머신이 되어버리는 결과가 발생하게 된다.

 

 

또한, 소프트웨어 개발의 장점은 다른 물건들을 만드는것들에 비해서 수정이 용이하다는 것인데, RDBMS 테이블 스키마의 변경비용은 객체변경에 비해 매우 비싸다는 문제점도 존재한다.

 

 

이렇게 되다보니 예쁘게 객체지향적으로 설계하는것이 거의 불가능 해졌다고 볼수있다..

물론 아키텍처를 잘 잡아서 어느정도 까지는 모듈화나 책임을 분리할수는 있겠지만, 결국 비즈니스 로직을 완벽하게 분리해내는것은 거의 불가능하다. 특히 특정 DBMS와 비즈니스 로직간의 결합이 매우 강하게 되는 경우가 발생 한다(결국 비즈니스 로직이 SQL에 담기는 경우가 대부분이므로..).

 

 

그리고, 해당 서비스의 비즈니스 로직을 이해하기 위해 SQL과 개발언어 둘 다를 알아야 하는 사태가 발생한다.

그리고 코드에 2가지 이상의 스타일(SQL, 개발언어)이 혼재하게 되어 보기가 더욱 불편하고 혼잡해진다.

또한, 이러한 구조를 잡으면 문제가 되는것이, 있으니 바로 테이블과 객체는 서로 다르다는 점이다.

구체적으로 어떤 점이 다른지 확인을 해보자.

 

 

  1. 객체와 테이블 에서 Identity는 어떻게 정의하는가?
  2. 테이블에 상속이라는 개념이 존재하는가?
  3. equality는 어떻게 정의할것인가?
  4. 만약 primary key가 null 값이면?

위에서 언급한 테이블(Relation) 과 객체의 불일치 를 비롯한 많은 문제를 해결해줄수 없는지에대해 사람들이 많이 고민을 했고, 저러한 고루한 매핑 작업을 자동화해주고, 최대한 객체지향(OOP) 스럽게 코드를 짜게 해주는 도구의 필요성을 느끼게 되었다.

과연 이러한 도구가 나왔을까?

 

 

 

ORM & JPA

 

 

다행히도 위에서 언급한 문제점들을 해결하기 위해 ORM(Object Relation Mapping)이라는 도구가 나오게 되었다.

 

 

사실 ORM이 나오기 전에 mybatis와 같은 단순 SQL Mapper와 같은 도구들도 나오기는 했다.

사실 이것도 이전의 방법보다는 조금더 나은 방법이기는 했지만, 여전히 SQL에 비즈니스 로직이 집중되는 문제는 막을 수가 없다.

결국 비즈니스 로직을 이해하기 위해 SQL 문을 개발자가 봐야한다는 문제점과 코드가 한눈에 안들어온다는 문제점이 생긴다.

 

 

또한, ORM을 이용하면, 데이터베이스 테이블의 구조를 바꾸는것보다 시간적,경제적 비용이 절감된다는 장점이 존재한다.

 

(특히, 프로토타입을 만드는 경우에는 제품이 어떻게 완성될지가 구체적으로 완전하게 정해지지않아 구조가 자주 바뀌므로 ORM이나 NoSQL을 쓰는게 더 이득일 수 도 있다.)

 

 

(물론 복잡한 쿼리가 많이 들어가는 프로젝트나 다이나믹 쿼리가 많이 들어가는 경우에는 MyBatis가 더 유리할 수 도 있기는 하다. 특히 금융권들..)

 

그리고 위에서 언급한 문제는 자바뿐만이 아닌, 어떤 언어로 웹 개발을 해도 데이터베이스와 같은 영구 저장소를 사용하는 이상,

무조건 발생할수 밖에없는 문제였고, 각 언어별 진영에서 이러한 ORM라이브러리 들을 개발하게 되었다.

 

python과 같은 경우에는 SQLAlchemy, peewee, DjangoORM 등이 있고,

node.js는 sequelize.js, 심지어 안드로이드에서도 ROOM이라는 ORM라이브러리를 지원해 준다.

 

일단 Java에는 ORM을 구현하는 JPA라는 표준이 존재하고 있다.

이 JPA 표준을 구현한 구현체중 1개가 바로 Hibernate이다.

 

(재미있는 사실은 JPA보다 하이버네이트가 먼저 나왔고, JPA는 사실 하이버네이트의 상당부분을 가져다 썼다는것이다..)

이제부터는 스프링에서 JPA를 이용하여 데이터를 수정,삽입,갱신,조회(CRUD) 하는 예제를 다루어보도록 하겠다.

예제

먼저, pom.xml(Maven 설정파일)에 spring과 관련된 의존성을 추가해준다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>springwebexample</groupId>
    <artifactId>com.semtax.springwebexample</artifactId>
    <version>1.0-SNAPSHOT</version>

    <name>demospringmvc</name>
    <description>Demo Project</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.build.outputEncoding>UTF-8</project.build.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.1.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <version>2.1.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.1.6.RELEASE</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

 

 

 

위의 pom.xml을 통해 "spring-boot-starter-data-jpa"와 "postgresql", "h2db" 등의 의존성을 추가해준다.

 

그리고 "test" 를 통해 테스팅을 수행할시에는 h2(스프링에서 제공하는 인-메모리 데이터 베이스,

메모리에서 바로 읽고 쓰므로 속도가 일반 DB에 비해 빠르다)를 테스트용 DB로 쓰겠다고 선언해준다.

 

 

 

그 다음으로, application.properties에 아래와 같은 내용을 추가해준다.

 

 

spring.datasource.url=jdbc:postgresql://localhost:5432/testdb
spring.datasource.username=testdb
spring.datasource.password=testdb

spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true

 

 

위의 설정을 이용해서 어떠한 DB에 접속할것인지, 데이터베이스의 아이디와 패스워드, 그리고 각 db별로 필요한 설정(해당 포스팅 같은경우, PostgreSQL)들을 추가해준다.

 

(스프링에서는 사용자가 스프링 프레임워크를 이용해서 작성한 프로그램이 실행될때 프레임워크에서 application.properties의 내용을 자동으로 읽어서 설정에 반영한다.)

 

 

다음으로 Entity라는 패키지를 만들고 그 안에 Memo라는 객체를 추가해준다.

 

Entity 패키지는 앞으로 우리가 만들려는 서비스에서 사용할 데이터들을 모아놓는 패키지가 될 것이다.

 

package com.semtax.application.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.Objects;

@Entity
public class Post {

    @Id
    @GeneratedValue
    private Long Id;

    private String title;
    private String content;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Post post = (Post) o;
        return Objects.equals(Id, post.Id) &&
                Objects.equals(title, post.title) &&
                Objects.equals(content, post.content);
    }

    @Override
    public int hashCode() {
        return Objects.hash(Id, title, content);
    }
}

 

 

보통 JPA 같은 경우 자바 스펙의 상당부븐을 따르므로, POJO(Plain Old Java Object) 규약을 따라서 작성하게 된다.

해당 방식을 따르지 않는경우. ORM이 변환을 제대로 수행하지 못한다.

 

 

(사실 이러한 작업들이 귀찮기 때문에, lombok과 같은 편리한 도구를 제공하고 있고, 그걸 사용해도 된다.)

그 다음으로, repository라는 패키지를 만들어 준다. 해당 패키지는 실제 데이터 저장소에서 데이터를 어떻게 가져오는 방법들을 서술한 코드들을 모아놓은 패키지들이 될것이다.

 

 

package com.semtax.application.repository;


import com.semtax.application.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PostRepository extends JpaRepository<Post,Long> {

}

 

 

JPA의 편한점은 바로 위의 코드와 같이 Entity 객체에 ORM대상이다 라는것을 어노테이션을 이용하여 명시만 해주면 알아서 그에 해당하는 Repository를 만들어준다는것에 있다.

 

 

그 다음으로 실제로 위에서 선언한 레포지토리를 사용하는 컨트롤러를 아래와 같이 구현해준다.

 

 

package com.semtax.application.controller;

import com.semtax.application.entity.Post;
import com.semtax.application.repository.PostRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
public class PostController {

    @Autowired
    PostRepository postRepository;

    @GetMapping("/")
    public String hello(){
        return "Main Page!";
    }


    @GetMapping("/post")
    public List<Post> getAllPost(){
        return postRepository.findAll();
    }


    @GetMapping("/post/{id}")
    public Post getPost(@PathVariable String id){
        Long postID = Long.parseLong(id);

        Optional<Post> post = postRepository.findById(postID);

        return post.get();
    }


    @PostMapping("/post/{id}")
    public Post updatePost(@PathVariable String id, @RequestBody Post newPost){
        Long postID = Long.parseLong(id);

        Optional<Post> post = postRepository.findById(postID);

        post.get().setTitle(newPost.getTitle());
        post.get().setContent(newPost.getContent());

        postRepository.save(post.get());

        return post.get();
    }


    @PutMapping("/post")
    public Post createPost(@RequestBody Post post){
        Post newPost = postRepository.save(post);

        return newPost;
    }


    @DeleteMapping("/post/{id}")
    public String deletePost(@PathVariable String id){
        Long postID = Long.parseLong(id);
        postRepository.deleteById(postID);

        return "Delete Success!";
    }
}

 

 

(사실 위의 코드는 에러처리를 고려하지는 않은 코드이다. 해당 부분의 에러처리는 추후 포스팅에서 다루도록 하겠다.)

먼저 @Autowired 어노테이션을 통해서 스프링부트(스프링 프레임워크)가 자동적으로 postRepository 객체를 자동으로 생성하게 해주었다.

 

 

그리고 나서, "@XXXMapping" 어노테이션을 통해 사용자가 http를 이용해서 요청을 할때마다, 그에 해당하는 메서드가 실행되도록 매핑을 해주었다.

 

 

매핑목록은 아래와 같다.

 

어노테이션 명(HTTP Method) 역할
@GetMapping 데이터 조회
@PostMapping 데이터 수정
@PutMapping 데이터 삽입
@DeleteMapping 데이터 삭제

 


그리고 매핑된 각 메서드별로 아래와 같이 데이터를 조회/조작 한다.

 

  • getAllPost() 에서는 findAll() 함수를 통해 데이터베이스에서 모든 post를 가져온다.
  • getPost() 에서는 findById() 함수를 통해 데이터베이스에서 지정된 id를 가진 post를 가져온다.

  • createPost() 에서는 save() 함수를 통해 json으로 넘겨받은 post를 저장한다.

  • updatePost() 에서는 findById()를 통해 수정할 post를 가져와서 setter를 이용해서 데이터를 수정하고,
    다시 반환하는 방식으로 구현하였다.

  • deletePost() 에서는 deleteById() 함수를 통해 지정한 id의 post를 삭제한다.
    • 삭제 후에는 "Delete Success!" 메시지를 반환한다.

 

또한, 각 메서드 별로 @PathVariable 어노테이션을 이용해서 URI를 통해서 id값을 받아와서,
postRepository의 메서드에 넘겨주게 된다.

 

 

그리고, 어떻게 Post를 저렇게 바로 넘겨받을수 있지? 라고 생각 할수 있지만,

사실은 내부적으로 스프링의 HTTPMessageConverter에 의해 JSON과,

객체간의 변환이 수행되어서 저렇게 사용할 수 있는것이다.

 

 

 

작성이 끝나고 나서 패키지의 폴더 구조를 자세히 보면 해당 프로젝트는 아래와 같은 구조를 가지게 된다.

 

 

(AccountRepository와 Account 객체는 일단 무시해도 된다.)

각 패키지별 구조는 대략 아래와 같다.

 

 

사용자
뷰(받은 데이터를 어떻게 넘길것인가/어떻게 보여줄것인가) (이번 프로젝트에서는 json으로 뿌려 줌)
컨트롤러(데이터를 가져와서 어디에 뿌려줄것인가)
비즈니스 로직(데이터를 어떻게 가공할것인가) (이번 프로젝트에서는 Controller와 비즈니스 로직을 합쳐서 구현 함)
레포지토리(데이터를 가져오기)
엔티티(핵심 비즈니스 로직 및 핵심 비즈니스 데이터)

 

위의 표와 같이 각 계층별로 역할이 나누어진것을 볼수가 있다.

 

위와 같은 구조를 보통 Layered 아키텍처(또는, N-tier 아키텍처) 라고 한다.

 

사실 이러한 방식 말고도 각 기능별로 패키지를 만들어서 쪼개는 방식도 사용할 수 있다.(보통 이러한 경우를 수직적 계층화라고 한다.)

 

 

 

이제 스프링을 실행시키고 Postman 을 이용해서 아래와 같이 테스트를 수행해보도록 하자.

정상적으로 데이터를 주고, 받고, 수정하고 갱신한다는것을 확인 할 수 있다.

반응형
Comments