JPA ← Hibernate ← Spring Data JPA
- 게시판 기능
- 게시글 조회, 등록, 수정, 삭제
- 회원 기능
- 구글/네이버 로그인, 로그인한 사용자 글 작성 권한, 본인 작성글에 대한 권한 관리
compile('org.springframework.boot:spring-boot-starter-data-jpa')
: 스프링 부트 버전에 맞춰 자동으로 JPA 관련 라이브러리 버전들을 관리해줌
compile('com.h2database:h2')
: 별도의 설치 필요업이 프로젝트 의존성만으로 관리할수 있음, 메모리에서 실행됨 → 애플리케이션을 재시작할때마다 초기화되기 때문에 테스트 용도로 많이 사용함
@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;
}
}
@NoArgsConstructor
: 기본 생성자 자동 추가, public Posts() {}
와 같은 효과@Getter
: 클래스 내 모든 필드의 Getter 메소드를 자동 생성@Builder
: 해당 클래스의 빌더 패턴 클래스를 생성, 생성자 상단에 선언시 생성자에 포함된 필드만 빌더에 포함Posts 클래스의 한가지 특이점 : Setter 메소드가 없다.
getter / setter를 무작정 생성하게 되면 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확히 구분할 수가 없다.
그래서 Entity 클래스에서는 절대 Setter 메소드를 만들지 않는다.
대신, 해당 필드 값을 변경하고 싶다면 명확히 그 목적과 의도를 나타낼 수 있는 메소드를 추가하여 변경하여야 한다.그럼 Setter가 없는 이 상황에서 어떻게 값을 채워서 DB에 삽입을 할까?
1. 기본적인 구조는 생성자를 통해 최종값을 채운 후 DB에 삽입하는것
2. 변경이 필요할 경우 해당 이벤트에 맞는 메소드를 호출하여 변경하는것을 전제로함
public interface PostsRepository extends JpaRepository<Posts, Long> {
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class PostRepositoryTest {
@Autowired
PostsRepository postsRepository;
@After
public void cleanup() {
postsRepository.deleteAll();
}
@Test
public void 게시글저장_불러오기() {
//given
String title = "테스트 게시글";
String content = "테스트 본문";
postsRepository.save(Posts.builder()
.title(title)
.content(content)
.author("jojoldu@gmail.com")
.build());
//when
List<Posts> postsList = postsRepository.findAll();
//then
Posts posts = postsList.get(0);
assertThat(posts.getTitle()).isEqualTo(title);
assertThat(posts.getContent()).isEqualTo(content);
}
}
@After
: 단위 테스트가 끝날때마다 수행되는 메소드를 지정, 여러 테스트가 동시에 수행되면 테스트용 데이터가 그대로 남아 있어 다음 테스트가 실패할 수 있기때문에 전부 삭제해준다.save
: 테이블 posts에 insert/update 쿼리를 실행한다. id 값이 있다면 update, 없다면 insert 쿼리가 실행된다.findAll
: 테이블 posts에 있는 모든 데이터를 조회해오는 메소드API를 만들기 위해서는 3개의 클래스가 필요함
1. Request 데이터를 받을 dto
2. API 요청을 받을 Controller
3. 트랜잭션, 도메인 기능간의 순서를 보장하는 Service
여기서 Service는 비지니스 로직을 처리하지 않고 트랜잭션, 도메인 간의 순서보장만 함
기존에 Service로 로직을 처리하던 방식을 트랜잭션 스크립트라고 부르는데, 이 방식을 사용하다보면
서비스 클래스 내부에서 모든 로직이 처리되게 되고 객체는 단순히 데이터 덩어리 역할만 하게 됨
PostsApiController
@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);
}
}
PostService
@RequiredArgsConstructor
@Service
public class PostsService {
private final PostsRepository postsRepository;
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);
}
}
@Autowired 어노테이션이 없는 이유
final이 선언된 모든 인자값으로 하는 생성자를 롬복의 어노테이션 @RequiredArgsConstructor가 생성해서 빈을 주입생성자를 직접 안쓰고 롬복의 어노테이션을 사용하는 이유 : 해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 계속 수정해야하는 번거로움을 줄이기 위해서
update 메소드에서 데이터베이스에 쿼리를 날리는 부분이 없다.
-> JPA의 영속성 컨텍스트 때문
? 영속성 컨텍스트 : 엔티티를 영구 저장하는 환경, JPA의 핵심 내용은 엔티티가 영속성 컨텍스트에 포함되어 있냐 아니냐로 갈린다.
JPA의 엔티티 매니저가 활성화된 상태로 트랜잭션 안 데이터베이스에서 데이터를 가져오면 이 데이터는 영속성 컨텍스트가 유지된 상태
영속성 컨텍스트가 유지된 상태에서 해당 데이터 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경된 사항을 반영함
PostsSaveRequestDto
@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();
}
}
각자의 역할에 맞게 사용해야 하기 때문에 Entity 클래스인 Posts와 거의 유사한 형태임에도 dto 클래스를 새로 추가했음
Entity 클래스 | Request/Response dto |
---|---|
데이터베이스와 맞닿은 핵심 클래스 | view를 위한 클래스 |
Entity 클래스를 기준으로 테이블이 생성, 스키마 변경 | 변경이 잦으며, 결과 값으로 여러 테이블을 조인해서 줘야할 경우가 빈번함 |
PostResponseDto
@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();
}
}
PostUpdateRequestDto
@Getter
@NoArgsConstructor
public class PostsUpdateRequestDto {
private String title;
private String content;
@Builder
public PostsUpdateRequestDto(String title, String content){
this.title = title;
this.content = content;
}
}
보통 엔티티에는 해당 데이터의 생성시간과 수정시간을 포함한다.(차후 유지보수에 중요한 정보이기 때문에) 그렇다 보니 매번 날짜 데이터를 등록/수정하는 코드가 포함되게 된다.
이런 단순하고 반복적인 코드가 모든 테이블과 서비스에 포함되어야 한다면 매우 지저분해지기 때문에 JPA Auditing을 사용하여 해결하려고 한다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
@CreatedDate
private LocalDateTime createDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
마지막으로 JPA Auditing에 관련된 모든 어노테이션을 활성화 할 수 있도록 @EnableJpaAuditing 어노테이션을 메인메소드에 추가함