web패키지에 PostsApiController 클래스에 추가
web.dto 패키지에 PostsResponseDto 생성
web.dto 패키지에 PostsUpdateRequestDto 생성
domain.posts 패키지에 Posts 클래스에 추가
com.jojoldu.book.springboot에 service 패키지에 PostsService 클래스에 추가
PostsApiController 코드
package com.jojoldu.book.springboot.web;
import com.jojoldu.book.springboot.service.PostsService;
import com.jojoldu.book.springboot.web.dto.PostsResponseDto;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import com.jojoldu.book.springboot.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@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);
}
}
PostsResponseDto 코드
⇒ DTO 클래스의 역할은 데이터를 받아오는 것이다.
package com.jojoldu.book.springboot.web.dto;
import com.jojoldu.book.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();
}
}
PostsResponseDto는 Entity의 필드 중 일부만 사용하므로 생성자로 Entity를 받아 필드에 넣는다.
PostsUpdateRequestDto 코드
⇒ DTO 클래스의 역할은 데이터를 받아오는 것이다.
package com.jojoldu.book.springboot.web.dto;
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;
}
}
Posts 코드
package com.jojoldu.book.springboot.domain.posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Getter
@NoArgsConstructor
@Entity
public class Posts {
@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;
}
}
PostsService 코드
package com.jojoldu.book.springboot.service;
import com.jojoldu.book.springboot.domain.posts.Posts;
import com.jojoldu.book.springboot.domain.posts.PostsRepository;
import com.jojoldu.book.springboot.web.dto.PostsResponseDto;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import com.jojoldu.book.springboot.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@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);
}
}
findById에는 @Transactional이 없다.
조회하는것은 트랜잭션이 필요가 없다. (물론 개발자가 짜기 나름)
update나 delete 같은 것에는 트렌잭션이 있어야함
원자성을 확보하기 위함
원자성
a → b 돈 송금
어카운트라는 테이블이 있다고 할 때
a의 어카운트에 있는 잔액을 업데이트하고 b의 어카운트에 있는 잔액을 업데이트하고 동시에 일어나야함
update 코드를 보면 데이터베이스에 쿼리를 날리는 부분이 없다. JPA의 영속성 컨텍스트 덕분이다.
영속성 컨텍스트
영속성 컨텍스트는 Entity를 영구히 저장하는 환경으로, 일종의 논리적인 개념이다. JPA의 핵심은 Entity가 영속성 컨텍스트에 포함되는지 아닌지로 갈린다.
JPA의 EntityManager
가 활성화된 상태로 트랜잭션 안에서 DB 데이터를 가져온다면 영속성 컨텍스트가 유지된 상태라고 볼 수 있다. Spring Data Jpa를 쓴다면 기본값으로 EntityManager
가 활성화 된다.
이 상태에서 해당 데이터의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블의 변경 사항을 반영한다. 즉, Entity 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없는 것이다. 이 개념을 더티 체킹
이라고 한다.
만약 @Transactional가 없다면 아래처럼 작성 해야한다.
public Long update(Long id, PostsUpdateRequestDto requestDto) {
Posts posts = postsRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));
posts.update(requestDto.getTitle(), requestDto.getContent());
postsRepository.save(posts);
return id;
}
더티 체킹
엔티티의 라이브사이클
JPA 트랜잭션
엔티티에 변화가 생긴 후 jpa컨택스트가 감지해서 수정을 할 수 있게끔함
트랜잭션이 붙으면 추적가능
안붙으면 추적 불가능
즉, @Transactional이 있어야만 영속성 컨텍스트에 포함된다.
트랜잭션이 붙어있으면 디비를 마지막의 상태로 바꿔준다. set 2개가 있어도 마지막 update만 한 번 일어난다
@Transactional
public PostsResponseDto findById(Long id) {
Posts entity = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id= " + id));
entity.setAuthor("dlcksgml");
entity.setAuthor("dlwjdcgns");
return new PostsResponseDto(entity);
}
즉, 중간에 데이터를 가로채서 dlwjdcgns로 나타낸다.