스프링부트와 AWS로 혼자 구현하는 웹서비스 라는 책을 읽고 JPA 를 보게 되었다.
책 내용만으로 JPA를 사용하기 어려워서
김영한님이 세미나에서 JPA강의 하신 영상을 찾아서 봤는데
30분짜리 영상 1개인줄알았는데 8강까지 있더라
.. JPA 카테고리를 만들었다
아래 내용은 책내용 / 블로그 / 김영한님 강의내용에 대한 나의 필기이다.
※ JPA
책에서는 지금까지 내가 학원에서 배운것을 토대로 프로젝트를 만들면서 가진 의문점들을 콕 찝어서 말해준다
▶ JPA 등장배경
문제점 1. 객체지향 프로그래밍을 배웠는데 왜 객체지향 프로그래밍을 못하지 ?
객체 모델링보다는 테이블 모델링에만 집중하고,
객체를 단순히 테이블에 맞추어 데이터 전달 역할만 하는 다소 기형적인 형태.
프로젝트의 모든 코드는 SQL 중심이었음 (관계형 데이터베이스가 SQL 만 인식할 수 있기 때문).
따라서 테이블마다 기본적인 CRUD SQL 을 매번 생성해야 하고,
SQL을 통해야만 데이터베이스에 저장하고 조회할 수 있다.
학원에서 진행한 토이프로젝트에서 CRUD SQL 만드는 것도 단순반복 작업이라서 지겨웠는데,
현업에서는 이런걸 몇천개를 만드는건가 ..? 의문이 있었음
(호돌맨은 단순반복 작업이 3번이상 진행되면 의문을 가지라했음)
반복 SQL 작업 외에 책에서 말하는 문제점이 한가지 더 있다
문제점 2. 패러다임 불일치 (패러다임 : 인식의 체계)
관계형 데이터베이스 : 어떻게 데이터를 저장할지에 초점이 맞춰진 기술임
객체지향 프로그래밍언어 : 메시지를 기반으로 기능과 속성을 한곳에서 관리하는 기술
둘의 패러다임이 서로 다른데, 객체를 데이터베이스에 저장하려고 하니 문제가 발생하고, 이를 패러다임 불일치 라고 한다
Q. 어떻게 하면 관계형 데이터베이스를 이용하는 프로젝트에서 객체지향 프로그래밍을 할 수 있을까 ?
A. JPA
▶ JPA 란
- 자바 표준 ORM
ㄴ Object Relational Mapping (객체 관계 매핑)
: 데이터베이스를 사용하는 서비스를 객체지향적으로 구현하는데 큰 도움을 주는 도구이다
- 관계형 데이터베이스와 객체지향 프로그래밍언어의 중간에서 패러다임 일치를 시켜주기위한 기술이다
- 개발자는 객체지향적으로 프로그래밍을하고, JPA 가 관계형데이터베이스에 맞게 SQL을 대신 생성해서 실행한다
- JPA 를 통해 개발자는 더이상 SQL 에 종속적인 개발을 하지 않아도 된다 !
▶ JPA 필수 설정 속성 persistance.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" version="2.2">
<persistence-unit name="hello">
<properties>
<!-- 필수 속성 -->
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<!-- 옵션 -->
<property name="hibernate.show_sql" value ="true"/>
<property name="hibernate.format_sql" value ="true"/>
<property name ="hibernate.use_sql_comments" value="true"/>
</properties>
</persistence-unit>
</persistence>
▷ h2 접속
cmd > cd C:\Program Files (x86)\H2\bin > h2.bat
▶ JPA와 CRUD
저장 : jpa.persist(member)
- 기존 ) 회원가입 => 회원테이블 insert, 주소테이블 insert, 방문자테이블 insert => DB와 3번 인터렉션
- JPA ) 회원가입 => 3개 insert를 받아놨다가 한번에 DB에 insert 한다
transaction.begin() //트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
// 여기까지 INSERT SQL을 DB에 보내지 않는다
//커밋하는 순간 DB에 INSERT SQL을 모아서 보낸다
transaction.commit(); //[트랜잭션] 커밋
test
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
//EMF는 하나만 생성해서 애플리케이션 전체에서 공유
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
//JPA의 모든 데이터 변경은 트랜잭션 안에서 실행
tx.begin();
Member member = new Member();
member.setId(100L);
member.setName("한서현");
tx.commit();
em.close();
emf.close();
}
실제사용예시
조회 : Member member = jpa.find(memberId)
- 개발자가 type 과 식별자를 넣으면 객체를 뱉어냄
- jpa 알아서 조인해서 한방쿼리로 날려서 가져옴
Album album = jpa.find(Album.class, albumId);
수정 : member.setName("변경할이름")
- 트랜잭션이 커밋되는 시점에 jpa 가 알아서 변경된 내용을 찾아서 수정해줌 .. 왁 대박
삭제 : jpa.remove(member)
▶ JPA 를 사용한다면 당신은
- CRUD 쿼리를 직접 작성할 필요가 없다
- 부모 - 자식 관계표현, 1:N 관계표현, 상태와 행위를 한곳에서 관리할 수 있다
▶ JPA 기본 어노테이션
@Getter
@NoArgsConstructor
@Entity
public class Posts extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
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;
}
public void update(String title, String content) {
this.title = title;
this.content = content;
}
}
@Entity
- 실제 DB 의 테이블과 일대일로 매칭될 클래스 (보통 Entity 클래스라고 부름)
- JPA 사용 시 실제 쿼리를 날리기보다, Entity 클래스의 수정을 통해 작업할 수 있다
- Entity 클래스에서는 절대 Setter 메소드를 만들지 않는다 !
=> 해당 클래스의 인스턴스 값들이 언제 어디서 변해야하는지 코드상으로 명확하게 구분할 수가 없기 때문
- 절대로 Entity 클래스를 Request / Response 클래스로 사용하지마라
@Id
- 해당 테이블의 PK
@GeneratedValue
- PK 생성규칙을 나타냄
- GenerationType.IDENTITY 옵션을 추가해야만 auto_increment 가 됨 ( = 오라클 .nextval)
@Table (name = "user_address")
- @Table 어노테이션의 실제 명칭을 지정하지 않는다면
Entity 클래스의 이름 그대로 카멜케이스를 유지한 채 테이블이 생성됨
(위 예시의 경우엔 posts 라는 이름으로 테이블이 생성되겠지 ? )
@Colum
- 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 컬럼이 됨
- 기본값 외에 추가로 변경이 필요한 옵션이 있으면 사용
ex ) 문자열 기본값 : varchar(255) 인데 varchar(500) 으로 늘리고 싶음
@Builder => 얘는 롬복 어노테이션임
- setter 를 쓰지 않는 대신 생성자를 통해 DB에 삽입을 해야하는데,
생성자 대신 @Builder 어노테이션을 통해 제공되는 빌더 클래스를 사용할 수 있다
▶ JpaRepository
- Mybatis 에서 DAO 라고 불리는 DB Layer
- DAO = Repository
- 단순히 인터페이스를 생성 후 JpaRepository<Entity 클래스, PK타입 > 을 상속하면
기본적인 CRUD 메소드가 자동으로 생성된다
- 이때 Entity 클래스와 Entity Repository는 함께 위치해야한다
Post.java : Entity 클래스
PostsRepository.java : Entity Repository
▷ PostsRepository.java (Entity Repository)
public interface PostsRepository extends JpaRepository<Posts, Long> {
@Query("SELECT p FROM Posts p ORDER BY p.id DESC")
List<Posts> findAllDesc();
}
▶ JpaRepository Test 하기
- @WebMvcTest 는 JPA 기능이 작동하지않는다. 따라서 @SpringBootTest사용
@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {
@Autowired
PostsRepository postsRepository;
@After //단위테스트가 끝날대마다 수행되는 메소드를 지정
public void cleanup() {
postsRepository.deleteAll();
}
@Test
public void 게시글저장_불러오기() {
//given
String title = "테스트 게시글";
String content = "테스트 본문";
postsRepository.save(Posts.builder() //테이블posts에 insert/update 쿼리 실행
//id가 있으면 update,없으면 insert가 실행됨.. 대박 !!
.title(title)
.content(content)
.author("jojoldu@gmail.com")
.build());
//when
List<Posts> postsList = postsRepository.findAll(); // posts 테이블의 모든 데이터 조회 (selectAll)
//then
Posts posts = postsList.get(0);
assertThat(posts.getTitle()).isEqualTo(title);
assertThat(posts.getContent()).isEqualTo(content);
}
}
▷ TEST 결과
우왕 신기 . .!!
자 그럼 테스트가 성공된것을 확인했으니
API 를 만들어보자 !
▶ API 만들기
▷ API 생성을 위한 클래스
- DTO : Request 데이터를 받음
- Controller : API 요청을 받음
- Service : 트랜잭션, 도메인 기능 간의 순서를 보장
[ 스프링 웹 계층 ]
Service Layer
- @Service 영역
- Controller 와 Dao 중간 영역에서 사용됨
- @Transactional 이 사용되어야하는 영역
Repository Layer
- DB에 접근하는 영역 ( 기존 DAO )
DTOs
- DTO : 계층간에 데이터 교환을 위한 객체
- Dtos : DTO 의 영역
Domain Model
- @Entity 가 사용된 영역 = 도메인 모델 이라고 이해할 수 있지만,
무조건 데이터베이스의 테이블과 관계가 있어야하는 것은 아니다. VO 처럼 값 객체들도 이 영역에 해당함
▷ 등록하기 TEST
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private PostsRepository postsRepository;
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
@After
public void tearDown() throws Exception {
postsRepository.deleteAll();
}
@Test
@WithMockUser(roles="USER")
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
mvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(new ObjectMapper().writeValueAsString(requestDto)))
.andExpect(status().isOk());
//then
List<Posts> all = postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(title);
assertThat(all.get(0).getContent()).isEqualTo(content);
}
▷ 등록하기
PostApiController
@RequiredArgsConstructor
@RestController
public class PostsApiController {
private final PostsService postsService;
@PostMapping("/api/v1/posts")
public Long save(@RequestBody PostsSaveRequestDto requestDto) { //
return postsService.save(requestDto);
}
△ 왜 PostSaveRequestDto 를 또 만들었을까 ? => @Entity 클래스를 Request / Response 클래스로 사용하믄안돼서 !
PostSaveRequestDto
@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();
}
}
PostsService
@RequiredArgsConstructor
@Service
public class PostsService {
private final PostsRepository postsRepository;
@Transactional
public Long save(PostsSaveRequestDto requestDto) {
return postsRepository.save(requestDto.toEntity()).getId();
}
▷ 수정하기 / 삭제하기 / 조회하기 / 전체조회하기
PostApiController
@PutMapping("/api/v1/posts/{id}")
public Long update(@PathVariable 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;
}
@GetMapping("/api/v1/posts/{id}")
public PostsResponseDto findById(@PathVariable Long id) {
return postsService.findById(id);
}
@GetMapping("/api/v1/posts/list")
public List<PostsListResponseDto> findAll() {
return postsService.findAllDesc();
}
PostsService
@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;
}
@Transactional
public void delete (Long id) {
Posts posts = postsRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 사용자가 없습니다. id=" + id));
postsRepository.delete(posts);
}
@Transactional(readOnly = true)
public PostsResponseDto findById(Long id) {
Posts entity = postsRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 사용자가 없습니다. id=" + id));
return new PostsResponseDto(entity);
}
@Transactional(readOnly = true)
public List<PostsListResponseDto> findAllDesc() {
return postsRepository.findAllDesc().stream()
.map(PostsListResponseDto::new)
.collect(Collectors.toList());
}
'JPA' 카테고리의 다른 글
[JPA] 연관관계 매핑이론 : 단방향, 양방향 매핑 (0) | 2022.07.25 |
---|