[혼자 구현하는 웹 서비스] 3장 JPA로 데이터베이스 다뤄보자

2020. 7. 6. 10:24책/스프링 부트와 AWS로 혼자 구현하는 웹 서비스

3장 JPA

JPA가 무엇인지는 알고 있었지만, 직접 사용해보게 된 것은 처음이었다.

실제로 스프링 프로젝트를 JPA 없이 몇 번 진행해 보았는데, 쿼리문을 작성하는데 

꽤나 많은 시간이 필요했다. 자사 서비스를 개발하는 대부분의 곳에서

SpringBoot와 JPA를 전사 표준으로 사용한다고 하니, 개념을 잘 익히고 연습해 봐야겠다.

 

1. JPA란?

현대의 웹 애플리케이션에서 관계형 데이터베이스는 빠질 수 없는 요소이다. 관계형 데이터베이스는 쉽게 말해 키와 값으로 이루어진 데이터 베이스 형태이다. 관계형 데이터베이스는 SQL만 인식할 수 있기 때문에, 프로젝트에는 애플리케이션 코드보다 SQL이 더 많아지는 상황이 벌어지게 되었다. 실제 현업에서는 수십, 수백 개의 테이블이 있는데, 각각의 테이블에 CRUD SQL을 모두 생성해 주어야 하므로, 굉장히 많은 시간이 필요할뿐더러 유지 보수하기도 힘들어졌다. 프로그래밍에서 이러한 단순 반복 작업만큼은 용납되어서는 안 되는 것인데 말이다.

 

단순 반복 작업 외에도 패러다임 불일치 문제가 있다. 관계형 데이터베이스와 객체지향적 프로그래밍의 패러다임이 서로 불일치하는 것이다. 관계형 데이터베이스는 어떻게 데이터를 저장할지에 초점이 맞춰진 기술이고, 객체지향 프로그래밍 언어는 기능과 속성을 한 곳에서 관리하는 기술이다. 다양한 객체 모델링을 데이터베이스로 구현하기는 쉽지 않은 것이다. 그러다 보니 웹 애플리케이션은 점점 데이터 베이스 모델링에만 집중하게 되었는데, 이것을 해결하기 위해 나온 것이 JPA이다.

 

JPA는 서로 지향하는 바가 다른 관계형 데이터베이스와 객체지향 프로그래밍 언어를 중간에서 패러다임 일치를 시켜주기 위한 기술이다. 즉, 개발자는 객체지향적으로 프로그래밍을 하고, JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행해준다. 개발자는 더 이상 데이터베이스 모델링이 아닌 객체지향적으로 코드를 표현함으로써, SQL에 종속적인 개발을 하지 않아도 되게 되었다.

 

2. SpringData JPA

JPA는 인터페이스로서 자바 표준 명세서이다. 이러한 인터페이스를 사용하기 위해서는 구현체가 필요하다. JPA인터페이스의 대표적인 구현체는 Hibernate, Eclipse Link 등이 있다. 그리고, 이러한 구현체들을 좀 더 쉽게 사용하기 위해 추상화시킨 것이  SpringData JPA이다. 관계를 표현해보자면 JPA <- Hibernate <- SpringData JPA로 표현할 수 있다. Hibernate와 SpringData JPA를 사용하는 것에는 큰 차이가 없지만, 이렇게 한 단계 더 감싸 놓은 기술이 등장한 이유는 구현체 교체의 용이성과 저장소 교체의 용이성 때문이다. '구현체 교체의 용이성'이란 Hibernate 외에 다른 구현체로 쉽게 교체하기 위함이고, '저장소 교체의 용이성'이란 관계형 데이터베이스 외에 다른 저장소로 쉽게 교체하기 위함이다. 

 

SpringData JPA를 사용하기 위해서 의존성을 등록해야 한다.

 

첫 의존성이 스프링 부트용 추상화 라이브러리를 받은 것이고, h2는 인메모리 관계형 데이터베이스로 별도의 설치 없이 관리할 수 있다. (스프링 부트 강의를 들으며 정리한 내용이 따로 있다.)

 

3. JPA를 사용하기 위한 클래스

Jpa를 사용하기 위해 domain패키지에 Posts클래스를 만들어 본다.

 

1. @Entity : JPA의 어노테이션이다. 테이블과 링크될 클래스임을 나타내 주는 역할을 한다. 기본값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍으로 테이블 이름을 매칭한다.

 

2. @Id : 해당 테이블의 Primary Key 필드를 나타낸다.

 

3. @GeneratedValue : Primary Key 필드의 생성규칙을 나타낸다. 여기서 GenerationType.IDENTITY를 설정해줘야 auto_increment가 적용된다. 

 

4. @Column : 테이블의 칼럼을 나타내며 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 칼럼이 된다. 

위와 같이 사용하는 이유는 기본값 외에 추가로 변경이 필요한 옵션이 있는 경우이다. (문자열의 경우 원래 length값이 255인데, 500으로 바꾸고 싶다거나, 타입을 "TEXT"로 바꾸고 싶다거나)

 

5. @NoArgsConstructor : 기본 생성자를 자동 추가해준다.

 

6. @Getter : 클래스 내 모든 필드의 Getter 메소드를 자동 생성해준다.

 

7. @Builder : 해당 클래스의 빌더 패턴 클래스를 생성한다. 생성자 상단에 선언 시 생성자에 포함된 필드만 빌더에 포함한다.

 

5,6,7번은 롬복의 어노테이션인데, 코드 변경량을 최소화시켜 주기 때문에 적극적으로 사용하는 것이 좋다.

 

위의 Posts클래스는 Setter 메소드가 없다는 특징이 있다. 자바빈 규약을 생각하면서 Getter/Setter를 무작정 생성하는 경우가 많은데, 이렇게 될 경우 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수 없게 되어 차후 기능 변경시 매우 복잡해진다고 한다. 따라서 Entity 클래스에서는 절대 Setter메소드를 만들지 않는 것이 좋다고 한다. 대신, 해당 필드의 값 변경이 필요한 경우 목적과 의도를 잘 나타낼 수 있는 메소드를 따로 만들어 주어야 한다.

 

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

기본적으로는 생성자를 통해 최종 값을 채운 후 DB에 넣으며, 값 변경이 필요한 경우 해당 이벤트에 맞는 메소드를 호출하여 변경하는 것을 전제로 한다. 이 책에서는 생성자 대신 @Builder를 통해 제공되는 클래스를 사용하는데, 생성시점에 값을 채워주는 것은 동일하지만 어떠한 필드에 어떠한 값을 넣어주어야 할지 명확히 지정해 줄 수 있다는 장점이 있다. 

 

4. 데이터베이스 연동

Posts클래스를 잘 구성했다면, 이 클래스로 Database에 접근하게 해 줄 JpaRepository를 생성한다.

이렇게 <Entity 클래스타입, Primary Key 타입> 속성으로 JpaRepository를 상속하면 기본적인 CRUD 메소드가 자동으로 생성된다. 보통 MyBatis를 사용할 때 Dao로 불리던 DB Layer 접근자 역할을 한다고 보면 되겠다.

Entity클래스와 이러한 기본 Entity Repository는 꼭 함께 위치해야 하는데, 그렇지 않으면 Entity 클래스가 제대로 역할을 할 수 없게 된다. 따라서 도메인 패키지에 함께 관리하여 위치를 변경할 때도 함께 이동할 수 있도록 구성해야 한다. 

 

Respository의 save, findAll기능이 잘 동작하는지 테스트해본다.

 

1. @After : Junit에서 단위 테스트가 끝날 때마다 수행되는 메소드를 지정하는 어노테이션이다. 여러 테스트가 동시에 수행될 때, 테스트용 데이터베이스인 h2에 데이터가 그대로 남아있어서 다음 테스트 실행에 영향을 주기 때문에 데이터베이스를 비우는 cleanup()을 만들어 수행하도록 하였다.

 

2. postsRepository.save : 테이블 posts에 insert/update 쿼리를 실행하도록 한다. id값이 있다면 update가 실행되고, 없다면 insert쿼리가 실행된다.

 

3. postsRepository.findAll : 테이블 posts에 있는 모든 데이터를 조회해온다. 

 

5. JPA Auditing으로 시간 자동화

보통 엔티티에는 해당 데이터의 생성시간과 수정시간을 포함한다. 언제 만들어졌는지, 언제 수정되었는지 등은 차후 유지보수에 있어 중요한 정보가 될 수 있기 때문이다. 이러한 정보를 반복적으로 작성해야하는 것을 막기 위해 JPA Auditing을 사용할 수 있다. 

 

Java 8 부터는 LocalDate와 LocalDateTime 이라는 날짜 타입이 등장하였다.

다음과 같은 BaseTimeEntity는 모든 Entity의 상위 클래스가 되어 Entity들의 createdDate, modifiedDate를 자동으로 관리한다.

따라서, 위치해야 할 곳은 domain패키지이다. (타 entity들은 domain.xxx 에 위치)

 

1. @MappedSuperclass : JPA Entity 클래스들이 BaseTimeEntity을 상속할 경우 BaseTimeEntity에 있는 필드들도 칼럼으로 인식하도록 해준다.

 

2. @EntityListeners(AuditingEntityListener.class) : BaseTimeEntity 클래스에 Auditing 기능을 포함시킨다.

 

3. @CreatedDate : Entity가 생성되어 저장될 때 시간이 자동으로 저장된다.

 

4. @LastModifiedDate : 조회한 Entity의 값을 수정할 때 시간이 자동으로 저장된다.

 

앞서 만들었던 Posts 가 BaseTimeEntity를 상속받도록 변경한다.

이후, JPA Auditing 어노테이션들이 모두 활성화 되려면, 메인 클래스에 활성화 어노테이션 @EnableJpaAuditing을 추가해주어야 한다.

 

위와 같이 테스트 코드를 작성하고 콘솔을 확인해 보면 시간이 잘 표시되는 것을 확인할 수 있다.