🔖배경, 이론
- 관계형 데이터 베이스의 중요성 대두
- 객체를 관계형 데이터 베이스에서 관리하는 것 중요’
- 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 등등
- 주로… 개발 시 → H2 DB 이용 (메모리에 존재)
그 외 개념 용어
- 도메인 : 일반적 요구 사항. 소프트웨어로 해결하고자 하는 문제 영역
- 도메인 모델 : 특정 도메인을 개념적으로 표현한 것
- 모델 : 웹페이지에서 쓰는 인벤토리 개념 (웹페이지에서 사용할 정보 넘기는 그릇)
- 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로 별도로 추가하기
- Entity의 PK → Long 타입 + Auto_increment 무난
어노테이션
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)
- nullable
- name
🐢 ~~이런 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 메소드 호출해 값 변경
- … setter 없으면 뭘 가지고 DB에 insert함? → Constructor로 ( 🔗 DI)
🎲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 데이터베이스 자동으로 실행
- 통합 테스트에 사용 (↔ @WebMvcTest : 단위 테스트용)
@Autowired
- Bean 주입 받는 방식 (그 외 setter, 생성자 방식 존재)
- setter 방식과 유사한 방식으로 진행
- Bean 주입 받는 방식 (그 외 setter, 생성자 방식 존재)
@AfterEach
- 각각의 Test 메소드가 실행될 때 호출됨. → 단위 테스트 후 (실행 전 =
@BeforeEach
) - 사용되고 난 후 종료돼야 할 리소스 처리 (여기선 JpaRepository 테스트이므로 DB 리소스를 각 테스트가 끝난 뒤 초기화 해주는 것 정의)
- 즉, 전체 테스트 수행 시 테스트 간 데이터 침범 방지
- 테스트 코드에서는
@Transactional
대신@AfterEach
+deleteAll
사용 (영속성 context 직접 관리 피함)
- 각각의 Test 메소드가 실행될 때 호출됨. → 단위 테스트 후 (실행 전 =
- 어노테이션
🐢JpaRepository 기본 메소드 (CRUD)
postRepository.save()
- posts(table)에 insert /update 쿼리 날려주는 메소드
- id값이 기존 table에 존재 → update
- id값 존재 X → insert (+id(PK) 생성)
- posts(table)에 insert /update 쿼리 날려주는 메소드
postRepository.findAll()
- posts에 있는 모든 data(record) 조회해서 가져옴
- 주로 객체형태로 테이블에 저장된 data → 가져와 List에 넣음
postRepository.deleteAll()
- post table에 있는 data 모두 삭제
- 이 외에도 findById, deleteById, count, exisits 등 존재
🔖등록/수정/조회 API 만들기
🐢 API 개발 시 순서
- domain Entity (클래스) 생성
- Repository (인터페이스) 생성
- Service (클래스) 생성
- Controller (클래스) 생성
- Dto (클래스) 생성
- 위 기능의 API 구현 위해 필요한 클래스 총 3가지
- Dto : Request 데이터 받음
- Controller : API 요청 받음
- Service : 트랜잭션, 도메인 기능 간 순서 보장
- 비지니스 로직 처리X
Spring 웹 계층
+) Presentation layer - Sevice layer - Data Access Layer
- Web Layer
- 컨트롤러 등의 뷰 템플릿 영역
- @Filter, 인터셉터, @ControllerAdvice 등 외부 요청과 응답에 대한 전반적 영역
- Dtos
- Dto (계층 간 데이터 교환을 위한 객체)들의 영역
- 🔗 DTO, DAO
- 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 무시)
- 단순히 객체를 반환. VO객체 →JSON 변경 후 HTTPResponse에 담아 전송
@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
로 분리해 사용
- 해당 클래스를 루트 컨테이너에 Bean 객체로 생성
@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)에 따라 어떤 페이지를 보여줄지 결정
- HTTP GET 요청을 처리하는 메소드를 매핑 (
@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 조회 기능 테스트
잘 반영되는 것을 확인할 수 있다.
- https://ttl-blog.tistory.com/114https://dawitblog.tistory.com/34https://velog.io/@sumusb/Spring-Service-Layer에-대한-고찰https://wildeveloperetrain.tistory.com/144https://sjh836.tistory.com/141https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html#postForEntity-java.lang.String-java.lang.Object-java.lang.Class-java.lang.Object...-https://byul91oh.tistory.com/433
- https://esoongan.tistory.com/118
- https://www.appletong.com/entry/Spring-PathVariable
- https://joomn11.tistory.com/117?category=936835
- https://dncjf64.tistory.com/288
- https://mangkyu.tistory.com/72
- https://tecoble.techcourse.co.kr/post/2020-08-31-dto-vs-entity/
- https://gmlwjd9405.github.io/2018/12/25/difference-dao-dto-entity.html
- https://jsaver.tistory.com/2
🔖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으로 인식하게 함
- 엔티티 클래스들이 BaseTimeEntity을 상속할 경우 필드들(created
@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으로 인식하게 함
- 엔티티 클래스들이 BaseTimeEntity을 상속할 경우 필드들(created
@EntityListeners(AuditingEntityListener.class)
- BaseTimeEntity 클래스에 Auditing 기능을 포함시킴
@CreatedDate
- 엔티티 생성→ 저장 시 시간 자동 저장
@LastModifiedDate
- 조회한 엔티티의 값 변경 시의 시간을 자동 저장
테스트 결과
→ 등록, 수정 날짜, 시간이 잘 출력된다
'Spring > [dsc] Spring-Novice-Study' 카테고리의 다른 글
[Chap_2] Rest API Controller 생성 및 단위 테스트 (0) | 2022.10.01 |
---|---|
[Chap_1] 프로젝트 생성 및 깃 동기화 (1) | 2022.10.01 |