Web

2 - Spring Boot - JPA 구현 by Spring Web Layer

kakaroo 2022. 2. 7. 18:00
반응형

Spring Web Layer 구조로 H2 database를 사용하여 JPA를 구현해 보겠습니다.

 

https://kakaroo.tistory.com/41

 

Spring Web Layer (공사중...)

보호되어 있는 글입니다. 내용을 보시려면 비밀번호를 입력하세요.

kakaroo.tistory.com

 

1. Dependency 등록 (lombok, h2, JPA)

<build.gradle>

dependencies {
	implementation('org.springframework.boot:spring-boot-starter-web')
	testImplementation('org.springframework.boot:spring-boot-starter-test')
	implementation('org.projectlombok:lombok')
	implementation('com.h2database:h2')
	implementation('org.springframework.boot:spring-boot-starter-data-jpa')
}

 

2. domain 패키지에 entity 클래스와 entityRepository를 생성합니다.

도메인이란 ? 게시글, 댓글, 회원, 정산 , 결제 등 소프트웨어에 대한 요구사항 혹은 문제영역

domain 패키지에 entity 클래스와 entityRepository를 생성
domain 패키지에 entity 클래스와 entityRepository를 생성

 

<Posts.java -> Entity(DB Table)>

package com.kakaroo.intellij.springboot.domain.posts;

import com.fasterxml.classmate.GenericType;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter
//Entity 클래스에서는 절대 Setter 메소드를 만들지 않는다.
//대신, 해당 필드의 값 변경이 필요하면 명확히 그 목적과 의도를 나타낼수 있는 메소드를 추가해야 한다.
//생성자 또는 Builder를 통해 최종값을 채운 후, DB에 삽입한다.
//값 변경이 필요한 경우 해당 이벤트에 맞는 public 메소드를 호출하여 변경한다.
@NoArgsConstructor
@Entity //클래스의 카멜케이스이름을 언더스코어 네이밍으로 테이블이름을 만든다.
//ex)SalesManager.java -> sales_manager table
public class Posts {

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

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

    @Column(columnDefinition = "TEXT", nullable = false)    //기본값이 VARCHAR(255)
    private String content;

    private String author;

    @Builder    //Builder pattern class를 생성 ; 생성자 상단에 선언시 생성자에 포함된 필드만 빌더에 포함됨
    public Posts(String title, String content, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }
}

 

 

package com.kakaroo.intellij.springboot.domain.posts;

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

//MyBatis에서 Dao라고 불리는 DB Layer 접근자
//JPA에서는 Repository라고 부르며, interface로 생성한다.
//<Entity 클래스, PK타입>
//기본적인 CRUD 메소드가 자동으로 생성된다.
//@Repository를 추가할 필요도 없다.
//주의: Entity 클래스와 Entity Repository는 반드시 함께 위치해야 한다.
public interface PostsRepository extends JpaRepository<Posts, Long> { }

 

 

Controller로 Test를 해 봅니다.

package com.kakaroo.intellij.springboot.web;

import com.kakaroo.intellij.springboot.domain.posts.Posts;
import com.kakaroo.intellij.springboot.domain.posts.PostsRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class HelloController {

    @Autowired
    PostsRepository postsRepo;

    @GetMapping("/hello")
    public String get() {

        //save 메소드는 테이블 posts에 insert/update 쿼리를 실행한다.
        postsRepo.save(Posts.builder().content("KKK").title("title").author("author").build());

        List<Posts> list = postsRepo.findAll();
        System.out.println(list.get(0).getTitle());

        return "hello world";
    }
}

 

실제로 실행된 쿼리가 SQL 버전으로 나오게 하기 위해서 application.properties에 다음 옵션을 추가합니다.

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

 

 

SQL 로그 출력
SQL 로그 출력


 

Spring Web Layer 구조로 CRUD 를 구현해 보겠습니다.

 

https://kakaroo.tistory.com/41

 

소스 트리
소스 트리

 

Web layer의 Controller는 Web의 요청을 Mapping으로 구현하고 DTO를 param으로 사용하는 ServiceLayer를 호출합니다.

package com.kakaroo.intellij.springboot.web;

import com.kakaroo.intellij.springboot.services.posts.PostsService;
import com.kakaroo.intellij.springboot.web.dto.PostsResponseDto;
import com.kakaroo.intellij.springboot.web.dto.PostsSaveRequestDto;
import com.kakaroo.intellij.springboot.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RequiredArgsConstructor
@RestController
public class PostsApiController {
    private final PostsService postsService;

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

    @GetMapping("/api/v1/posts/")
    public List<PostsResponseDto> findAll() {
        return postsService.findAll();
    }

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

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

    @DeleteMapping("/api/v1/posts/{id}")
    public Long delete(@PathVariable Long id) {
        postsService.delete(id);
        return id;
    }
}

Repository는 interface 구현으로 이전 코드에서 변함이 없습니다.

 

Domain의 entity는 이전에 구현했던 부분에서 update 메소드만 추가로 구현해 줍니다.

update 메소드가 필요한 이유는 update시 repository에 query를 따로 보내지 않습니다.

JPA에는 엔티티를 영구저장하는 환경인 영속성 컨텍스트 기능이 있는데, 데이터의 값을 변경하면 트랜젝션이 끝나는 시점에 해당 테이블에 변경분을 반영합니다. 이것을 더티체킹이라 합니다.

package com.kakaroo.intellij.springboot.domain.posts;

import com.fasterxml.classmate.GenericType;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter
//Entity 클래스에서는 절대 Setter 메소드를 만들지 않는다.
//대신, 해당 필드의 값 변경이 필요하면 명확히 그 목적과 의도를 나타낼수 있는 메소드를 추가해야 한다.
//생성자 또는 Builder를 통해 최종값을 채운 후, DB에 삽입한다.
//값 변경이 필요한 경우 해당 이벤트에 맞는 public 메소드를 호출하여 변경한다.
@NoArgsConstructor
@Entity //클래스의 카멜케이스이름을 언더스코어 네이밍으로 테이블이름을 만든다.
//ex)SalesManager.java -> sales_manager table
public class Posts {

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

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

    @Column(columnDefinition = "TEXT", nullable = false)    //기본값이 VARCHAR(255)
    private String content;

    private String author;

    @Builder    //Builder pattern class를 생성 ; 생성자 상단에 선언시 생성자에 포함된 필드만 빌더에 포함됨
    public Posts(String title, String content, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }

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

 

Service layer를 구현하겠습니다. Request / SaveResponse /UpdateResponse DTO 를 파라미터로 갖습니다.

package com.kakaroo.intellij.springboot.services.posts;

import com.kakaroo.intellij.springboot.domain.posts.Posts;
import com.kakaroo.intellij.springboot.domain.posts.PostsRepository;
import com.kakaroo.intellij.springboot.web.dto.PostsResponseDto;
import com.kakaroo.intellij.springboot.web.dto.PostsSaveRequestDto;
import com.kakaroo.intellij.springboot.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

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

    @Transactional
    public Long save(PostsSaveRequestDto requestDto) {
        return postsRepository.save(requestDto.toEntity()).getId();
    }
    @Transactional
    public Long update(Long id, PostsUpdateRequestDto requestDto) {
        Posts posts = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("Id not exist!! id="+id));

        //update시 repository에 query를 보내지 않는다. JPA의 엔티티를 영구저장하는 환경인 영속성 컨텍스트 때문이다.
        //해당 데이터의 값을 변경하면 트랜젝션이 끝나는 시점에 해당 테이블에 변경분을 반영한다. 이것을 더티체킹이라 한다.
        posts.update(requestDto.getTitle(), requestDto.getContent());
        return id;
    }

    // javax.transaction.Transactional 는 readOnly 등의 옵션을 허용하지 않는다.
    @Transactional(readOnly = true)
    public PostsResponseDto findById(Long id) {
        Posts posts = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("Id not exist!! id=" + id));
        return new PostsResponseDto(posts);
    }
    @Transactional(readOnly = true)
    public List<PostsResponseDto> findAll() {
        List<PostsResponseDto> list = postsRepository.findAll().stream().map(new PostsResponseDto(posts)).collect(Collectors.toList());
        for(PostsResponseDto posts : list) {
            System.out.println(posts);
        }
        return list;
    }

    @Transactional
    public void delete(Long id) {
        Posts posts = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("Id not exist!! id=" + id));
        postsRepository.delete(posts);
    }
}

 

View type에 따라 분류해서 DTO를 생성합니다.

<PostsResponseDto.java>

package com.kakaroo.intellij.springboot.web.dto;

import com.kakaroo.intellij.springboot.domain.posts.Posts;
import lombok.Getter;

@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();
    }
}

 

<PostsUpdateRequestDto.java>

package com.kakaroo.intellij.springboot.web.dto;

import com.kakaroo.intellij.springboot.domain.posts.Posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class PostsUpdateRequestDto
{
    private String title;
    private String content;

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

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

 

<PostsSaveRequestDto.java>

package com.kakaroo.intellij.springboot.web.dto;

import com.kakaroo.intellij.springboot.domain.posts.Posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

//Entity 클래스와 거의 유사한 형태임에도 Dto 클래스를 추가로 생성했다.
//절대로 테이블과 연결된 Entity 클래스를 Request/Response 클래스로 사용해서는 안 된다.
//Request/Response 용 DTO는 View를 위한 클래스라 자주 변경이 필요하기 때문이다.
//View Layer와 DB Layer의 역할 분리를 철저하게 하는게 좋다.
@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();
    }
}

 

post
post

 
Raw data는 아래와 같은 형태로
{
    "title""title1",
    "content":"content1",
    "author":"author1"
}
 

 

 

id 1의 column을 delete

 

 

source : https://github.com/kakarooJ/SpringBoot-JPA-IntelliJ-01

 

GitHub - kakarooJ/SpringBoot-JPA-IntelliJ-01

Contribute to kakarooJ/SpringBoot-JPA-IntelliJ-01 development by creating an account on GitHub.

github.com

 

반응형

'Web' 카테고리의 다른 글

Spring Web Layer (공사중...)  (0) 2022.02.07
JPA(Java Persistence API) vs Mapper  (0) 2022.02.07
1 - Spring Boot - Start Application<IntelliJ-Gradle>  (0) 2022.02.07
Spring Boot - Database 처리방법  (0) 2022.02.06
Spring Boot - Thymeleaf  (0) 2022.02.05