관리 메뉴

A seeker after truth

스프링부트 책 3단원: spring data JPA 다뤄보기 본문

Springboot

스프링부트 책 3단원: spring data JPA 다뤄보기

dr.meteor 2020. 3. 7. 11:36

* 본문은 <스프링부트와 aws로 혼자 구현하는 웹서비스>(2019, 이동욱, 프리렉)을 공부하고 정리한 내용입니다. 코드에서 import 부분은 모두 생략했습니다.

 

- import static ~ : 임포트의 스태틱은 무슨 기능일까

- 인터페이스만 들어있는 PostsRepository 같은 파일도 .java 붙여서 자바 파일로 생성해야 한다! 클래스 파일로 처음에 생성해도 인터페이스 입력하면 인터페이스로 도로 바뀜! 어이없는 삽질경험 +1.

 

1. JPA(Java Persistence API)란?

- 목적: 데이터베이스를 프로그래밍하는 일과 객체지향 프로그래밍을 하는 일 사이에 간극을 줄이기 위해 JPA라는 자바 표준 ORM 기술(인터페이스 모음)이 등장했다. 영속성(entity에 데이터를 영구히 저장하기 위한 환경) 관리도 할 수 있다.

- 기존 상황: 데이터베이스 모델링과 객체 모델링은 사실 서로 다른 영역이다. 전자(관계형 데베)는 어떻게 데이터를 저장할지에 초점이 맞춰진 기술이고, 후자는 기능과 속성을 한 곳에서 관리하는 기술. ORM은 객체를 매핑하고, SQL Mapper는 쿼리를 매핑한다. 그런데 객체를 데베에 저장하려고 하니 문제가 생기는 것. 이 둘은 사상부터 다른 시작점에서 출발했으므로 "패러다임 불일치" 문제는 필연이었는지도.

- ⭐️해결: 개발자는 객체지향적으로 프로그래밍 하고, JPA는 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행할 수 있게 되었다.

- 현업 현황: SI환경에선 아직 Spring&MyBatis를 많이 사용하지만, 쿠팡, 우아한형제들, NHN 등 자사 서비스를 개발하는 곳에서는 SpringBoot&JPA를 전사 표준으로 사용하고 있다. 후자에선 점점 더 많이 쓰는 추세.

- 효과: 생산성 향상, 유지보수 용이

- 기타 장점: CRUD 쿼리 작성 안해도 됨, 부모-자식 관계 표현, 1:N 관계 표현, 상태와 행위를 한 곳에서 관리하는 등 객체 지향 프로그래밍을 쉽게할 수 있음.

- 단점: 러닝커브 높음

- 자세한 개념+심화 설명: https://gmlwjd9405.github.io/2019/08/04/what-is-jpa.html

  •동작 원리: 개발자가 JPA를 사용하면, JPA 내부에서 JDBC API를 사용하여 SQL을 호출하여 DB와 통신

 

 

지금부터 게시판 사이트를 만드는 프로젝트 본격적 진행.

 

 

2. domain 패키지

web 패키지와 같은 위치에 domain 패키지를 만든다. 도메인이란 게시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 혹은 문제 영역을 말한다. DDD의 도메인도 이 도메인을 말하는 것. 그간 xml에 쿼리를 담고, 클래스엔 쿼리의 결과만 담던 일들이 모두 도메인 클래스라고 불리는 곳에서 해결된다.

 

1) Posts 패키지, 클래스

이 클래스는 실제 데베의 테이블과 매칭될 클래스이며 보통 Entity 클래스라고도 한다. JPA를 사용해 데베 작업을 할 경우, 실제 쿼리를 날리기 보단 Entity 클래스의 수정을 통해 작업한다.

 

•JPA 어노테이션

- @Entity: 객체 하나를 의미. 클래스와 데이터를 매핑하겠다고 알려주는 어노테이션. 테이블과 클래스가 링크됨. 기본값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍으로 테이블 이름을 매칭.

ex) SalesManager.java -> sales_manager table

- @Id: 해당 테이블의 PK 필드를 나타냄. @Entity의 고유한 아이디를 식별하기 위해 사용함

- @GeneratedValue: PK 생성 규칙을 나타냄. 스프링부트 2.0부터는 strategy = GenerationType.IDENTITY 으로 GenerationType.IDENTITY 옵션을 추가해야만 auto_increment가 된다.

- @Column: 테이블의 칼럼을 나타내며 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 칼럼이 된다. 기본값 외에 추가로 필요한 옵션이 있으면 사용한다.

 

롬복 어노테이션

- @NoArgsConstructor: 기본 생성자 자동 추가. public Posts() {} 와 같은 효과.

- @Builder: 해당 클래스의 빌더 패턴 클래스 생성. 생성자 상단에 선언 시 생성자에 포함된 필드만 빌더에 포함. 앞으로 계속 빌더 패턴을 적극적으로 사용할 예정. 자세한 내용은 바로 아래 'setter가 없는 이유' 참고.

* 빌더 패턴: https://webcoding.tistory.com/entry/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EB%B9%8C%EB%8D%94-%ED%8C%A8%ED%84%B4Builder-pattern

 

•setter 메서드가 없는 이유

Entity 클래스에선 절대 Setter 메서드를 만들지 않는다! 자바빈 규약을 생각해서 getter, setter를 무조건 생성하는 경우가 있다. 이렇게 하면 차후 기능을 변경할 일이 생겼을 때 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수가 없어 매우 복잡해진다. 그래서 Entity클래스에선 이것을 명확히 드러낼 수 있는 메소드를 setter 대신 추가해야 한다.

기본적인 구조는 생성자를 통해 최종값을 채운 후 데베에 삽입하는 것이며, 값을 변경하려는 경우 해당 이벤트에 맞는 퍼블릭 메서드를 호출하여 변경하는 것이라고 하자. ->이 프로젝트에선 생성자 대신 @Builder를 통해 제공되는 빌더 클래스를 사용하는 것이다. 생성 시점에 값을 채워주는 역할은 생성자와 동일하다. 생성자의 경우 지금 채워야 할 필드가 무엇인지 명확히 지정할 수 없단 것이 차이점. 빌더는 어느 필드에 어떤 값을 채워야할지 명확하게 인지할 수 있다!

자세한 내용은 책 92~4쪽 참고.

 

코드:

@Getter
@NoArgsConstructor //기본 생성자 자동 추가
@Entity //테이블과 링크될 클래스
public class Posts {
    @Id //해당 테이블의 pk 필드
    @GeneratedValue(strategy = GenerationType.IDENTITY) //pk 생성규칙
    private Long id;

    //table's column. 해당 클래스의 필드는 모두 칼럼이 된다.
    @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;
    }

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

 

 

2) PostsRepository.java

Posts 클래스로 데베에 접근하게 하는 코드다. DB Layer 접근자. iBatis, MyBatis 등에선 DAO라 부르며, JPA에선 Repository라 부른다. 단순히 인터페이스 생성 후 JpaRepository<Entity 클래스, PK 타입>을 상속하면 기본적인 CRUD 메서드가 자동으로 생성된다!

public interface PostsRepository extends JpaRepository<Posts, Long> {
}

단, Entity 클래스와 Entity Repository는 반드시 함께 위치해야 한다! 자세한 설명은 95쪽 참고.

 

 

3)위 코드의 테스트 코드. 일단 패스

 

 

3. 등록/수정/조회 API 만들기 / service.posts, web.dto

API를 만들기 위해 3개의 클래스가 필요하다.

•Request 데이터를 받을 DTO

•API 요청을 받을 Controller

•트랜잭션, 도메인 기능 간의 순서를 보장하는 Service <--- 이 역할만 하고, 비즈니스 로직을 처리하는 부분은 "도메인"이다!!!

 

아래는 스프링 웹 계층을 나타낸 사진이다.

각 계층에 대한 설명 중 일부만 작성하면 아래와 같다. 책 101~2쪽

•웹 계층

- @Controller, JSP 등 뷰 템플릿의 영역. 외부 요청과 응답에 대한 전반적인 영역을 이야기 함.

 

•서비스 계층

- 일반적으로 컨트롤러와 DAO의 중간 영역에서 사용됨

 - @Service, @Transactional이 사용됨

 

•Repository Layer

- 기존의 DAO(Data Access Object) 영역(ex: PostsRepository)으로 이해할 수 있으며, 데베와 같은 데이터 저장소에 접근하는 영역.

 

•DTOs (Data Transfer Object)

- 계층 간 데이터 교환을 위한 객체의 영역( 데베에서 데이터를 얻어 Service나 Controller 등으로 보낼 때 사용하는 객체). 뷰 템플릿 엔진에서 사용되는 객체나 Repository Layer에서 넘겨준 객체 등이 이것의 예다.

- 자바빈(Java Beans)이기도 하다...!?.

- 보통 로직을 갖고 있지 않은 순수 data 객체이며, 그 data에 접근하기 위한 getter, setter만 있다.

 

•Domain Model

- 앞에서 언급한 거 그대로. 택시앱이라고 치면 배차, 탑승, 요금 모두 도메인이 될 수 있다.

- 주로 @Entity와 엮이는 객체들이 도메인이다.

- 모든 도메인 객체가 꼭 데이터베이스와 연동되어야 하는 것은 아니다.

- 비즈니스 로직 처리는 각각의 Domain 영역에서 진행한다.

코드로써 이를 더 자세히 설명한 부분은 책 102~4쪽 참고.

 

1) web/PostsApiController.java

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

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

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

- @RestController: @Controller와 비교되는 어노테이션이다. 둘 간의 차이점을 이해하면 어떤 기능을 하는지 이해할 수 있다.

        @Controller는 Spring MVC의 컨트롤러고, @RestController는 RESTful 컨트롤러다.

        @ResponseBody는 문자열과 JSON, 객체 등을 전송할 수 있다. @Controller는 뷰를 리턴하는 메서드를 가지고 있다.

        @Controller와 @ResponseBody 를 합친 게 @RestController. 컨트롤러 클래스에 이 어노테이션만 붙이면 @ResponseBody 어              노테이션을 붙이지 않아도 된다.

        기본 개념 이해에는 아래 링크가 가장 좋다.

        https://wondongho.tistory.com/76

        각 자료형을 어떻게 전송하는지는 아래 링크에서 심화로 공부할 수 있다.

        https://webcoding.tistory.com/entry/Spring-REST-API-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

- @ResponseBody: 컨트롤러로 전송된 정보가 여기로 들어간다. 자세한 내용은 아래 링크 참고.

https://webcoding.tistory.com/entry/Spring-%EC%8A%A4%ED%94%84%EB%A7%81-RequestBody-ResponseBody-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-1

 

2) service.posts/PostsService.java

@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=" + 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);
    }
}

 - @Transactional

보다 복잡한 프로그램을 개발하다보면 2개 이상의 쿼리를 처리해야 하는 경우가 생긴다. 여러 개의 쿼리를 처리하는 과정에서 문제가 생겨버린다면 시스템에 큰 결함을 남기게 된다.

Transactional은 2개 이상의 쿼리를 하나의 커넥션으로 묶어 데베에 전송하고, 이 과정에서 에러가 발생할 경우 자동으로 모든 과정을 원래대로 되돌려 놓는다. 이런 과정을 구현하기 위해 트랜잭션은 하나 이상의 쿼리를 처리할 때 동일한 커넥션 객체를 공유하도록 한다.

- @RequiredArgsConstructor: final이 선언된 모든 필드를 인자값으로 하는 생성자로 자바 빈을 주입받음! @Autowired 없이 @Autowired와 동일한 효과를 냄. 롬복 어노테이션이기 때문에 해당 클래스의 의존성 관계가 변경되거나, 해당 컨트롤러에 새로운 서비스를 추가하거나, 기존 컴포넌트를 제거해야 하는 상황 등에서 생성자 코드를 수정하지 않아도 된다는 장점이 있다.

•@Service

링크가 진짜 마땅한게 없음. 공식 문서 자료도 없음. 요 링크가 그나마 젤 나음.

https://onlyformylittlefox.tistory.com/13

 

 

3) web/dto/PostsResponseDto, PostsSaveRequestDto, PostsUpdateRequestDto

@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();
    }
}
@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();
    }
}
@Getter
@NoArgsConstructor
public class PostsUpdateRequestDto {
    private String title;
    private String content;

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

 

- @NoArgsConstructor: (파라미터가 없는) 기본 생성자를 만들어줌.

- PostsUpdateRequestDto에서 데베에 쿼리를 날리는 부분이 없는데, 이는 JPA의 영속성 컨텍스트(=엔티티를 영구 저장하는 환경) 때문이다.

책에는 엔티티 매니저가 ~했을 땐 이게 유지된 거다 정도로만 이야기하지, 자세한 원리 및 개념은 없음. 따로 공부할 것

 

 

4. 테스트 코드에서 고찰할 부분

1) PostsApiControllerTest

@RestTemplate:

   - Spring 4.x 부터 지원하는 Spring의 HTTP 통신 템플릿

   - HTTP 요청 후 Json, xml, String 과 같은 응답을 받을 수 있는 템플릿

   - Blocking I/O 기반의 Synchronous API (비동기를 지원하는 AsyncRestTemplate 도 있음)

   - ResponseEntity와 Server to Server 통신하는데 자주 쓰임

   - 또는 Header, Content-Type등을 설정하여 외부 API 호출

   - Http request를 지원하는 HttpClient를 사용함

출처: https://a1010100z.tistory.com/125

공식문서: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html

위 내용 이상의 내용도 공부할 수 있는 좋은 포스트다.

 

@WebMvcTest를 사용하지 않고 @SpringBootTest를 사용한 이유

@WebMvcTest는 JPA 기능이 작동하지 않고 Controller, ControllerAdvice 등 외부 연동과 관련된 부분만 활성화된다.

-->지금은 JPA 테스트 --> @SpringBootTest 와 TestRestTemplate 사용

 

TestRestTemplate 클래스의 exchange 메서드

Execute the HTTP method to the given URI template, writing the given request entity to the request, and returns the response as ResponseEntity. TestRestTemplate 자체에 대한 더 자세한 소개는 공식 문서 아래 링크에!

출처: https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/web/client/TestRestTemplate.html

 

 

 

5. JPA Auditing으로 생성/수정시간 자동화

보통 엔티티에는 해당 데이터의 생성시간과 수정 시간을 포함한다.

->데베에 삽입/갱신하기 전, 날짜 데이터를 등록, 수정하는 코드가 매번 들어간다 (문제:같은 기능을 하는 단순한 코드가 모든 메서드마다 들어간다)

-> JPA Auditing로 해결 ->좀 더 정확히는 "java.time.LocalDateTime;"

 

 

 

[공부하면 좋을 링크]

- 자주 사용되는 Lombok 어노테이션

https://www.daleseo.com/lombok-popular-annotations/

- @Autowired, @Service, @Repository 구조

https://m.blog.naver.com/scw0531/220988401816

- <백엔드면 이정도는 해야한다> 시리즈 예전에 본 글 주에 <사용자 인증 방식> 이란게 있었다!!!! 이거 최고임 진짜 request body, 요청의 query parameter, Cookie 헤더, Authorization 헤더 네 개에 대한 이야기가 다 나옴!!!

https://velog.io/@city7310/%EB%B0%B1%EC%97%94%EB%93%9C%EA%B0%80-%EC%9D%B4%EC%A0%95%EB%8F%84%EB%8A%94-%ED%95%B4%EC%A4%98%EC%95%BC-%ED%95%A8-5.-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%9D%B8%EC%A6%9D-%EB%B0%A9%EC%8B%9D-%EA%B2%B0%EC%A0%95

 

 

 

[개발 TIP]

- 어노테이션 정렬 기준: 주요 어노테이션을 '클래스 선언부'에 가깝게 둔다(헷갈 ㄴㄴ). 그래서 아래 코드에 JPA 어노테이션(@Entity)은 클래스 선언 바로 위에, 롬복 어노테이션(@Getter, @NoArgsConstructor)은 그 윗줄에 선언했다. 롬복은 코드를 단순화해줄 뿐 필수 어노테이션은 아니기 때문(*서비스 초기 구축 단계에선 테이블 설계-여기선 엔티티 설계-가 빈번하게 변경되는데, 이때 롬복의 어노테이션들은 코드 변경량을 최소화시켜 주기 때문에 적극적으로 사용한다). -----> 코틀린 등의 새 언어 전환으로 더이상 롬복이 필요 없게 되면 쉽게 삭제 가능!

- Entity의 PK는 Long타입의 Auto_increment(지금은 이게 뭔지 몰겠)를 추천. MySQL 기준으로 이렇게 하면 bigint 타입이 된다. 주민등록번호같이 비즈니스상 유니크 키나 여러 키를 조합한 복합키로 PK를 잡을 경우 난감한 상황이 종종 발생하기 때문. 자세한 내용은 책 91쪽 참고.

- Entity Request/Response 클래스로 사용해서는 안 된다. Entity에 대한 더 자세한 내용은 책 107~8쪽.