본문 바로가기
개발/스프링

[혼자 구현하는 웹서비스] 3. (1) JPA와 데이터베이스

by 카펀 2021. 7. 8.

0. 개요

1. JPA 소개

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

 

 

0. 개요

웹 서비스와 데이터베이스는 뗄 수 없는 관계다. 관계형 데이터베이스 (RDBMS)와 객체지향 프로그래밍 (OOP)를 같이 사용할 수 있을까?

 

1. JPA 소개

웹 서비스를 개발하며 데이터베이스를 사용하는 방법은 크게 두 가지가 있다.

하나는 MyBatis와 같은 SQL 매퍼 사용하기, 또 하나는 ORM을 이용하여 객체를 매핑하기.

 

MyBatis도 많이 사용되고 있는 서비스지만, 개발을 하는 시간보다 SQL을 다루는 시간이 더 많아지게 된다.

JPA (Java Persistence API)라는 자바 표준 ORM (Objecxt Relational Mapping)을 이용하여, 객체를 매핑하는 방법이 존재한다.

최근 현업에서 많이 사용되고 있기도 하다.

 

웹 어플리케이션을 제작하며 데이터베이스를 다루는 일은 필수이다. Oracle, MySQL, MSSQL 등을 사용하지 않는 경우는 없다.

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

RDBMS가 웹 서비스의 중심이 되며, 모든 코드는 SQL 중심이 되어간다. 이는 RDBMS가 SQL만을 인식할 수 있기 때문이다.

현업에서는 수십, 수백개의 테이블이 있고, 이 테이블의 몇 배의 SQL을 만들고 유지보수 하는것은 필수적이지만 쉬운 일이 아니다.

또한, 객체지향 프로그래밍 언어와 관점부터가 다른 RDBMS는 패러다임 불일치 문제를 겪는다.

RDBMS는 어떻게 데이터를 저장할지에 초점이 맞춰진 기술이고, 객체지향은 메세지를 기반으로 기능과 속성을 한 곳에서 관리하는 기술이다.

따라스 방향이 다른 둘을 한 번에 사용하려니 쉽지 않을 수밖에 없다.

 

예를 들어 객체지향 언어에서, 부모가 되는 객체를 가져오는 방법은 다음과 같다.

 

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 등 다양한 객체 모델링을 DB에서는 구현할 수 없다.이러다 보니 웹 개발은 점점 DB 모델링에 신경을 쓰게 된다.

 

JPA는 이를 해결하기 위해 등장하였다. 중간에서 페러다임 일치를 시켜주는 매개채라고 생각하면 된다.

개발자는 객체지향적으로 코드를 작성하고, JPA가 이를 RDBMS에 맞게 SQL을 대신 생성해서 실행해 준다.

JPA는 인터페이스로써, 자바 표준 명세서이다. 따라서 인터페이스인 JPA를 사용하기 위해서는 구현체가 필요하다.

구현체의 예로는 Hibernate, Eclipse Link 등이 있다.

구현체를 더 쉽게 사용하고자 추상화시킨 Spring Data JPA라는 모듈이 있다. 즉, JPA <- Hibernate <- Spring Data JPA의 관계를 가진다.

Hibernate를 쓰는 것과 Spring Data JPA를 쓰는 것은 큰 차이가 없지만, Spring 진영에서는 Spring Data JPA를 굳이 따로 만들었고, 사용을 권장하고 있다. 이는 크게 두 가지 이유가 있다.

첫째로는 '구현체 교체의 용이성' 이 있다. Hibernate 이외에 다른 구현체로 쉽게 교체하기 위함이다. 언젠가 Hibernate보다 더 좋은 JPA 구현체가 대세로 떠오를 때, Spring Data JPA를 쓰는 중이라면 손쉽게 교체할 수 있다. Spring Data JPA 내부에서 구현체 매핑을 지원해 주기 때문이다.

둘째로는 '저징소 교체의 용이성' 이 있다. RDBMS 이외 다른 DBMS로 쉽게 교체할 수 있다. 점점 트래픽이 많아져 RDBMS로는 감당이 안 될 때가 생긴다. MongoDB로의 교체가 필요한 상황이 있다면, Spring Data JPA에서 Spring Data MongoDB로 의존성만 교체하면 된다.

 

앞으로 하나의 게시판 (웹 어플리케이션)을 만들고, AWS에 무중단 배포하는 과정을 진행해보려 한다.

 

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

 

먼저 build.gradle에 아래와 같이 새로운 dependencies (의존성)을 등록하자.

 

아래의 두 줄이 새로 추가된 dependencies이다.

spring-boot-starter-data-jpa는 스프링 부트용 Spring Data JPA 추상화 라이브러리이다.

h2는 인메모리 RDBMS이며, 별도의 설치 없이 프로젝트를 dependencies만으로 관리하게 해 준다. 메모리에서 실행되기 때문에 어플리케이션을 재시작할 때마다 초기화된다는 점을 이용하여 테스트 용도로 많이 사용된다. JPA의 테스트, 로컬 환경에서의 구동 등을 위해 사용하고자 한다.

 

새로운 dependencies가 등록되었다면, 본격적으로 JPA 기능을 사용해 볼 차례이다.

새로운 패키지를 만들자.

 

springboot 패키지 아래에 domain 패키지를 만든다.

 

이 domain 패키지는 도메인을 담을 패키지이다. 도메인이란, 게시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 혹은 문제 영역을 의미한다.

MyBaits와 같은 쿼리 매퍼와 비교해보자면, dao 패키지와 유사하지만 조금 다르다. xml에 쿼리를 담고, 클래스는 오로지 쿼리의 결과만 담던 일들이, 모두 도메인 클라스라고 불리는 곳에서 해결하게 된다.

 

domain 패키지 아래에 posts 패키지를 만들고, 그 아래에 Posts 클라스를 만들자.

 

domain 패키지 아래에 posts 패키지, 그리고 그 아래에 Posts 클라스를 만들었다.

Posts 클라스에는 다음과 같이 코드를 작성하자.

 

Posts.java

어노테이션을 보자. @Entity는 JPA의 어노테이션이며, @Getter와 @NoArgsConstructor는 앞서 살펴본 lombok의 어노테이션이다.

lombok은 필수 어노테이션은 아니므로, lombok이 더 이상 필요 없을 경우 쉽게 삭제할 수 있다.

 

Posts 클라스는 실제 DB의 테이블과 매칭될 클라스이다. Entity 클라스라고도 한다.

JPA를 사용하면, DB 데이터에 작업할 경우, 실제 쿼리를 이용하기보다는 이 Entity 클라스의 수정을 통해 작업한다.

 

다음은 사용된 어노테이션에 대한 설명이다.

  • Entity: 테이블과 링크될 클라스임을 나타낸다. 클라스의 카멜케이스 이름을 언더스코어 네이밍으로 테이블 이름에 매칭한다.
  • Id: 해당 테이블의 PK  필드를 나타낸다.
  • GeneratedValue: PK의 생성 규칙을 나타낸다. 스프링 부트 2.0에서는 GenerationType.Identity를 추가해야 auto_increment가 된다.
  • Column: 테이블의 칼럼을 나타낸다. 굳이 선언하지 않아도 되지만, 기본값 외에 추가로 변경이 필요한 옵션이 있으면 사용한다.
  • NoArgsConstructor: lombok의 어노테이션으로, 기본 생성자 자동 추가를 한다.
  • Getter: lombok의 어노테이션으로, 클래스 내 모든 필드의 getter 메소드를 자동으로 생성한다.
  • Builder: lombok의 어노테이션으로, 해당 클라스의 빌더 패턴 클라스를 생성한다. 생성자 상단에 선언 시 생성자에 포함된 필드만 빌더에 포함된다.

서비스 초기 구축 단계에선 테이블 (Entity) 설계가 종종 변경되는데, 이때 lombok의 어노테이션들은 코드 변경량을 최소화시켜 주므로 적극적으로 사용하도록 한다.

 

이 Posts 클라스에는 Setter 메소드가 없다.

Entity 클라스에서는 절대 Setter 메소드를 만들지 않는다. 해당 클라스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수 없고, 따라서 차후 기능 변경 시 매우 복잡해지기 때문이다.

아래의 예시를 참고하자.

 

잘못된 예:

 

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

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

 

올바른 예:

 

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

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

 

그렇다면, Setter가 없는 이 상황에서 어떻게 값을 채워 DB에 삽입 (insert)해야 할까?

 

기본적인 구조는 "생성자를 통해" 최종값을 채운 후 DB에 삽입하는 것이며, 값의 변경이 필요한 경우 해당 이벤트에 맞는 public 메소드를 호출하여 변경하는 것을 전제로 한다.

여기서는 @Builder를 통해 제공되는 빌더 클라스를 사용한다. 역할은 같지만, 생성자의 경우 지금 채워야 할 필드가 무엇인지 명확히 지정할 수 없다.

 

생성자의 문제점을 다음 예시를 통해 알아보자.

 

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

 

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

 

위처럼 어느 필드에 어떤 값을 채워야 할지 명확하게 인지할 수 있다.

 

Posts 클라스 생성이 끝났다면, Posts 클라스로 DB를 접근하게 해 줄 JpaRepository를 만든다.

우클릭 후 New > Java Class를 선택하고, 아래와 같이 Interface를 골라주면 된다.

 

Interface를 선택한다.

 

PostsRepository.java

보통 쿼리 매퍼에서 Dao라고 불리는 DB Layer 접근자이다. JPA에서는 Repository라고 부르며, 인터페이스로 생성한다.

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

주의할 점은 Entity 클라스와 기본 Entity Repository는 함께 위치해야 한다는 점이다. 둘은 아주 밀접한 관계이며, Entity 클라스는 기본 Repository 없이는 제 기능을 할 수 없다.

프로젝트 규모가 커져, 도메인별로 프로젝트를 분리해야 한다면, Entity 클라스와 기본 Repository는 함께 움직여야 하므로, 도메인 패키지에서 함께 관리한다.

 

다음 글에서는 간단하게 테스트 코드로 기능을 검증해 보겠다.

 

 

 

*이 글은 '스프링 부트와 AWS로 혼자 구현하는 웹 서비스' (프리렉, 이동욱 저)를 공부하며 내용을 정리한 글입니다.

댓글