티스토리 뷰





반응형

JPA 소개

Oracle, mySql, msSql 등을 쓰지 않는 웹 애플리케이션은 거의 없습니다.
그러다보니 객체를 관계형 데이터베이스에서 관리하는 것이 무엇보다 중요합니다.
관계형 데이터베이스가 계속해서 웹 서비스의 중심이 되면서 모든 코드는 SQL 중심으로 되어갑니다.
이는 관계형 데이터베이스가 SQL만 인식할 수 있기 때문인데, SQL만 가능하니 테이블마다 기본적이 CRUD를
매번 생성해야합니다. 예를 들어 User라는 테이블이 추가되면 기본적으로 아래와 같은 CRUD가 생성됩니다.

select * from user 
insert into user vales(...) 
update user set .. 
delete from user 


이런 문제말고도 패러다임 불일치 라는 문제도 있습니다. 

관계형 데이터베이스는 어떻게 데이터를 저장할 것인가에 초점이 맞춰진 기술입니다.
반대로 객체지향은 메시지를 기반으로 기능과 속성을 한 곳에서 관리하는 기술입니다.
추상화, 캡슐화, 정보은닉, 다형성등등 이것을 관계형 데이터베이스로 표현할 수 있을까요?

그래서 중간에서 패러다임을 일치 시켜주기위해 나온것이 JPA입니다.
즉 개발자는 객체지향적으로 프로그래밍하고, JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해줍니다.
더는 SQL에 종속적인 개발을 하지 않아도 됩니다.

인터페이스인 JPA를 사용하기 위해서는 구현체가 필요합니다. 대표적으로 하이버네이트가 있습니다.
하지만 스프링에서는 Spring Data JPA기술을 사용합니다. 이들의 관계는 아래와 같습니다.

Spring Data JPA => Hibernate => JPA

사실 하이버네이트를 쓰는것과 스프링 데이터 JPA를 쓰는것은 차이가 없습니다. 

하지만 이렇게 굳이 한번더 감싸놓은 이유는 크게 2가지입니다.

1. 구현체 교체의 용이성 
2. 저장소 교체의 용이성 


현재는 하이버네이트가 대세지만 다른 대세가 생길경우 아주 손쉽게 교체가 가능해집니다. 중간다리인 하이버네이트만 바꿔주면 되니까 기존에 사용하는 코드들을 일일히 수정할 필요가 없어집니다.

서비스 초기에는 관계형 데이터베이스로 모든 기능을 처리했지만, 점점 트래픽이 많아져 관계형 데이터베이스로 도저히 감당 안될 때가 있습니다. 이때 몽고디비로 교체가 필요하면 개발자는 의존성만 교체해주면 됩니다.

프로젝트에 Spring Data JPA 적용해보기

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'com.h2database:h2'                                      
}

먼저 그래들에 필요한 의존성을 추가합니다. 처음 추가한 의존성은 스프링용 JPA 추상화 라이브러리이며, 스프링부트 버전에 맞춰 자동으로 버전관리를 해줍니다.

두번째는 인메모리 관계형 데이터베이스인데 간단하게 메모리로 디비를 쓰고싶을 때 사용됩니다.

@Entity 어노테이션

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
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;
  }
}

여기서 Potst 클래스는 실제 DB의 테이블과 매칭될 클래스입니다. JPA를 사용하시면 DB 데이터에 작업할 경우 실제 쿼리를 날리기보다, 이 Entity 클래스의 수정을 통해 작업합니다. 그리고 테이블의 카멜케이스 이름을 언더스코어네이밍으로 테이블 이름을 매칭합니다.

1. @Id 

해당 테이블의 PK를 지정합니다.

 

2. @GeneratedValue 

PK 생성규칙을 나타냅니다. 부트 2.0에서는 GenerationType.IDENTITY을 추가해야만 auto_increment가 적용

 

3. @Column

테이블의 칼럼을 나타내며 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 컬럼이 됩니다. 하지만 사용하는 이유는, 기본값 외에 추가로 변경이 필요한 옵션이 있으면 사용합니다.

ex) VARCHAR(255)가 기본값인데, 사이즈를 500으로 늘리거나 타입을 TEXT로 변경하고 싶을 경우

 

이 Posts 클래스에는 한 가지 특이점이 있습니다. 바로 Setter가 없다는 것입니다. getter/setter는 무작정 생성하는 경우가 많습니다. 이렇게 되면 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수가 없어, 차후 기능 변경 시 정말 복잡해집니다. 그래서 Entity 클래스에서는 절대 Setter 메소드를 만들지 않습니다. 대신 해당 필드의 값 변경이 필요하면 명확히 그 목적과 의도를 나타낼 수 있는 메소드를 추가 해야만 합니다.

JPA의 Repository

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 없이는 제대로 역할을 할 수가 없습니다.

테스트 코드 작성해보기

import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
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;

@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("ian").build());

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

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

@After

단위 테스트가 끝날 때마다 수행되는 메소드를 지정, 여러 테스트가 동시에 수행되면 테스트용 DB인 H2에 데이터가 그대로 남아 있어 다음 테스트 실행 시 테스트가 실패할 수 있습니다.

 

postsRepository.save

테이블 posts에 id값이 있으면 insert 없으면 update 쿼리 실행

실제코드 작성하기

src/main/java/.../web/PostsApiController

import com.bahngFamily.jihoon.springboot.service.posts.PostsService;
import com.bahngFamily.jihoon.springboot.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class PostsApiController {

  private final PostsService postsService;

  @PostMapping("/api/v1/posts")
  public Long save(@RequestBody PostsSaveRequestDto requestDto){
    return postsService.save(requestDto);
  }
}

src/main/java/.../service/PostsService

import com.bahngFamily.jihoon.springboot.domain.posts.Posts;
import com.bahngFamily.jihoon.springboot.domain.posts.PostsRepository;
import com.bahngFamily.jihoon.springboot.web.dto.PostsSaveRequestDto;
import javax.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class PostsService {
  private final PostsRepository postsRepository;

  @Transactional
  public Long save(PostsSaveRequestDto requsetDto){
    return postsRepository.save(requsetDto.toEntity()).getId();
  }
}

그런데 여기서 이상한 점이 하나 있습니다. 스프링을 어느정도 써본 분들이라면 Controller와 Service에 @Autowired가 없는 것이 어색하게 느껴질 겁니다. 스프링에선 Bean을 주입받는 방식들이 다음과 같습니다.

@Autowired, setter, 생성자

 

이 중 가장 권하는 방식이 생성자로 주입받는 방식입니다.(@Autowired는 권장하지 않습니다.) 그러면 앞에서 생성자는 어디 있을까요? 바로 @RequiredArgsConstructor 해결해 줍니다. final이 선언된 모든 필드를 인자값으로 하는 생성자를 룸복의 @RequiredArgsConstructor 가 대신 생성해 준 것입니다.

 

생성자를 직접 안 쓰고 룸복 어노테이션을 사용한 이유는 간단합니다. 해당 클래스의 의존성 관계가 바뀔 때마다 생성자 코드를 계속해서 수정하는 번거로움을 해결하기 위함입니다.

 

자 이제 컨트롤러와 서비스에서 사용할 Dto 클래스를 생성하겠습니다.

@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
  private String title;
  private String content;
  private String author;

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

  public Posts toEntity(){
    return Posts.builder()
        .title(title)
        .content(content)
        .author(author)
        .build();
  }
}

여기서 Entity 클래스와 거의 유사한 형태임에도 Dto 클래스를 추가로 생성했습니다. 하지만, 절대로 Entity 클래스를 Request/Response 클래스로 사용해서는 안 됩니다. Entity 클래스는 데이터베이스와 맞닿는 핵심 클래스 입니다. Entity 클래스를 기준으로 테이블이 생성되고, 스키마가 변경됩니다. 화면 변경은 아주 사소한 기능인데, 이를 위해 테이블과 연결된 Entity 클래스를 변경하는 것은 너무 큰 변경입니다.

 

그렇게 때문에 View Layer와 DB Layer의 역할 분리를 철저하게 하는게 좋습니다. 꼭 Entity 클래스와 Controller에서 쓸 Dto는 분리해서 사용해야 합니다.

등록기능 테스트 코드 작성

import static org.assertj.core.api.Assertions.assertThat;
import com.bahngFamily.jihoon.springboot.domain.posts.Posts;
import com.bahngFamily.jihoon.springboot.domain.posts.PostsRepository;
import com.bahngFamily.jihoon.springboot.web.dto.PostsSaveRequestDto;
import java.util.List;
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.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest {

  @LocalServerPort
  private int port;

  @Autowired
  private TestRestTemplate restTemplate;

  @Autowired
  private PostsRepository postsRepository;

  @After
  public void tearDown() throws Exception {
    postsRepository.deleteAll();
  }

  @Test
  public void Posts_등록된다() throws Exception {
    //given
    String title = "title";
    String content = "content";
    //String author = "author";
    PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder().title(title).content(content).build();

    String url = "http://localhost:" + port +"/api/v1/posts";

    //when
    ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);

    //then
    assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
    assertThat(responseEntity.getBody()).isGreaterThan(0L);

    List<Posts> all = postsRepository.findAll();
    assertThat(all.get(0).getTitle()).isEqualTo(title);
    assertThat(all.get(0).getContent()).isEqualTo(content);
  }
}

Api Controller를 테스트하는데 HelloController와 달리 @WebMvcTest를 사용하지 않았습니다. 그 이유는 @WebMvcTest는 JPA 기능이 작동하지 않기 때문입니다. 지금같이 JPA 기능까지 한번에 테스트할 때는 @SpringBootTest와 TestRestTemplate을 사용하면 됩니다.

 

 

 

출처: 스프링부트와 AWS로 혼자 구현하는 웹 서비스

반응형
댓글
반응형
최근에 달린 댓글
글 보관함
Total
Today
Yesterday
최근에 올라온 글
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31