코딩하는 문과생

[Spring Boot] JPA, 데이터베이스 연동 본문

웹 프로그래밍/Spring Boot

[Spring Boot] JPA, 데이터베이스 연동

코딩하는 문과생 2020. 11. 30. 23:31

※ 해당 글은 튜토리얼이 아닌 헷갈리거나 중요한 개념 위주로 정리한 글입니다. 

 

[JPA]

: 자바 표준 ORM 기술

관계형 DB는 어떻게 데이터를 저장할 지에 초점이 있고, 객체지향프로그래밍은 기능과 속성에 초점이 맞춰져 있다.

즉, 데이터베이스와 스프링이 지향하는 점이 다르다. 이 둘의 패러다임(사상)을 중간에서 일치시켜주는 것이 JPA다.

 

* MyBatis나 iBatis는 ORM(객체맵핑)이 아닌 SQL Mapper(쿼리맵핑)다.

 

  • JPA 는 인터페이스로서 자바표준명세서다.
  • 구현체로 Hibernate, Eclipse Link 등이 있다.
  • 그러나 더 추상화시킨 Spring Data JPA를 사용한다. 이는 구현체(Hibernate) 교체와 저장소(MySql) 교체가 용이하기 때문이다. 구현체와 저장소에서는 기본적으로 save(), findOne()등 기본적인 CRUD 인터페이스를 제공하기 때문에 추후 구현체나 저장소를 교체해도 동일한 메소드를 사용할 수 있다.
  • 결론: JPA <- Hibernate <- Spring Data JPA

 

-Entity

  • 도메인 패키지에 관리되는 객체
  • 한 Entity가 한 테이블을 뜻한다.
  • 비즈니스 로직을 작성하기도 한다.
@Getter
@NoArgsConstructor // 기본생성자 자동추가
@Entity //JPA 어노테이션, 테이블과 링크된다.
public class Posts {

    @Id //PK
    @GeneratedValue(strategy = GenerationType.IDENTITY) //auto increment, PK를 이렇게 설정하는 것이 좋다.
    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;
    }
}

* @Builder 

  • 생성자와 역할이 같다.
  • 단, @Builder 어노테이션을 사용하면 안전하게 생성시점에 값을 채울 수 있다.
  • .build()를 사용해 객체를 생성한다. 
  • 값을 안전하게 맵핑하기 위해 사용한다.

 

-JpaRepository

public interface PostsRepository extends JpaRepository<Posts, Long> { //CRUD 자동생성
}

JpaRepository<Entity클래스, PK타입>를 상속받아 PostsRepository 라는 인터페이스를 생성하면 해당 인터페이스에서는 기본적인 CRUD 메소드가 자동으로 생성된다.

* 단, Entity클래스와 Repository는 함께 위치해야 한다. 이 둘은 도메인 패키지에서 주로 관리된다. 

 

- 참고사항

cheese10yun.github.io/spring-builder-pattern/

 

Builder 기반으로 객체를 안전하게 생성하는 방법 - Yun Blog | 기술 블로그

Builder 기반으로 객체를 안전하게 생성하는 방법 - Yun Blog | 기술 블로그

cheese10yun.github.io


[스프링의 웹 계층]

Web Layer: 컨트롤러 등 뷰 템플릿 영역, 외부 요청과 응답에 대한 영역이다.(@Filter, @Interceptor)

|     Dto: 계층 간 데이터 교환을 위한 객체   

Service Layer: 트랜잭션과 도메인 기능간 순서를 보장, 비즈니스 로직 처리하는 부분이 아니다.

|     Domain Model: 개발 대상을 단순화 시킨 것. ex.배달앱의 주문, 요금, 배달원배정 등이 있다. 따라서 VO객체도 여기에 포함된다. 

Repository Layer: DB에 접근하는 영역, DAO layer와 동일하다.

* 도메인 영역이 바로 비즈니스 로직을 처리하는 부분이다.

 

[RestController & Service]

스프링에서는 빈객체를 주입받는 방식을 대게 @Autowired를 사용했지만, 사실 빈객체를 주입받는 방식은 setter생성자를 통해서도 가능하다. 오히려 생성자를 통해 주입받는 게 안전하다.

 

-RestContorller

@RequiredArgsConstructor //final or @NonNull 인 필드 대상으로 생성자 생성
@RestController
public class PostsApiController {
    private final PostsService postsService;

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

 

-Service

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

    @Transactional
    public long save(PostsSaveRequestDto requestDto){
        return postsRepository.save(requestDto.toEntity()).getId();
    }
}

 

- Dto

  • Dto와 Entity는 서로 목적이 다르다.
  • Dto는 뷰 레이어에서 요청과 응답을 담당하지만, Entity Model은 DB에 직접 접근하는 객체다.
  • 반드시 분리해서 사용하도록 한다. 
@Getter
@NoArgsConstructor //기본 생성자만 생성
public class PostsSaveRequestDto {
    private String title;
    private String content;
    private String author;

    @Builder //Builder 어노테이션을 사용해 객체 생성 시 값을 안전하게 넣을 수 있다.
    public PostsSaveRequestDto(String title, String content, String author){
        this.title = title;
        this.content = content;
        this.author = author;
    }

    //도메인 모델을 view layer에서 사용하기 위해서는 아래와 같이 객체를 생성해서 사용한다.
    //직접 entity객체를 view layer에서 사용하지 않도록 주의하자.
    public Posts toEntity() { //Posts 객체를 생성해서 가져온다.
        return Posts.builder().title(title).content(content).author(author).build();
    }
}

 

-단위 Test

: Controller에서 Dto를 생성해 DB까지 데이터를 조회하고, 값이 잘 저장되는 지 확인

* 항상 given, when, then을 이용해 코드를 작성하도록 한다.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 
//JPA까지 테스트할 수 있도록 해준다. WebMvcTest는 단지 컨트롤러 테스트할 때 사용
public class PostsApiControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate; 
    //webEnvironment설정시 이용 가능하다. Rest Api를 호출 후 응답을 기다리는 동기 방식

    @Autowired
    private PostsRepository postsRepository;

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

    @Test
    public void Posts_Insert() throws Exception {
        //given
        String title = "title";
        String content = "content";
        PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder().title(title).content(content).author("author").build();

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

        //when
        ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class); //응답을 Long을 받는다.
        //postForObject: post요청에 따른 객체를 응답받는다.
        //postForEntity: post요청에 따른 Entity를 응답받는다.

        //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);
    }

}

[Update & Select]

update 와 select 를 정리하기 전에, 각 객체별 역할과 메소드의 역할을 정리해보았다.

 

1. PostsController(WEB LAYER) - url과 REST Http Method를 이용해 Service Layer로 자원을 넘긴다.

2. PostsSaveRequestDto(WEB LAYER) - Web과 Service계층간 이동을 담당한다.(특히 요청)

    (@Builder어노테이션은 계층간 이동과 요청 시 안전한 객체 생성을 위해, toEntity() 메소드는 Posts도메인을 이용하기 위해 생성)

3. PostsUpdateRequestDto(WEB LAYER) - Web과 Service계층간 이동을 담당한다.(특히 요청)

    (@Builder 어노테이션만 존재, JPA의 영속성 컨텍스트 개념으로 인해 데이터변경만으로 update가 이루어진다.)

4. PostsResponseDto(WEB LAYER) - Web과 Service계층간 이동을 담당한다.(특히 응답)

    (생성자(Posts)만 존재, Posts도메인을 이용하기 위해 생성)

5, PostsService(SERVICE LAYER) - 트랜잭션과 순서 보장

6. Posts(Domain, REPO LAYER) - 테이블과 맵핑되며, 로직을 담당

7. PostsRepository(Domain, REPO LAYER) - DB 접근을 위한 기본적인 CRUD 메소드 생성

 

* JPA 영속성 컨텍스트:

엔티티를 영구저장하는 공간.

  • 자바애플리케이션과 데이터베이스 사이의 가상 데이터베이스 개념이다.
  • 매 요청마다 EntityManagerFactory에서 EntityManager를 생성한다.
  • 내부적으로 EntityManager가 DB커넥풀을 사용해 DB에 붙는다.

- 참고사항

ict-nroo.tistory.com/130

 

[JPA] 영속성 컨텍스트와 플러시 이해하기

영속성 컨텍스트 JPA를 공부할 때 가장 중요한게 객체와 관계형 데이터베이스를 매핑하는 것(Object Relational Mapping) 과 영속성 컨텍스트를 이해하는 것 이다. 두가지 개념은 꼭 알고 JPA를 활용하자.

ict-nroo.tistory.com

 

1. 우선 계층간 이동을 담당할 DTO 생성

- 업데이트할 Dto객체 생성

@Getter
@NoArgsConstructor //기본 생성자만 생성
public class PostsUpdateRequestDto {
    private String title;
    private String content;

    @Builder
    public PostsUpdateRequestDto(String title, String content){ //안전한 객체 생성 가능
        this.title = title;
        this.content = content;
    }
}


- 응답 Dto객체 생성

@Getter
public class PostsResponseDto {
    private Long id;
    private String title;
    private String content;
    private String author;

    public PostsResponseDto(Posts entity){ //생성자
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.content = entity.getContent();
        this.author = entity.getAuthor();
    }
}

 

2. Service에서 사용할 도메인모델 내 로직 생성

-Posts 도메인 내 메소드 생성

public void update(String title, String content){
        this.title = title;
        this.content = content;
    }

 

3. Service 와 Controller 작성

-Service

@Transactional
public Long update(Long id, PostsUpdateRequestDto requestDto){
    Posts posts = postsRepository.findById(id).orElseThrow(()->new IllegalArgumentException("해당 게시글이 없습니다. id = "+id));
    posts.update(requestDto.getTitle(), requestDto.getContent());
    return id;
}

public PostsResponseDto findById(Long id){
    Posts entity = postsRepository.findById(id).orElseThrow(()->new IllegalArgumentException("해당 게시글이 없습니다. id = "+id));
    return new PostsResponseDto(entity);
}

-Controller

@PutMapping("/api/v1/posts/{id}")
public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto){
    return postsService.update(id, requestDto);
}

@GetMapping("/api/v1/posts/{id}")
public PostsResponseDto findById (@PathVariable Long id){
    return postsService.findById(id);
}

 

4. 단위 test

    @Test
    public void Posts_Update() throws Exception {
        //given
        //1. 데이터 저장
        Posts savedPosts = postsRepository.save(Posts.builder().title("title").content("content").author("author").build());

        //2. id값 얻고,
        Long updateId = savedPosts.getId();

        //3. 업데이트할 데이터 생성
        String expectedTitle = "title2";
        String expectedContent = "content2";

        //4. 업데이트 객체 생성
        PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder().title(expectedTitle).content(expectedContent).build();

        //5. 테스트할 url 생성
        String url = "http://localhost:"+port+"/api/v1/posts/" + updateId;

        //6. 업데이트할 객체를 http객체안에 넣는다.
        HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);



        //when
        //7. 테스트 시작, url에 post방식으로 http객체를 던지고, 반환값으로 long을 얻는다.
        ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, 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(expectedTitle);
        assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
    }

[로컬테스트]

내장WAS로 구동시 H2라는 데이터베이스가 메모리에서 실행되기 때문에, 웹 콘솔을 이용해 데이터를 넣고, url을 조회해본다.

 

1. application.approties 내 옵션 추가

spring.h2.console.enabled=true

2. localhost:8080/h2-console 로그인 후 데이터 삽입

3. url을 이용해 json데이터 확인


[Jpa Auditing]

: Audit는 '감시하다' 라는 뜻으로, Jpa Auditing은 생성일자나 수정일자 등 자동으로 칼럼을 넣어주는 기능을 뜻한다.

* DB 테이블에 값을 넣거나 수정하는 행위는 '누가'. '언제' 하였는지 기록을 잘 남겨 놓아야 한다.

 

1.BaseTimeEntity 생성

@Getter
@MappedSuperclass //Jpa Entity들이 해당 필드들을 칼럼으로 인식하도록 한다
@EntityListeners(AuditingEntityListener.class) //Auditing기능 포함
public abstract class BaseTimeEntity {
    @CreatedDate
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime modifiedDate;
}

 

2. 내장was 구동하는 Application.java에 활성화 어노테이션을 추가

@EnableJpaAuditing //Jpa Auditing 활성화 
@SpringBootApplication //스프링 부트의 자동설정, 스프링 빈 읽기 모두 자동으로 설정
public class Application {
    public static void main(String[] args){
        SpringApplication.run(Application.class, args); //내장 WAS를 실행, 톰캣 설치가 필요X, Jar파일을 실행하면 된다.
    }
}

 

3. 단위 테스트

    @Test
    public void BaseTimeEntity_Insert(){
        //given
        LocalDateTime now = LocalDateTime.of(2020,12,3,12,0,0);
        postsRepository.save(Posts.builder().title("title").content("content").author("author").build());

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

        //then
        Posts posts = postsList.get(0);
        System.out.println(">>>>> "+posts.getCreatedDate()+" >>>>>>>> "+posts.getModifiedDate());
        assertThat(posts.getCreatedDate()).isAfter(now);
        assertThat(posts.getModifiedDate()).isAfter(now);
    }

 

 

※ 해당 글은 "스프링 부트와 AWS로 혼자 구현하는 웹 서비스(이동욱 저)"를 참고해 작성하였습니다.