It's going to be one day 🍀

안녕하세요! 매일 매일 공부하려고 노력하는 백엔드 개발자 지망생의 공부 흔적입니다.

Back-End/Spring

[Spring] JPA로 CRUD 블로그 만들기 실습 정리

2jin2 2024. 3. 12. 17:17

오늘은 JPA로 CRUD를 진행하며 블로그 만들기 실습을 진행했다.

수요일부터 비슷한 내용을 계속 반복하니까 드디어 비로소 좀 이해가 된 것 같다. 아직 완벽하게 이해하진 못했지만, 계속 복습하면 완벽하게 익힐 수 있을 거라구 생각한다..

 

1단계 : 프로젝트 새로 생성하기 (의존성 추가하기)

blog-jpa 프로젝트 새로 생성

먼저 새로운 프로젝트를 생성했다. 화면에 보이는 의존성들을 추가해주었다.


2단계 : 엔티티 구성하기

JPA로 쿼리를 작성하고 데이터베이스와 연결해주었다.

create table if not exists article (
    id BIGINT AUTO_INCREMENT primary key,
    title varchar(255) not null,
    content varchar(255) not null
);

INSERT INTO article (title, content) VALUES ('블로그 제목', '블로그 내용');
INSERT INTO article (title, content) VALUES ('블로그 제목2', '블로그 내용2');
INSERT INTO article (title, content) VALUES ('블로그 제목3', '블로그 내용3');

나는 h2-console에서 테이블을 확인했다. localhost:8080/h2-console로 찾아가면 된다.

localhost:8080/h2-console를 입력하면 나오는 화면

여기에 내가 사용하는 환경의 application.properties를 보면서 알맞게 입력하자. 대부분 그대로 Connect 하면 될텐데 나는 JDBC URL이 틀리게 입력되어있어서 내가 사용하는 환경의 URL에 맞게 다시 입력해주었다. 틀리게 입력하면 Connect가 안된다.

들어가서 article 테이블을 직접 확인하면 데이터들이 잘 들어가 있는 걸 볼 수 있다. 

 

그리고 인텔리제이에 Entity를 구성해주자.

@Getter
@Entity
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", updatable = false)
    private Long id;

    @Column(name = "title", nullable = false)
    private String title;

    @Column(name = "content", nullable = false)
    private String content;

    @Builder // 생성자 위에 빌더 패턴을 사용하면 어느 필드에 어떤 값이 들어가는지 명시적으로 파악할 수 있음
    public Article(String title, String content) {
        this.title = title;
        this.content = content;
    }

    public Article() {
    }

    public Long getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public String getContent() {
        return content;
    }

- @Entity를 사용하면 이 클래스를 Entity로 사용한다고 선언해주는 것이다. 여기서 빌터 패턴을 사용했는데,

 

빌더 패턴이란?

@Builder 어노테이션은 롬복에서 지원하는 어노테이션인데, 이 어노테이션을 생성자 위에 입력하면 명시적으로 객체를 생성할 수 있어서 편리하다.

// 빌더 패턴을 사용하지 않았을 때
new Article("블로그 제목", "내용");

// 빌더 패턴 사용했을 때
Article.Builder()
	.title("블로그 제목")
	.content("내용");

 

이 구조를 다시 복습하면서 이제 본격적으로 패키지를 만들어보자. 


3단계 : 레파지토리 만들기

repository에 BlogRepository 인터페이스를 생성해준다.

// JPA를 사용할 수 있게 해줌
@Repository
public interface BlogRepository extends JpaRepository<Article, Long> {
}

JpaRepository 클래스를 상속받을 때 엔티티 Article과 엔티티의 PK 타입인 Long을 인수로 넣으면, 이 레파지토리를 사용할 때 JpaRepository에서 제공하는 여러 메소드를 사용할 수 있다.


4단계 : 블로그 글 작성을 위한 API 구현하기 (Create)

1) 서비스 코드 작성하기

먼저, dto 패키지 안에 서비스 계층에서 요청을 받을 객체인 AddArticleRequest 클래스를 생성한다.

@NoArgsConstructor
@Getter
@AllArgsConstructor
@Builder
public class AddArticleRequest {
    private String title;
    private String content;

    public Article toEntity() { // 생성자를 사용해 객체 생성 
        return Article.builder()
                .title(title)
                .content(content)
                .build();
    }
}

toEntity()는 위에서 사용한 빌더 패턴을 사용해서 dto를 엔티티로 만들어주는 메소드이다. 이 메서드는 나중에 블로그 글을 추가할 때 저장할 엔티티로 변환하는 용도로 사용한다.

 

그리고 service 패키지에 BlogService 클래스를 생성한다.

@Service
public class BlogService {
    private final BlogRepository blogRepository;

    public BlogService(BlogRepository blogRepository) {
        this.blogRepository = blogRepository;
    }

    // save 메소드는 JpaRepository에서 제공하는 저장 메소드
    // AddArticleRequest 클래스에 저장된 값들을 article 데이터베이스에 저장한다.
    public Article save(AddArticleRequest request) {
        return blogRepository.save(request.toEntity());
    }
}

save() 메소드는 JpaRepository에서 지원하는 저장 메소드이다. AddArticleRequest 클래스에 저장된 값들을 article 데이터베이스에 저장하는 역할을 한다.

 

2) 컨트롤러 코드 작성하기

다음으로는 URL 매핑을 위한 컨트롤러 코드를 추가한다.

@RestController
public class BlogController {
    private final BlogService blogService;

    public BlogController(BlogService blogService) {
        this.blogService = blogService;
    }

    @PostMapping("/api/articles") // json {"title" : "제목", "content" : "내용"}
    public ResponseEntity<ArticleResponse> addArticle(@RequestBody AddArticleRequest request) {
        // @RequestBody로 AddArticleRequest를 받아와 request로 저장
        Article article = blogService.save(request);
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(article.toResponse()); // json
    }
}

@RestController 어노테이션을 클래스에 붙이면, HTTP 응답으로 객체 데이터를 JSON 형식으로 반환한다. 그러면 /api/articles는 addArticle() 메소드에 매핑하게된다. 

 

여기까지 Create(POST) 구현 완료!

postman으로 실습해보면 잘 Create 된 걸 볼 수 있다!


 

5 단계 : 글 목록 조회를 위한 API 구현하기 (Read)

1) 서비스 코드 추가하기

글을 조회하기 위해선 API를 통해서 글 목록을 조회해야한다. 해당 기능은 서비스 로직에 들어가야 하는 기능이기 때문에, BlogService 클래스에 findAll() 메소드를 추가해보자. 

public List<Article> findAll() {
        return blogRepository.findAll();
    }

    public Article findById(Long id) {
        return blogRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("!예외처리! 찾을 수 없는 id값 : "+id)); // 빈값일 경우에 대한 예외처리 로그 남기기
//        return blogRepository.findById(id).orElse(new Article()); // return 값이 Object로 돌아오기 때문에 orElse() 처리를 해줬음. 없으면 빈값 나옴.
    }

JPA 지원 메소드인 findAll()을 호출하면 article 테이블에 저장되어있는 모든 데이터를 조회한다. findById()는 블로그 단건을 상세조회하는 API이다. 만약 찾을 수 없는 (예를들어 데이터 id가 1, 2, 3 밖에 없을 때 4를 찾는다면) id를 찾는다면 에러가 나는데 이때 따로 orElseThrow()를 사용해 예외처리를 해줄 수 있다.

 

2) 컨트롤러 코드 추가하기

dto 디렉터리에 응답을 위한 ArticleResponse DTO를 작성한다. 

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ArticleResponse {
    private String title;
    private String content;
    private Long id;

    public ArticleResponse(Article article) {
        title = article.getTitle();
        content = article.getContent();
        id = article.getId();
    }
}

 

그리고 BlogController에 코드를 추가해준다.

// @RequestMapping은 value와 method를 명시해주어야 함.
@RequestMapping(value = "/api/articles", method = RequestMethod.GET)
public ResponseEntity<List<ArticleResponse>> showArticle() {
    // findAll() 메소드를 호출한 다음 응답용 객체인 ArticleResponse로 파싱함
    List<Article> articleList = blogService.findAll();
    // Article -> ArticleResponse로 변환하는 로직에 스트림을 적용한 것
    List<ArticleResponse> responseList = articleList.stream()
            .map(ArticleResponse::new)
            .toList();
    return ResponseEntity.ok(responseList);
}

// 블로그 글 단건 상세조회 API 구현
@GetMapping("/api/articles/{id}")
public ResponseEntity<ArticleResponse> showOneArticle(@PathVariable Long id) {
    Article article = blogService.findById(id);
    return ResponseEntity.ok(article.toResponse());
}

 

-> 전체를 조회할 땐 findAll()을 사용했고 단건만 조회할 땐 findById()를 사용했다.

 

findAll() 조회시
findById() 사용시

각각 잘 GET 하는 걸 볼 수 있다!

여기까지 Read(GET) 구현 완료!


6단계 : 블로그 글 삭제 API 구현하기 (Delete)

1) 서비스 코드 추가

public void deleteById(Long id) {
    blogRepository.deleteById(id);
}

JPA에서 제공해주는 deleteById 메소드를 사용해 간단하게 구현할 수 있다.

 

2) 컨트롤러 코드 추가

@DeleteMapping("/api/articles/{id}")
// 여기서 Void로 반환값을 지정한 이유는 아무런 값도 return을 해주지 않기 때문
public ResponseEntity<Void> deleteOneArticle(@PathVariable Long id) {
    blogService.deleteById(id);
    return ResponseEntity.ok().build();
}

/api/articles/{id} URL에서 {id}에 해당하는 값이 @PathVariable Long id 값으로 들어온다. 꼭 이 어노테이션을 선언해줘야 요청으로 들어온 id값을 받아올 수 있다.

DELECT를 통해 id 1, 3 데이터를 지우고 다시 GET로 검색해본 결과이다. 1, 3 데이터가 잘 사라진걸 볼 수 있다.

h2-console로 데이터베이스를 확인해봐도 잘 바뀐걸 볼 수 있다.

여기까지 Delete(DELECT) 구현 완료!


7단계 : 블로그 글 수정 API 구현하기 (Update)

먼저, Entity 객체인 Article에 update 메소드를 추가해준다.

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

 

1) 서비스 코드 추가

@Transactional
// 이 @Transactional을 붙여주면 이게 실제 쿼리로 날라감. 안붙여주면 반영이 안됨

public Article update(Long id, AddArticleRequest request) {
    // begin transaction
    Article article = findById(id);
    article.update(request.getTitle(), request.getContent());

    // commit / rollback
    return article;
}
@Transactional
public Article updateTitle(Long id, AddArticleRequest request) {
    Article article = findById(id);
    blogRepository.updataTitle(id, request.getTitle());
    return article;
}

Transactional 어노테이션은 매칭한 메소드를 하나의 트랜잭션으로 묶는 역할을 한다. 

updateTitle은 데이터의 title만 변경하는 메소드이다.

 

2) 컨트롤러 코드 추가

@PutMapping("api/articles/{id}")
    public ResponseEntity<Article> updateOneArticle(@PathVariable Long id,
                                                    @RequestBody AddArticleRequest request) {
        Article update = blogService.update(id, request);
//        Article update = blogService.updateTitle(id, request);
        return ResponseEntity.ok(update);
    }

PUT 메서드로 /api/articles/{id} URL이 들어오면, RequestBody 정보가 request 정보로 넘어온다. 그리고 다시 서비스 클래스의 update() 메소드에게 id, request를 넘겨준다. 그리고 최종 응답값을 출력해준다.

postman 확인 결과, id가 2인 데이터의 title과 content가 잘 변경되었다.
h2-console에서도 변경된 값을 확인 할 수 있다.

여기까지 Update(PUT) 구현 완료!


지금까지의 핵심 요약

  1. REST API는 웹의 장점을 최대한 활용하는 API로, 자원을 이름으로 구분해 자원의 상태를 주고받는 방식이다.
  2. JpaRepository를 상속받으면 Spring Data JPA에서 지원하는 여러 메소드를 간편하게 사용할 수 있다.
  3. 롬복을 사용하면 더 깔끔하게 코드를 작성할 수 있다.

정말 반복만이 답이다....!!