백엔드/Spring Boot

6. 블로그 기획하고 API 만들기 (3) - 블로그 글 작성을 위한 API 구현❷

두개의 문 2023. 7. 27. 09:34

4. 블로그 전체 글 목록 조회를 위한 API 구현하기 

 

 - 클라이언트는 데이터베이스에 직접 접근 불가 → API를 구현해볼 수 있도록 해야 함 

 - 모든 글을 조회하는 API를 구현해보자 

 

 


1) 서비스 메서드 코드 작성하기 

 

◉ BlogService.java 파일에 데이터베이스에 저장되어 있는 글을 모두 가져오는 findAll() 메서드 추가 

 

▪︎ BlogService.java

 - JPA 지원 메서드인 findAll( )을 호출해 article 테이블에 저장되어 있는 모든 데이터 조회 

@RequiredArgsConstructor	// final이 붙거나 @NotNull이 붙은 필드의 생성자 추가 
@Service 	// 빈으로 등록 
public class BlogService {
	// 데이터베이스와 작업 모델 ( 특히 JPA의 모델은 리포지토리 ) 멤버변수 선언 
	// 의존성 주입 방법 
	private final BlogRepository blogRepository;	// 보통 클래스명의 소문자로 객체변수 선언 
	
	// * save () 메서드 생략 * 

	// * 데이터베이스에 저장되어 있는 글을 모두 가져오는 메서드
	public List<Article> findAll() {	 
		
		// ① 전체 흐름 만들기 
		// : 서비스는 레포지토리로부터 데이터를 가져온다 
		// blogRepository.findAll();
		
		// ② 리턴값 지정 
		// : JPA repository는 entity 자바객체와 테이블의 레코드가 1 : 1 연결됨 
		// * findAll()의 결과, 모든 글 반환 → List 컬렉션 이용 
		// : List<Article>
		
		return blogRepository.findAll();
	} 
}

 

 


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

 

◉ /api/articles GET 요청이 오면, 글 목록을 조회할 findAllArticles() 메서드 작성 

 

 

◎ dto 패키지에 ArticleResponse.java 파일 생성 

 

  - 글은 제목과 내용 구성 → 해당 필드( title, content )를 가지는 클래스를 만든 다음, 엔티티를 인수로 받는 생성자 추가 

package org.choongang.dto;

import org.choongang.domain.Article;
import lombok.Getter;

// * ArticleResponse 객체 : Article 엔터티의 응답용 객체로 임시 저장용
//   → 데이터 변수 구조는 같고 메서드는 생성자만 있으면 됨 
// * 임시 객체이므로 VO 객체가 아니라 DTO 객체라고 함
// - VO 객체 : 생성 후 변하지 않을 때 사용 
// - DTO 객체 : 생성 후 내용이 변할 수 있거나 임시 사용으로 만들어 질때 사용 / SQL의 뷰에 해당함 

@Getter 
public class ArticleResponse {
	
	// * 임시이지만 생성되고 고치면 안되므로, final로 지정 
	private final String title;
	private final String content;
	
	// * 데이터를 최초 한번 초기화하기 위한 메서드 -> 생성자 
    //   : 매개인자로 엔티티인 Article이 사용 
	public ArticleResponse(Article article) {
		this.title = article.getTitle();
		this.content = article.getContent();
	}
}

 

 

 

◎ controller 디렉터리에 있는 BlogApiController.java에 전체 글을 조회한 뒤 반환하는 findAllArticles() 메서드 추가 

 

	// * 글 전체 목록 조회  
	@GetMapping("/api/articles")
    // : GET METHOD /api/articles로 요청하면 이 메서드가 실행 
	public ResponseEntity<List<ArticleResponse>> findAllArticles() {
		// 지금은 JPA 공부 중이므로 서비스 메서드 이름과 리포지토리 메소드 이름을 같게 설정 
		// JPA의 findAll()과 동일하게 작성함 
		
		// ① 글 목록 요청 URL 주소 라우팅 
		//   서비스로 요청 - 서비스 객체의 메서드 호출 
		log.info("GET MAPPING /api/articles");
		log.info("브라우저에서 JSON 방식으로 아티클 목록 요청");

		
		// ② 서비스로부터 데이터 리턴 후 응답 준비 
		// 1) blogService.findAll()의 반환값은 List<Article> 타입 
		// 		따라서 List<Article> articleList = blogService.findAll()

		// 2) 기존의 고전적 방법 : 향상된 for구문을 이용한 절차적 프로그래밍을 이용 
		// ArticleResponse 객체를 응답용 json 또는 맵 데이터를 보관하는 배열이라고 하면
		// List<ArticleResponse> articles = new ArrayList<>();
		// for(Article item : articleList){
		// ArticleResponse articleResponse = 
		//			new ArticleResponse(
		//						item.get(0).title, item.get(0).content) ;
		//			articles.add(item);
		// }
		// return ResponseEntity.ok().body(articleResponse);
		
		// 3) 앞으로는 간단한 컬렉션은 스트림 객체로 대신할 수 있음 
		// for( 한개의 item : list객체 ) => List객체의 stream() 메서드로 대체 가능 
		// new 변환객체(item.get(0).개별멤버변수 ... ) => stream() 객체의 map(변환방법)으로 대체 가능 
		// 최종적으로 루프의 개별 항목들을 toList() 메서드로 List<변환객체>로 바꿀 수 있음 
		// ArticleResponse : 응답할 레코드 객체 하나 
		// ArticleResponse의 데이터는 Article 객체의 데이터부분과 같다 
		
		// blogService.findAll() => List<Article> 
		// blogService.findAll().stream() => 
		//   			for(Article item : blogService.findAll())
		// .map(Article::new) => 
		//			new ArticleResponse(item.get(0).title, item.get(0).content)
		// .toList() => List<ArticleResponse> 객체 반환 
		List<ArticleResponse> articles = blogService.findAll()
								.stream()
								.map(ArticleResponse::new)
								.toList();
								// ArticleResponse 객체를 생성하라.
								// 파라미터로 blogService.findAll()의 리턴값에
								// 요소 데이터(Article 객체)를 대입한다 
								// blogService.findAll() => List<Article>의 요소
							// => Article 객체 
		// ③ 응답데이터 웹 브라우저로 전송 
		// 1) 응답데이터는 뷰 페이지(HTML코드)거나 JSON 형태의 데이터 자체 
		//      rest서버이므로 JSON 형태 / ㄹ우리가 직접 만들 필요 없음 
		//      일반적인 경우는 스프링 부트가 대 해줌 
		//		ResponseEntity 객체가 바로 그 주인공
		// ResponseEntity 객체 :  엔터티 객체의 멤버 내용을 맵 방식으로 변환 가능 
		// 자바의 맵 데이터 형식은 json형식과 유사 
		// resongseEntity 객체는ㄴ 맵을 응답 데이터로 보냄 
		// 그 메서드가 responseenetity.body()
		
		//  ResponseEntity는 기본적으로 응답코드가 필요함 
		// ok() 메서드가 바로 응답코드 200을 의미 
		//ResponseEntity.ok().body(map 또는 json 데이터);
		//컨트롤러가 브라우저로 rest 데이터를 보ㅓ내게 됩니다. 

		
		//위의 List<ArticleResponse> articles를 .body(articles)하면 끝 
		return ResponseEntity.ok()
					.body(articles);
	}

 - /api/articles GET 요청이 오면, 글 전체를 요청하는 findAll( ) 메서드를 호출

   → 응답용 객체인 ArticleRepsonse로 파싱해 body에 담아 클라이언트에게 전송함 

        ( 이 코드에는 스트림을 적용 )

 

 


3) 실행 테스트하기 

 

◉ resources 폴더에 data.sql 생성 

INSERT INTO article(title, content) VALUES ('제목1', '내용1')
INSERT INTO article(title, content) VALUES ('제목2', '내용2')
INSERT INTO article(title, content) VALUES ('제목3', '내용3')

 

 

◉ 포스트맨에서 확인 

 - 포스트맨에서 HTTP 메서드 : GET / [Params] 탭으로 변경 후, URL에 'http://localhost/api/articles' 입력 → Send 클릭 


 오라클의 경우 ★ 

 - 일련번호의 자동 생성 기능 지원 안됨 → SEQUENCE 객체 생성해야 함 

    SQL : CREATE SEQUENCE 시퀀스명; 

 - SEQUENCE 객체 생성 후, '시퀀스명.nextval'로 새로운 일련번호 생성 가능 

 - '시퀀스명.currval' : 현재 번호 의미

INSERT INTO ARTICLE
	(id, title, content, created_at, updated_at)
    VALUES
    (article_seq.nextval, '제목1', '내용1', SYSDATE, SYSDATE);
    
INSERT INTO ARTICLE
	(id, title, content, created_at, updated_at)
    VALUES
    (article_seq.nextval, '제목2', '내용2', SYSDATE, SYSDATE);
    
INSERT INTO ARTICLE
	(id, title, content, created_at, updated_at)
    VALUES
    (article_seq.nextval, '제목3', '내용3', SYSDATE, SYSDATE);

 


4) 테스트 코드 작성하기 

▪︎ BlogApiController.java

 

given 블로그 글을 저장함 
when 목록 조회 API를 호출함 
then 응답 코드가 200 Ok이고, 반환받은 값 중에 0번째 요소의 content와 title이 저장된 값과 같은지 확인함 
@DisplayName("findAllArticles() : 블로그 글 목록 조회 실행 성공")
	@Test
	void testFindAllArticles() throws Exception {
		// * given : 조건 설정 
		//  - 요청 주소 uri 및 입력 데이터 변수 선언 
		//  -> 이 부분은 조건으로 중간에 변경할 수 없도록 상수로 설정 
		final String url = "/api/articles";
		final String title = "테스트용 title";
		final String content = "테스트용 content";
		
		//  - 입력데이터 추가 : JPA에서 제공하는 save() 메서드 이용 
		//   	INSERT INTO 테이블 (컬럼, ...) VALUES (값, ...);
		blogRepository.save(Article.builder()
							.title(title)
							.content(content)
							.build());
		
		// * when : 설정된 조건을 이용하여 실행 
		//  - ResultActions
		//    : 테스트에서 어떤 테스트 명령을 수행하여 그 결과를 저장하는 객체 
		//      이 객체의 andExpert()를 가지고 우리가 기대하는 값과 결과를 비교함 
		//      -> 값이 일치하면 테스트 성공, 아니면 실패 
		//  - mockMvc.perform : 가상서버의 주어진 url로 접속하여 
		// 			데이터(MediaType)의 결과값을 json형식으로 전송 
		final ResultActions resultActions = mockMvc.perform(get(url)
							.accept(MediaType.APPLICATION_JSON));
				
		
		// * then : 그 실행한 결과가 예상하는 데이터와 같은가 
		//  - isOk() : 데이터 응답코드가 올바른지 체크  
		//  실제 Article 테이블에 저장하는 데이터와 같은지 확인 
		//  jsonPath : resultActions에 가져온 json 데이터 리소스 
		resultActions
				.andExpect(status().isOk())
				.andExpect(jsonPath("$[0].content").value(content))
				.andExpect(jsonPath("$[0].title").value(title));
	}

 

 


5. 블로그 글 조회 API 구현하기 

 

1) 서비스 메서드 코드 작성하기 

◉ BlogService.java 파일에 블로그 글 하나를 조회하는 메서드인 findById() 메서드 추가 

 - 이 메서드는 데이터베이스에 저장되어 있는 글의 ID를 이용해 글을 조회 

 

// * 블로그 글 하나를 조회하는 메서드 
public Article findById() {
    return blogRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("not found : " + id));
    // * Optional<T>의 객체 값 가져올 때, null일 경우 지정된 예외 발생 				
}

▪︎ JPA에서 제공하는 findById() 메서드를 사용해 ID를 받아 엔티티를 조회하고, 없으면 IllegalArgumentException 예외 발생 

▪︎ Optional<T> 객체의 값 가져올 때, null일 경우 지정된 예외 발생시킴 

 

 


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

 

◉ /api/articles/{id} GET 요청이 오면 블로그 글을 조회하기 위해 매핑할 findArticle( ) 메서드 작성 

 

// * 게시물 하나 구하기 
// GET /api/articles/{id}
// GET + ... id = 게시물 하나 검색해주세요 
// CRUD에서 상세보기 기능에 해당 
@GetMapping("/api/articles/{id}")
// URI 경로에서 값 추출 
public ResponseEntity<ArticleResponse> findArticle(@PathVariable long id){
    Article article = blogService.findById(id);

    return ResponseEntity.ok()
                .body(new ArticleResponse(article));
}

▪︎ @PathVariable 애너테이션 

  - URL에서 값을 가져오는 애너테이션 

  - 동작원리

     /api/articles/3 GET 요청을 받으면, id에 3이 들어옴 

     → 이 값이 서비스 클래스의 findById( ) 메서드로 번호가 3번 블로그 글을 찾게 됨 

     → 3번 글의 정보를 body에 담아 웹 브라우저로 전송함 

 

 

 


3) 테스트 코드 작성하기 

 

▪︎ BlogApiControllerTest.java

given 블로그 글을 저장함 
when 저장한 블로그 글의 Id값으로 API를 호출함 
then 응답 코드가 200 Ok이고, 반환받은 content와 title이 저장된 값과 같은지 확인함 

 

@DisplayName("findArticle : 블로그 글 조회에 성공한다.")
@Test 
public void findArticle() throws Exception{
    // * given
    //  - 테스트로 사용할 데이터 입력 
    final String url = "/api/articles/{id}";
    final String title = "테스트 제목";
    final String content = "테스트 내용";

    //  - 테스트용 데이터를 BlogRepository의 save() 메서드를 이용해 저장
    //    -> 그 결과 Article 객체가 반환됨 
    Article savedArticle = blogRepository.save(Article.builder()
            .title(title)
            .content(content)
            .build());

    // * when
    //  - 실제 웹 서버로 실행시킨 것과 같은 결과를 반환하게 해서 
    //    ResultActions 객체에 결과정보를 저장함 
    final ResultActions resultActions 
                = mockMvc.perform(get(url, savedArticle.getId()));

    // * then
    resultActions
        .andExpect(status().isOk())   
        .andExpect(jsonPath("$.content").value(content))
        .andExpect(jsonPath("$.title").value(title));

}