🌱JAVA/Spring

[Spring Boot] 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - 05

뉴발자 2022. 11. 15.
728x90

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Spring Data JPA

 

 

 

 

MyBatis, iBatis는 ORM이 아니고 SQL Mapper이다.

 

ORM은 객체를 매핑하는 것이고,SQL Mapper는 쿼리를 매핑한다.

 

현대의 웹 애플리케이션에서 관계형 데이터베이스(RDB, Relational Database)는 빠질 수 없는 요소이다.

 

Oracle, MySQL, MSSQL 등을 쓰지 않는 웹 애플리케이션은 거의 없다.

 

객체를 관계형 데이터 베이스에서 관리하는 것이 무엇보다 중요하다.

 

 

관계형 데이터베이스가 계속해서 웹 서비스의 중심이 되면서 모든 코드는 SQL중심이 되어간다.

 

현업 프로젝트 대부분이 애플리케이션 코드보다 SQL로 가득하게 됐다.

 

이는 관계형 데이터베이스가 SQL만 인식할 수 있기 때문인데, SQL로만 가능하니

 

각 테이블마다 기본적인 CRUD(Create, Read, Update, Delete)  SQL을 매번 생성해야 한다. 

 

 

개발자가 아무리 자바 클래스를 잘 설계해도, SQL을 통해야만 데이터베이스에 저장하고 조회할 수 있다.

 

결국, 관계형 데이터베이스를 사용해야만 하는 상황에서 SQL은 피할 수 없다.

 

 

실제 현업에서는 수십, 수백 개의 테이블이 있는데,

 

이 테이블의 몇 배의 SQL을 만들고 유지 보수해야된다.

 

이런 단순 반복 작업의 문제 외에도 한 가지 문제가 더 있다.

 

그건 바로 패러다임 불일치 문제이다.

 

 

관계형 데이터베이스어떻게 데이터를 저장할지에 초점이 맞춰진 기술이다.

 

반대로 객체지향 프로그래밍 언어메세지를 기반으로 기능과 속성을 한 곳에서 관리하는 기술이다.

 

 

관계형 데이터베이스와 객체지향 프로그래밍 언어의 패러다임이 서로 다른데,

 

객체를 데이터베이스에 저장하려고 하니 여러 문제가 발생하게 된다.

 

이를 패러다임 불일치라고 한다.

 

 

객체지향 프로그래밍에서 부모가 되는 객체를 가져오는 방법은 다음과 같다.

User user = findUser();
Group group = user.getGroup();

 

위 코드는 누구나 명확하게 User와 Group은 부모-자식 관계임을 알 수 있다.

 

User가 본인이 속한 Group을 가져오기 때문이다.

 

 

하지만 여기에 데이터베이스가 추가되면 다음과 같이 코드가 변경된다.

User user = userDao.findUser();
Group group = groupDao.findGroup(user.getGroupId());

 

User 따로, Group 따로 조회하게 된다.

 

위와 같은 코드를 보고 User와 Group이 어떤 관계인지 알 수 있을까?

 

상속, 1:N 등 다양한 객체 모델링을 데이터 베이스로는 구현할 수 없다.

 

그러다 보니 웹 애플리케이션 개발은 점점 데이터 베이스 모델링에만 집중하게 된다.

 

 

JPA는 이러한 문제점을 해결하기 위해 등장하게 된다.

 

서로 지향하는 바가 다른 2개 영역(객체지향 프로그래밍 언어와 관계형 데이터베이스)을

 

중간에서 패러다임을 일치시켜주기 위한 기술이다.

 

 

즉, 개발자는 객체지향적으로 프로그래밍을 하고,

 

JPA가 이를 관계형 데이터베이스에 맞게 SQL 대신 생성해서 실행한다.

 

개발자는 항상 객체지향적으로 코드를 표현할 수 있으니 더는 SQL에 종속적인 개발을 하지 않아도 된다.

 

 

객체 중심으로 개발을 하니 생산성 향상과 코드를 유지 보수하기가 편해진다.

 

이런 점 때문에 규모가 크고 매일 24시간, 대규모 트래픽과 데이터를 가진 서비스에서 JPA를 사용한다.

 

 

 

 

Spring Data JPA

JPA인터페이스로서 자바 표준명세서이다.

 

인터페이스인 JPA를 사용하기 위해서는 구현체가 필요한데,

 

대표적으로 Hibernate, OpenJPA 등이 있다.

 

 

하지만, Spring에서 JPA를 사용할 때는 이 구현체들을 직접 다루진 않는다.

 

구현체들을 좀 더 쉽게 사용하고자 추상화시킨 Spring Data JPA라는 모듈을 이용하여 JPA 기술을 다룬다.

 

 

Hibernate를 쓰는것과 Spring Data JPA를 쓰는 것 사이에는 큰 차이가 없다.

 

그럼에도 스프링 진영에서는 Spring Data JPA를 개발했고, 이를 사용하는 것을 권장하고 있다.

 

 

 

 


Spring Data JAP의 등장 이유

  • 구현체 교체의 용이성
  • 저장소 교체의 용이성

 

 

구현체 교체의 용이성

  • Hibernate 외에 다른 구현체로 쉽게 교체하기 위함이다.

   언젠간 수명을 다해서 새로운 JPS 구현체가 대세로 떠오를때 Spring Data JPA를 쓰는 중이라면 쉽게 교체할 수 있다.

 

   이유는 Spring Data JPA 내부에서 구현체 매핑을 지원해주기 때문이다.

 

   실제로Jedis에서 Lettuce로 대세가 넘어갈 때 Spring Data Redis를 쓴 사람들은 아주 쉽게 교체를 했다.

 

 

저장체 교체의 용이성

  • 서비스 초기에는 관계형 데이터베이스로 모든 기능을 처리했지만, 점점 트래픽이 많아지면서 관계형 데이터베이스로는 도저히 감당이 안되는 경우가 생길수도 있다.

   이때MongoDB로 교체가 필요하다면

 

   개발자는Spring Data JPA에서 Spring Data MongoDB로 의존성만 교체해서 사용하면 된다.

 

   이유는Spring Data의 하위 프로젝트들은 기본적인 CRUD의 인터페이스가 같기에 가능하다.

 

   즉, Spring Data JPA, Spring Data Redis, Spring Data MongoDB

 

   Spring Data의 하위 프로젝트들은 save(), findAll(), findOne() 등을 인터페이스로 갖고 있다.

 

 

   그러므로 저장소가 교체되어도 기본적인 기능은 변경하지 않아도 된다.


 

이러한 장점들로 인해 Spring팀에선 Hibernate를 직접 쓰기보단 Spring Data 프로젝트를 권장하고 있다.

 

실무에서 JPA를 사용하지 못하는 가장 큰 이유로 높은 러닝 커브를 이야기한다.

 

 

JPA를 잘 쓰려면 객체지향 프로그래밍과 관계형 데이터베이스를 둘 다 이해하고 있어야 한다.

 

 


JPA를 사용해서 얻는 이점

  • CRUD 쿼리를 개발자가 직접 작성할 필요가 없다.
  • 부모-자식 관계 표현, 1:N 관계 표현, 상태와 행위를 한 곳에서 관리하는 등 객체지향 프로그래밍을 쉽게 할 수 있다.

 

 

 

 

프로젝트에 Spring Data JPA 적용하기

먼저 'build.gradle'의 'dependencies'에 아래와 같이 의존성을 등록한다.

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')

    compile('org.projectlombok:lombok')

    compile('org.springframework.boot:spring-boot-starter-data-jpa')

    compile('com.h2database:h2')

    testImplementation('org.springframework.boot:spring-boot-starter-test')
}

 


코드설명

 

@spring-boot-starter-data-jpa

  • 스프링 부트용 Spring Data JPA 추상화 라이브러리
  • 스프링 부트 버전에 맞춰서 자동으로 JPA 관련 라이브러리들의 버전을 관리해준다.

 

 

@h2

  • 인메모리 관계형 데이터베이스이다.
  • 별도의 설치 없이 프로젝트 의존성만으로 관리할 수 있다.
  • 메모리에서 실행되기 때문에 애플리케이션을 재시작할 때마다 초기화된다는 점을 이용하여 테스트 용도로 많이 사용된다.
  • 여기서는 JPA의 테스트, 로컬 환경에서의 구동에서 사용할 예정이다.

 

의존성이 등록되었다면, 본격적으로 JPA 기능을 사용해보겠다. 아래와 같은 위치에 'domain'패키지를 생성한다.

 

domain 패키지 신규 생성

 

이 패키지는 도메인을 담을 패키지이다.

 

도메인이란 게시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 혹은 문제 영역이라고 생각하면 된다.

 

기존에 MyBatis와 같은 쿼리 매퍼를 사용했다면 dao 패키지를 떠올리겠지만,

 

dao 패키지와는 조금 결이 다르다고 생각하면 된다.

 

그간 xml에 쿼리를 담고, 클래스는 오로지 쿼리의 결과만 담던 일들이

 

모두 도메인 클래스라고 불리는 곳에서 해결된다.

 

이 'domain' 패키지의 하위 패키지로 'posts' 패키지와 그 안에 'Posts' 클래스를 생성한다.

 

posts 패키지와 Posts 클래스 신규 생성

 

그리고 Posts Class에 아래와 같이 코드를 작성해준다.

 

package com.tistory.tlseoqja.domain.posts;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter
@NoArgsConstructor
@Entity
public class Posts {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 500, nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;

    private String author;

    @Builder
    public Posts(String title, String content, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }

}

 


코드설명

 

@Entity

  • 테이블과 링크될 클래스임을 나타낸다.
  • 기본값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍(_)으로 테이블 이름을 매칭한다.
  • ex) SalesManager.java  ->  sales_manager table

 

 

@Id

  • 해당 테이블의 PK 필드를 나타낸다.

 

 

@ GeneratedValue

  • PK 생성 규칙을 나타낸다.
  • 스프링 부트 2.0 에서는 Generation Type.IDENTITY 옵션을 추가해야만 auto_increment가 적용된다.

 

 

@Column

  • 테이블의 컬럼을 나타내며 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 컬럼이 된다.
  • 이 어노테이션을 사용하는 이유는 기본값 외에 추가로 변경이 필요한 옵션이 있을 때 사용한다.
  • 문자열의 경우 255가 기본 사이즈인데, 사이즈를 500으로 늘리고 싶거나 타입을 TEXT로 변경하고 싶을 때 사용된다.

 

 

@NoArgsConstructor

  • 기본 생성자를 자동으로 추가하는 어노테이션
  • public Posts() {}와 같은 효과이다.

 

 

@Getter

  • 클래스 내 모든 필드의 Getter 메소드를 자동 생성하는 어노테이션

 

 

@Builder

  • 해당 클래스의 빌더 패턴 클래스를 생성는 어노테이션
  • 생성자 상단에 선언 시 생성자에 포함된 필드만 빌더에 포함된다.

 

 

참고

웬만하면 Entity의 PK는 Long타입의 Auto_increment를 추천한다

 

(MySQL을 기준으로 이렇게 하면 bigint 타입이 된다.)

 

주민등록번호와 같이 비즈니스상 유니크 키나, 여러 키를 조합한 복합키로 PK를 잡을 경우 난감한 상황이 종종 발생한다.

 

  1. FK를 맺을 때 다른 테이블에서 복합키 전부를 갖고 있거나, 중간 테이블을 하나 더 둬야하는 상황이 발생한다.
  2. 인덱스에 좋은 영향을 끼치지 못한다.
  3. 유니크한 조건이 변경될 경우 PK 전체를 수정해야 하는 일이 발생한다.

 

위 Posts 클래스에는 한 가지 특이점이 있다. 바로 Setter 메소드가 없다는 점이다.

 

자바빈 규약을 생각하면 getter/setter를 무작정 생성하는 경우가 있다.

 

이렇게 되면 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지

 

코드상으로 명확하게 구분할 수 없어, 차후 기능 변경시 복잡해진다.

 

그래서 Entity 클래스에서는 절대 Setter 메소드를 만들지 않는다.

 

대신 해당 필드의 값 변경이 필요하면 명확히 그 목적과 의도를 나타낼 수 있는 메소드를 추가해야만 한다.

 


예시

주문 취소 메소드를 만든다고 가정해보면 다음 코드로 비교할 수 있다.

 

잘못된 사용의 예

public class Order {
	public void setStatus(boolean status) {
    	this.status = status
	}
}

public void 주문서비스의_취소이벤트() {
	order.setStatus(false);
}

 

올바른 사용의 예

public class Order {
	public void cancleOrder() {
    	this.status = false;
	}
}

public void 주문서비스의_취소이벤트() {
	order.cancleOrder();
}

 


 

그렇다면 Setter가 없는 상황에서 어떻게 값을 채워 DB에 삽입해야하는가?

 

기본적인 구조생성자를 통해 최종값을 채운 후 DB에 삽입하는 것이며,

 

값 변경이 필요한 경우 해당 이벤트에 맞는 public 메소드를 호출하여 변경하는 것을 전제로 한다.

 

위 코드에서는 생성자 대신에 @Builder를 통해 제공되는 빌더 클래스를 사용한다.

 

생성자나 빌더나 생성 시점에 값을 채워주는 역할은 똑같고,

 

생성자의 경우 지금 채워야 할 필드가 무엇인지 명확히 지정할 수 없다.

 


예시

public Example(String a, String b) {
	this.a = b;
    this.b = a;
}

 

위와 같은 생성자가 있다면 개발자가 new Example(b, a)처럼 a와 b의

 

위치를 변경해도 코드를 실행하기 전까지는 문제를 찾을 수 없다.

 


예시

Example.builder()
	.a(a)
    	.b(b)
    	.build();

 

하지만 위와 같이 빌더를 사용하게 되면 어느 필드에 어떤 값을 채워야 할지 명확하게 인지할 수 있다.

 

 

Posts Class의 생성이 끝났다면, Posts 클래스로 Database를 접근하게 해줄 JpaRepository를 생성한다.

 

[패키지 우클릭 -> 새로 만들기 -> Java 클래스 -> 인터페이스 선택 후 생성]

 

PostsRepository Interface 파일 신규 생성

 

PostsRepository 인터페이스 안에 아래와 같이 코드를 작성해준다.

 

package com.tistory.tlseoqja.domain.posts;

import org.springframework.data.jpa.repository.JpaRepository;

public interface PostsRepository extends JpaRepository<Posts, Long> {
}

 

보통 iBatis나 MyBatis 등에서 Dao라고 불리는 DB Layer 접근자이다.

 

JPA에서는Repository라고 부르며 인터페이스로 생성한다.

 

단순히 인터페이스를 생성한 후 JpaRepository<Entity 클래스, PK 타입>를 상속하면 기본적인 CRUD 메소드가 자동으로 생성된다.

 

@Repository 어노테이션을 추가할 필요도 없다.

 

여기서 주의할 점은 Entity 클래스와 기본 Entity Repository는 함께 위치해야 한다는 점이다.

 

둘은 아주 밀접한 관계이고, Entity 클래스는 기본 Repository없이는 제대로 역할을 할 수가 없다.

 

나중에 프로젝트 규모가 커져 도메인별로 프로젝트를 분리해야 한다면

 

Entity 클래스와 기본 Repository는 함께 움직여야 하므로 도메인 패키지에서 함께 관리한다.

 

 

 

 

Spring Data JPA 테스트 코드 작성하기

test 디렉토리에 domain.posts 패키지를 생성하고, PostsRepositoryTest Class를 생성한다.

test 디렉토리의 하위 패키지 domain.posts와 PostsRepositoryTest Class 신규 생성

 

PostsRepositoryTest Class 안에 아래와 같이 코드를 작성한다.

 

package com.tistory.tlseoqja.domain.posts;

import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {

    @Autowired
    PostsRepository postsRepository;

    @After
    public void cleanup() {
        postsRepository.deleteAll();
    }

    @Test
    public void 게시글저장_불러오기() {
        //given
        String title = "테스트 게시글";
        String content = "테스트 본문";

        postsRepository.save(Posts.builder()
                .title(title)
                .content(content)
                .author("jojoldu@gmail.com")
                .build());

        //when
        List<Posts> postsList = postsRepository.findAll();

        //then
        Posts posts = postsList.get(0);
        assertThat(posts.getTitle()).isEqualTo(title);
        assertThat(posts.getContent()).isEqualTo(content);
    }

}

 


코드설명

 

@After

  • jUnit에서 단위 테스트가 끝날 때마다 수행되는 메소드를 지정해주는 어노테이션
  • 보통은 배포 전 전체 테스트를 수행할 때 테스트간 데이터 침범을 막기 위해 사용한다.
  • 여러 테스트가 동시에 수행되면 테스트용 데이터베이스인 H2에 데이터가 그대로 남아있어 다음 테스트 실행 시 테스트가 실패할 수 있다.

 

 

@postsRepository.save

  • 테이블 posts에 insert/update 쿼리를 실행한다.
  • id 값이 있다면 update가,없다insert 쿼리가 실행된다.

 

 

@postsRepository.findAll

  • 테이블 posts에 있는 모든 데이터를 조회하는 메소드

 

별다른 설정 없이 @SpringBootTest를 사용할 경우 H2 데이터베이스를 자동으로 실행해준다.

 

이 테스트 코드 역시 실행할 경우 H2가 자동으로 실행된다.

 

게시글저장_불러오기() 테스트 코드 실행 성공 화면

 

그렇다면 '이 코드에서 실제로 실행된 쿼리는 어떤 형태일까?'라는 의문이 생길 수도 있다.

 

실행된 쿼리를 로그로 볼 수 있게 ON/OFF하는 설정이 있다.

 

다만, 이런 설정들을 Java 클래스로 구현할 수 있으나

 

스프링 부트에서는 application.properties, application.yml 등의 파일로

 

한 줄의 코드로 설정할 수 있도록 지원하고 권장해준다.

 

 

 

 

로그 표출 코드 작성

'src/main/resources' 디렉토리에 'application.properties' 파일을 생성한다.

 

[resources 디렉토리가 없을 경우 새로 만들기 -> 경로에 들어가면 resources 폴더가 목록에 있고 더블 클릭하면 생성된다.]

 

resources 폴더 안에 application.properties 파일 신규 생성

 

application.properties 파일 안에 아래의 코드를 작성해준다.

 

spring.jpa.show_sql=true

 

테스트 코드를 다시 실행시키면 아래 사진과 같이 콘솔에서 쿼리 로그를 확인할 수 있게 된다.

 

콘솔 로그 ON 설정 후 테스트 코드 실행시 콘솔창에서 확인되는 로그

 

위 코드에서 create table 쿼리를 보면 id bigint generated by default as identity라는 옵션으로 생성된다.

 

이는 H2의 쿼리 문법이 적용되었기 때문이다.

 

H2는 MySQL의 쿼리를 수행해도 정상적으로 작동하기 때문에

 

이후 디버깅을 위해서 출력되는 쿼리 로그를 MySQL버전으로 변경하겠다.

 

이 옵션 역시 application.properties에 아래의 코드를 추가해주면 설정이 가능하다.

 

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

 

코드를 추가한 후 테스트 코드를 실행하면 아래와 같은 콘솔 메세지가 출력된다.

 

출력되는 쿼리 로그를 MySQL 버전으로 변경한 후 테스트 코드 실행시 콘솔 출력 메세지

 

 

JPA와 H2에 대한 기본적인 기능과 설정을 마쳤다.

 

 

 

다음번에는 게시판의 등록/수정/조회 기능 API의 생성 방법을 포스팅 하겠다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90

댓글