Spring/[dsc] Spring-Novice-Study

[Chap3] JPA로 데이터베이스 다뤄보기

mopipi 2024. 1. 9. 17:27
반응형

🔖배경, 이론

  • 관계형 데이터 베이스의 중요성 대두
  • 객체를 관계형 데이터 베이스에서 관리하는 것 중요’
  • SQL 통해서만 DB 접근 가능 ∴ 각 테이블마다 CRUD SQL 매번 생성해줘야 함

     ⇒ 어플리케이션 코드 <<< SQL

 

JPA란?

  • 위 문제점을 해결하고자 등장한 자바 표준 ORM 기술이자 데이터를 다루는 기술.
  • 객체 지향 - 관계형 데이터 베이스의 패러다임 불일치 해결해주는 어탭터

      ∴ SQL, 데이터 중심 설계 → 객체 중심 설계로 전환 가능 (객체만 신경 쓰기 가능)

  • 개발 생산성 증대
    • 기존에 데이터 베이스 다룰 때 반복 코드 + 기본 SQL → 대신 자동으로 처리 해줌
    • 객체를 JPA에 넣으면, 중간에서 DB에 SQL날리고 Data 가져오기 → JPA가 다 처리
    • 항상 객체 지향적 코드로 표현 가능 → 유지 보수에도 good

🐢 ORM
      :Object + Relational + Mapping (객체와 Relational Database의 테이블을 매핑한다)

🐢 Mapping 방법? → @Annotation ****이용

Spring Data JPA

Spring Data JPA야 ~ 코더가 SQL을 써주지 않아도 스스로 CRUD 하는거야? ^^…

  • JPA = 표준 인터페이스 자체를 말함 ∴ Only 인터페이스 제공 ⏩ 구현체(인터페이스 구현한 클래스)가 추가적으로 필요
    • 구현체 예: Hibernate, Eclipse Link … (→ 주로 hibernate 사용) …

🐢 인터페이스

  • 환경 변화 ≈ 구현 기술 변화 ⇒ 사용 API 변화, 접근 방법 변화 문제
    ⇒ 규격 정해서 이용하자 (규격을 따르되, 구현체마다 다른 기능/효율)
  • 추후 DB 변경 → 구현 클래스 변경 가능한 인터페이스로
    • 확장에 대한 요구 사항 (변경엔 닫혀있고, 확장엔 열려있게 설계)
      → 객체간 느슨한 결합 필요 ⇒ 인터페이스
  • Spring Data JPA
    • JPA의 업그레이드 버전 프레임워크. JPA 편리하게 사용하기 위한 도구
    • Repository 구현 Class 작성해줄 필요X → 인터페이스만 있으면 됨!
    • 기본 CRUD기능도 저절로 제공
    • 단순 반복 코드 삭제 → 핵심 비즈니스 로직 개발에 집중 가능함
  • 만약 구현체를 변경하고 싶으면? (ex. Hibernate → Mongo DB)
    • Hibernate → Mongo DB로 의존성만 교체하면 됨! (설정 파일만 수정)
    • (∵ 하위 프로젝트의 기본적 CRUD 인터페이스는 동일 - save(), findAll(), findOne() 등)
🐢 Spring Data JPA, Spring Data Redis, Spring Data Mongo DB ⊂ Spring Data
    • 주로… 개발 시 → H2 DB 이용 (메모리에 존재)
      • 배포 전 테스트 or 실제 배포 → Maria DB, MySQL 등등

그 외 개념 용어

  • 도메인 : 일반적 요구 사항. 소프트웨어로 해결하고자 하는 문제 영역
  • 도메인 모델 : 특정 도메인을 개념적으로 표현한 것
    • 모델 : 웹페이지에서 쓰는 인벤토리 개념 (웹페이지에서 사용할 정보 넘기는 그릇)
  • Json : 일종의 data format으로, 서버가 client에게 전달하는 방식 + 서버끼리 통신시에도 사용함

🔖구현 및 검증

Spring Data JPA → 프로젝트에 적용

🎲bulid.gradle 수정

  • 의존성 등록
dependencies {
    ...
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    //spring boot data jpa 의존성 주입
    implementation 'com.h2database:h2'
    //데이터베이스로 사용할 h2
    ...
}
  • spring-boot-starter-data-jpa
    • Spring Data JPA 추상화 라이브러리
    • spring boot 버전에 맞춰 자동으로 JPA 관련 라이브러리 버전 관리
  • h2
    • 인메모리 관계형 데이터베이스 ⇒ 로컬 환경에서 구동 가능
    • → 별도의 설치 필요X - 프로젝트 의존성만으로 관리 가능
    • 메모리 실행 → 애플리케이션 재시작마다 초기화 (메모리 - 휘발성..)
      • 테스트 용도로 적합

경로 : [domian package (도메인 담을 패키지 (≠ dao 패키지)) / posts package / Posts class]

  • 도메인 : SW에 대한 요구사항 or 문제 영역 (ex. 게시글,댓글, 회원, 정산 …)

🎲 Posts ( + Entity Mapping )

  • Entity 클래스
    • 실제 DB table과 매칭될 클래스 (→ 여기서 Posts 클래스)
    • DB에 데이터 작업하려면 원래는 쿼리 날려야 했는데 대신 얘 수정해주면 끝
  • 코드
@Getter //lombok
@NoArgsConstructor //lombok
@Entity //<< 주요 어노테이션
public class Posts {  //실제 DB table과 매칭 될 클래스 == Entity 클래스
    
		// ***Entity Mapping 해주기***
    @Id //PK 정의
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; //DB에 값 들어올때마다 자동으로 PK 생성됨

    @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;
    }
}
  • 🐢 주요 어노테이션 → 클래스에 가깝게 둠 (주요 어노테이션 = @Entity)
    • Entity의 PK → Long 타입 + Auto_increment 무난
      • 주민등록번호, 복합키 → unique key로 별도로 추가하기

어노테이션

JPA 제공 어노테이션

  • @Entity
    • table과 링크될 클래스임을 나타냄
    • 클래스의 카멜케이스 이름 ↔ 언더스코어 네이밍으로 table name 매칭
    • (ex. table명 → posts인 테이블 생성)
카멜케이스 : 단어 전체 = 소문자/ 맨 첫 글자를 제외한 각 합성어 첫 글자 = 대문자
  • @Id
    • 해당 table의 PK 필드를 나타낸다 → DB에서 자동으로 생성
    • getter에 붙이거나 field에 붙임
  • @GeneratedValue
    • PK값에 대한 생성 전략 제공
    • 얘 없이 @Id만 사용 → pk 직접 할당
    • 선택적 속성 : strategy, generator
      • strategy : Entity PK 생성 시 사용해야 하는 PK 생성 전략 명시 (디폴트 : AUTO)
      • generator : @SequenceGenerator @TableGenerator 에서 명시된 PK 생성자를 재사용할 때 쓰임 (디폴트 : 공백문자(""))
🐢 (JPA가 지원하는) PK 생성 전략 4가지
     1. AUTO : DB에 맞게 자동 선택
     2. IDENTITY : identity column 이용
     3. SEQUENCE : sequence column 이용
     4. TABLE : 유일성이 보장된 DB TABLE 이용
  • @Column
    • 테이블 column 표시 (entity로 정의한 이상 이미 이 클래스의 필드는 모두 column임)
    • 즉, 객체 필드 → 테이블의 column에 mapping 시켜줌
    • column의 디폴트 속성 중 수정or 추가하고 싶은 부분 있을 때 사용
    • String에 붙이는 경우 디폴트→ VARCHAR(255)
      • size → 500 or type = TEXT로 변경 할 때 사용
    • 속성 종류 (주로 사용하는)
      • name
        • 필드와 mapping할 column명 지정 (디폴트 : 객체 field 이름 그대로)
      • DDL (데이터 정의어 - Structure)관련
        • nullable
          • DDL 생성 시 null 값 허용 여부 설정.
          • false → 제약조건으로 not null 붙음 (디폴트 : true)
        • unique
          • @Table 의 uniqueConstraints과 유사
          • but 얜 Column 1개에 적용 (디폴트: false)
        • columnDefinition
          • Column 정보 직접 줌
        • length
          • 문자 길이 Constraint → String 타입에만 적용! (디폴트 : 255)
        • precision, scale
          • BigDecimal 타입에서 사용. (디폴트 : 0)
          • precision → 소수점 포함 전체 자리 수, scale → 소수의 자리 수 (디폴트 : 0)
🐢 ~~이런 Annotation을 이용해 매핑된 정보들로 JPA가 쿼리 만들어 날려줌

lombok 어노테이션

  • @NoArgsConstructor
    • 기본 생성자 자동 추가
    • public Posts() { ~ }와 동일한 효과
    • @Getter
      • 클래스 내 field → getter 생성
    • @Builder
      • 해당 클래스의 빌더 패턴 클래스 생성
        • 빌더 패턴
          • 생성 패턴(인스턴스 만드는 절차를 추상화하는 패턴) 중 하나
          • 동일한 프로세스를 거쳐 다양한 구성의 인스턴스를 생성함
          • (복잡한) 객체 생성 클래스 // 표현 클래스 분리 → 서로 다른 표현 생성 방법 제공
      • 생성자 상단에 선언 → 생성자 포함된 필드만 빌더에 포함
      • 어느 필드에 값 채워야 할지 명확하게 인지 가능
Example.builder() //parameter로 한꺼번에 받아오는게 아니라 각각 할당함
			.a(A) //필드 a에 A값 할당
			.b(B) //필드 b에 B값 할당
			.build();
  • Posts 클래스 특이점 : Setter 메소드 X
❗❗ Entity Class에서는 Setter 메소드 생성 지양할 것❗
  • if) 해당 필드 값 변경이 필요하다 → 목적, 의도 명확히 드러나는 메소드 추가
  • 예시] 상황 가정: 주문 취소 메소드 생성
  • 잘못된 사용
public class Order{
	public void setStatus(boolean status){
			this.status = status;
		}
	}
public void 주문서비스_취소이벤트(){
	order.setStatus(false);
}

 

  • 올바른 사용 → 목적, 의도(cancel) 명확히 들어나는 메소드 추가 : false로 바꾸는 기능 아예 명시
public class Order{
		public void cancelOrder(){
			this.status = false;
		}
	}
public void 주문서비스_취소이벤트(){
	order.cancelOrder();
}
    • … setter 없으면 뭘 가지고 DB에 insert함? → Constructor로 ( 🔗 DI
      • 생성자 통해 생성 시점에 최종 값(=넣을 값) 채운 후 → DB 삽입
      • (생성자 대신 @Builder로 제공되는 빌더 클래스 이용도 가능)
      • 값 변경 필요한 경우 → 해당 event에 맞는 public 메소드 호출해 값 변경

🎲PostRepository (interface)

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

public interface PostRepository extends JpaRepository<Posts, Long> {

}
  • 게시글 저장할 데이터베이스
  • JpaRepository
    • Posts 클래스로 데이터베이스 접근 가능하게 함 (Dao 역할의 DB Layer 접근자)
    • 앞서 정리했다시피 Spring data jpa를 사용하기 위해선 구현 클래스 직접 작성할 필요 없이 인터페이스 생성해주면 됨 → JpaRepository 인터페이스 상속받기
    • JpaRepository <Entity 클래스, PK 타입>를 상속 → 기본 CRUD 자동 생성
  • 별도의 @Repository어노테이션 필요 X

+)

Entity 클래스, 기본 Entity Repository는 같은 디렉터리에 위치 → 보통 domain 패키지에서 함께 관리함


🔖Spring Data JPA → 테스트

🎲PostRepositoryTest - 기능 테스트 (save, findAll)

 

: save(), findAll() 기능 테스트 → 게시글저장_불러오기( ) 메소드 사용

@ExtendWith(SpringExtension.class)
@SpringBootTest
public class PostRepositoryTest {

    @Autowired
    PostRepository postRepository;//테스트 하기위해 주입받음

    @AfterEach
    public void cleanup() {
        postRepository.deleteAll(); //test 수행 시 마다 db 초기화
    }

    @Test
    public void 게시글저장_불러오기() {
        //1. given : 비교할 데이터 (제목-본문)
        String title = "테스트 게시글";
        String content = "테스트 본문";
	
        postRepository.save(Posts.builder() //@Builder로 인한 빌더 패턴
                .title(title) //title(field) -> title(given) 할당
                .content(content)
                .author("jojoldu@gmail.com")
                .build()); //최종 정보 담은 객체 생성 -> db에 insert

        //2. when : DB에 저장된 객체 불러와서 리스트에 저장 (총 1개 저장된 상태)
        List<Posts> postsList = postRepository.findAll();

        //3. then 
        Posts posts = postsList.get(0); //리스트의 첫번째 객체 꺼내서
				assertThat(posts.getTitle()).isEqualTo(title);
				assertThat(posts.getContent()).isEqualTo(content);
				//given인 정보 잘 저장됐는지 확인
    }
}
  • 코드
    • 어노테이션
      • @SpringBootTest
        • 통합 테스트에 사용 (↔ @WebMvcTest : 단위 테스트용)
          • 통합테스트
            • 실제 운영 환경에서 사용될 클래스들 통합해 테스트
            • 기능 검증용 X, 스프링 프레임워크에서 전체적 flow 테스트
        • 스프링 컨테이너와 테스트 함께 실행하게끔 함
        • H2 데이터베이스 자동으로 실행
      • @Autowired
        • Bean 주입 받는 방식 (그 외 setter, 생성자 방식 존재)
          • setter 방식과 유사한 방식으로 진행
      • @AfterEach
        • 각각의 Test 메소드가 실행될 때 호출됨. → 단위 테스트 후 (실행 전 = @BeforeEach)
        • 사용되고 난 후 종료돼야 할 리소스 처리 (여기선 JpaRepository 테스트이므로 DB 리소스를 각 테스트가 끝난 뒤 초기화 해주는 것 정의)
        • 즉, 전체 테스트 수행 시 테스트 간 데이터 침범 방지
        • 테스트 코드에서는 @Transactional 대신 @AfterEach+ deleteAll사용 (영속성 context 직접 관리 피함)

🐢JpaRepository 기본 메소드 (CRUD)

  • postRepository.save()
    • posts(table)에 insert /update 쿼리 날려주는 메소드
      • id값이 기존 table에 존재 → update
      • id값 존재 X → insert (+id(PK) 생성)
  • postRepository.findAll()
    • posts에 있는 모든 data(record) 조회해서 가져옴
    • 주로 객체형태로 테이블에 저장된 data → 가져와 List에 넣음
  • postRepository.deleteAll()
    • post table에 있는 data 모두 삭제
  • 이 외에도 findById, deleteById, count, exisits 등 존재
  •  

🔖등록/수정/조회 API 만들기

🐢 API 개발 시 순서
  1. domain Entity (클래스) 생성
  2. Repository (인터페이스) 생성
  3. Service (클래스) 생성
  4. Controller (클래스) 생성
  5. Dto (클래스) 생성
  • 위 기능의 API 구현 위해 필요한 클래스 총 3가지
    • Dto : Request 데이터 받음
    • Controller : API 요청 받음
    • Service : 트랜잭션, 도메인 기능 간 순서 보장
      • 비지니스 로직 처리X

Spring 웹 계층

+) Presentation layer - Sevice layer - Data Access Layer

  • Web Layer
    • 컨트롤러 등의 뷰 템플릿 영역
    • @Filter, 인터셉터, @ControllerAdvice 등 외부 요청과 응답에 대한 전반적 영역
  • Dtos
  • Service Layer
    • @Service에 사용되는 서비스 영역
    • Controller- Dao 중간 영역에서 사용됨
      • Service - Dao 단은 1대 1 매핑이 지향
    • @Transactional 사용 필요
    • 비즈니스 로직이 담기되 직접적으로 구현하는 곳은 X
      • 비즈니스 로직을 분할해서 → 각 도메인 담당 객체마다 메소드로 필요 비즈니스 로직이 구현되는데 필요한 핵심 기능이 정의되게 함.
      • Service 에선 특정 객체에게 메시지 보내 그 객체의 메소드를 이용함으로써 비즈니스 로직 구현
  • Domain Model
    • 개발 대상(=도메인)을 모두가 동일한 관점에서 이해, 공유 가능하게 단순화 한 것
    • @Entity가 사용된 영역 ⇒ 도메인 영역
    • db table과 관계 필수는 X (값 객체들도 포함하므로)
    • 비즈니스 처리를 담당해야 할 곳
  • Repository Layer
    • 데이터 저장소(database)에 접근하는 영역
    • Dao 영역

1. 등록 기능(save) 구현

🎲 PostsApiController

@RequiredArgsConstructor
@RestController
public class PostsApiController {

    private final PostsService postsService;

    @PostMapping("/api/v1/posts")
    public Long save(@RequestBody PostsSaveRequestDto requestDto) {
        return postsService.save(requestDto);
    }
}
  • 어노테이션
    • @RequiredArgsConstructor
      • final이 선언된 모든 필드를 인자값으로 하는 Constructor 대신 생성
      • 해당 클래스의 의존성 관계가 변할 경우 → Constructor 코드 수정 번거로움 예방
      • 해당 컨트롤러에 새로운 기능 추가 or 기존 컴포넌트 제거시 생성자 코드 수정 필요X
    • @RestController
      • 단순히 객체를 반환. VO객체 →JSON 변경 후 HTTPResponse에 담아 전송
        • 즉, method의 return값 → HTTPResponseBody에 담음
      • @Controller + @ResposeBody 의 동작을 하나로 결합한 편의 컨트롤러
        • @Controller : 스프링에서 해당 클래스를 Bean 객체로 → 서블릿용 컨테이너에 등록하게 함. View를 반환하기 위해 사용
        • 따라서 @ResposeBody 를 사용할 필요가 없음
      • 비동기 처리
      • View를 통해 출력하지 않고, 직접적으로(Json) 출력 (return값과 동일한 html 필요 없음. viewResolver 무시)
    • @PostMapping("...")
      • 주어진 url 표현식과 일치하는 HTTP post 요청을 처리
    • @RequestBody

                * Client가 전송하는 Json HTTP Body (요청) → 자바 객체로 매핑

                   - 요청은 주로 Json 형태의 RequestBody

                * 일반적인 GET 메소드의 request

                  → HTTPRequest의 requestBody로 요청 데이터가 전달 X

                  → URI 또는 URL의 파라미터로 전달 O             ∴ @RequestBody 사용 불가능

                   (Get은 @PathVariable, @RequestParam 이용)

                *  Body : Client ↔ Server 간 HTTP 통신 中 Request, Response 시 필요 data 담아서 보내는 공간

                                → 요청 본문 == RequestBody

                  +)@ResponseBody :자바 객체를 HTTPResponse의 본문 responseBody에 매핑

🎲 PostsService

@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostRepository postRepository;

    @Transactional
    public Long save(PostsSaveRequestDto requestDto) {
        return postRepository.save(requestDto.toEntity()).getId();
    }
}
  • @Service
    • 해당 클래스를 루트 컨테이너에 Bean 객체로 생성
      • Bean객체는 데이터 변경이 없는 객체에 한해 사용됨
    • 명시적으로 구분하기 위해 @Service , @Repository 로 분리해 사용
  • @Transactional
    • Data 변경이 발생하는 경우(ex. join) Transactional 안에서 수행 돼야 함
    • 해당 클래스의 method 실행 시 Transactional 안에서 수행 돼야 함
    • JPA 사용시 Service 계층에 필수 추가

🎲 PostsSaveRequestDto

  • Controller, Service에 데이터 넘겨줌
  • Request, Response용도의 View를 위한 클래스
  • Entity Class와 분리해서 사용해야 함
@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();
    }
}
  • Entitiy Class와 유사한 형태지만 Dto 추가로 만든 이유?
    • Request, Response 용으로 엔티티 클래스 사용 불가능
    • 엔티티 클래스
      • db와 직결된 핵심 클래스. 즉 DB Layer에 해당
      • 변경 시 여러 클래스에 영향감
      • ∴ 변경 지양해야 함
    • 반대로 Dto 클래스
      • View Layer에 해당
      • Controller에 결괏값으로 여러 테이블 join해서 줘야 하는 경우 사용

PostApiController 테스트 수행하기(1) - save

🎲 PostApiControllerTest

@ExtendWith(SpringExtension.class)
//JPA 기능까지 한번에 테스트
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 
//테스트에서 랜덤 포트 사용
public class PostsApiControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostRepository postRepository;

    @AfterEach
    public void  tearDown() throws Exception {
        postRepository.deleteAll();
    }

    @Test
    public void Posts_등록된다() 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);

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

        List<Posts> all = postRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(title);
assertThat(all.get(0).getContent()).isEqualTo(content);
    }
}
  • @LocalServerPort
    • 테스트에 포트 번호 삽입. 실제로 주입 되는 포트번호 확인 가능
  • restTemplate
    • Http Client (HTTP를 사용해 통신하는 범용 라이브러리)를 추상화(HttpEntity) 해서 제공함 → 최근엔 WebClient로 대체
    • REST 서비스를 호출하는 복잡한 과정 → 단순화
      • REST API 호출 후
    • .postForEntity()
      • `postForEntity(String url, Object request, Class responseType, +a)` 형식
      • “HTTP POST ; Create a new resource by POSTing the given object to the URI template, and returns the response as ResponseEntity"
      • POST 요청을 보내고 결과로 ResponseEntity(← 응답메세지를 변환) 반환 받음

→ 테스트 통과!


나머지 기능(update, findById) 구현 (코드 추가 + Dto 생성)

🎲 PostsApiController 수정

...
//게시판 수정 기능
@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);
}
  • 어노테이션
    • @PutMapping
      • 주어진 URL 표현식과 일치하는 HTTP PUT 요청 처리
      • HTTP PUT : 리소스 수정 or 새로 생성. 멱등성 O
    • @GetMapping
      • HTTP GET 요청을 처리하는 메소드를 매핑 (@RequestMapping )함
      • 메소드 (url)에 따라 어떤 페이지를 보여줄지 결정
    • @PathVariable
      • @GetMapping (or @PutMapping, @PostMapping 등…) 과 함께 사용됨
        • 해당 어노테이션의 { }안에 들어간 url → 변수가 들어감
      • url 부분에 {괄호 처리 된 변수} 들어갈 파라미터 연결

🎲 PostsResponseDto

@Getter
public class PostsResponseDto {
    //Entity의 필드 중 일부만 사용 -> 생성자로 엔티티 받아서 필요한 필드만 골라 사용

    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

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

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

🎲 Posts 수정

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

🎲 PostsService 수정

...
@Transactional
    public Long update(Long id, PostsUpdateRequestDto requestDto) {
        Posts posts = postRepository.findById(id) //수정할 객체 id로 찾아서
                .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));
        //requestDto 내용으로 title, content 수정 
        posts.update(requestDto.getTitle(), requestDto.getContent());

        //해당 data id 반환
        return id;
    }//

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

        //찾은 엔티티 data Dto에 담아 반환 (view에 뿌릴거니까 Dto)
        return new PostsResponseDto(entity);
    }
}

PostApiController 테스트 수행하기(2) - update

🎲 PostsApiControllerTest 수정

...
@Test
public void Posts_수정된다() throws Exception {
//1. given
 Posts savePosts = postRepository.save(Posts.builder()
                .title("title")
                .content("content")
                .author("author")
                .build());
 //Repository에 테스트용 객체 생성 후 save한 뒤 해당 엔티티 -> savePosts로 저장

 Long updateId = savePosts.getId(); //test할 엔티티 id 가져와 저장
//수정할 값 (결과와 비교할 값)
 String expectedTitle = "title2";
 String expectedContent = "content2";

//수정할 data Dto로 저장
  PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder()
                .title(expectedTitle)
                .content(expectedContent)
                .build();

 String url = "http://localhost:" + port + "/api/v1/posts/" + updateId;
 HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);

 //2. when
 ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT
                ,requestEntity, Long.class); //title, content 변경 값으로 업데이트

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

 List<Posts> all = postRepository.findAll();
 assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
 assertThat(all.get(0).getContent()).isEqualTo(expectedContent);

    }
  • HttpEntity<>
    • HTTP 요청 or 응답에 해당하는 HttpHeader, HttpBody를 포함하는 클래스
    • 얠 상속받아 구현한 클래스 → RequestEntity, ResponseEntity

 

UPDATE 쿼리 없이 쿼리가 실행되는 이유→ 🔗영속성 컨텍스트

마찬가지로 무사히 기능이 작동하는 것을 확인 가능하다.


H2 - 웹 콘솔 사용하기

🎲 application.properties

spring.h2.console.enabled=true 추가 후 Applicaiton의 main 메소드 실행

→ 8080포트로 서버열림 ⇒ 이때 localhost:8080/h2-console 입력하면 h2 콘솔 화면으로…

난 JDBC URL이 다르게 설정되어 있어서 화면과 같이 직접 변경해줬다.

POSTS 테이블이 잘 보인다.

API 조회 기능 테스트 해보기

insert into posts (author, content, title) values ('author', 'content', 'title');
  • insert 쿼리 실행 후 http://localhost:8080/api/v1/posts/1 로 접속해 API 조회 기능 테스트

잘 반영되는 것을 확인할 수 있다.


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

  • 엔티티 ∋ 해당 데이터의 생성/수정 시간
    • 추후 유지보수에 매우 중요한 정보
    • DB 삽입 전, 갱신 전에 날짜 데이터 등록/수정 코드 필수 → 테이블, 서비스 메소드에
  • 하나하나 기록해주기엔 너무 많음 → 해결책 : JPA Auditing

LocalDate 사용

🎲 BaseTimeEntity

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {

    @CreatedDate
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LastModifiedDate lastModifiedDate;

}
  • 어노테이션
    • @MappedSuperclass
      • 엔티티 클래스들이 BaseTimeEntity을 상속할 경우 필드들(created, modified)도 Column으로 인식하게 함
    • @EntityListeners(AuditingEntityListener.class)
      • BaseTimeEntity 클래스에 Auditing 기능을 포함시킴
    • @CreatedDate
      • 엔티티 생성→ 저장 시 시간 자동 저장
    • @LastModifiedDate
      • 조회한 엔티티의 값 변경 시의 시간을 자동 저장

🎲 Posts 수정

public class Posts extends BaseTimeEntity {
...
}

🎲 Application 수정

  • 활성화 어노테이션 @EnableJpaAuditing추가 → JPA Auditing 어노테이션 모두 활성화

JPA Auditing 테스트 코드 작성 및 테스트

🎲 PostsRepositoryTest 수정

//JPA Auditing 테스트 코드
    @Test
    public void BaseTimeEntity_등록() {
        //1.given
        LocalDateTime now = LocalDateTime.of(2019, 6, 4, 0, 0, 0);
                //기준 날짜 설정
        postRepository.save(Posts.builder()
                .title("title")
                .content("content")
                .author("author")
                .build());

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

        //3. then
        Posts posts = postsList.get(0);
        System.out.println(">>>>>>> createDate =" + posts.getCreatedDate()
                + ", modifiedDate =" + posts.getLastModifiedDate());
        //날짜 정상적으로 나오면 패스
                assertThat(posts.getCreatedDate()).isAfter(now); 
        assertThat(posts.getLastModifiedDate()).isAfter(now);
    }
  • 어노테이션
    • @MappedSuperclass
      • 엔티티 클래스들이 BaseTimeEntity을 상속할 경우 필드들(created, modified)도 Column으로 인식하게 함
    • @EntityListeners(AuditingEntityListener.class)
      • BaseTimeEntity 클래스에 Auditing 기능을 포함시킴
    • @CreatedDate
      • 엔티티 생성→ 저장 시 시간 자동 저장
    • @LastModifiedDate
      • 조회한 엔티티의 값 변경 시의 시간을 자동 저장

테스트 결과

→ 등록, 수정 날짜, 시간이 잘 출력된다

반응형