본문 바로가기
3.1SpringBoot/3.1.2 묘공단 SpirngBoot3

[묘공단/spring] 6장 블로그 기획하고 API만들기 -실습편

by Dohi._. 2023. 10. 11.

포스팅 목차  (책의 목차와 다릅니다 개인적으로 공부한 내용입니다)

6장  블로그 기획하고 API만들기 -실습편

  
  6-3. Entity
  6-4. 작성을 위한 API (Create)
  6-5. 조회,삭제,수정를 위한 API (Read, Delete, Update)
  6-6. 사용한 기술 정리
 

실습파트는 제가 제작하고 있는 프로젝트 기반으로 포스팅 되며.
제작중인 프로젝트와 책의 실습과 융합하여 제작한 포스팅이며 책과 같거나 다를 수 있습니다.
조회 삭제 수정까지 작성을 하니 글이 너무 길어져서 통합했습니다:)

 

6-3. Entity

@Entity로 지정하는 Entity를 구성해 보려고 합니다.
우선 Entity와 매핑되는 테이블의 구조를 구상해봅니다.

컬럼명 자료형 Null허용 설명
id BIGINT N 기본키 기본키,ID
title VARCHAR(255) N   제목
content VARCHAR(255) N   내용
user_id 미정 미정 미정 추후개발예정

 
user_id의 경우는 아직 제작하지 않을 예정이므로 제외하여 Entity를 작성해 보겠습니다
 

@Getter // Lombok으로 getter제작 
@NoArgsConstructor(access = AccessLevel.PROTECTED) //아래의 주석처리된 기본생성자를 생성해주는 애너테이션 
@Entity //엔티티로 선언
public class Article{
	
    @Id // Id필드를 기본키로 지정하겠다.
    @GeneratedValue(strategy = GenerationType.IDENTITY) //기본키 1씩 자동 증가 
    @Column(name = "id", updatable = false) //id와 매핑,  업데이트할때 해당id는 변경불가
    private Long id;

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

    @Column(name = "content", nullable = false)
    private String content;
	
    @Builder //Lombok 빌더 패턴으로 객체 생성
    public Article(String title, String content){
    this.title = title; //this.title -> 객체의 title / title-> 생성자가 불러들어온 title
    this.content = content;
    }
    
   //protected Article(){} //기본생성자
   //@NoArgsConstructor(access = AccessLevel.PROTECTED) 는 매개변수없는 생성자를 생성해주는 LOMBOK의 애너테이션
    
    public void update(String title, String content) {
        this.title = title;
        this.content = content;
     }
    }

 
이제 퍼시스턴트계층중 Repository를 만들어 봅시다

만약 Repository가 기억이 안난다면 3장 Spring boot 3 구조한번 다시 보면 기억날 것이에요..!
+ 아래에서 DAO, DTO도 이야기할 것같은데 이것도 기억이 안난다면 참고 바랍니다!

 

public interface BlogRepository extends JpaRepository<Article, Long>{
}

해당 BlogRepository는 JpaRepository를 상속받을 때 엔티티 Article와 기본키(PK)의 타입인 Long을 인수로 넣습니다.
JpaRepository를 상속 받음으로 다양한 메소드를 사용할 수 있게 됩니다.
 
JpaRepository  메소드

  • save(), findOne(), findAll(),  findById(), existsById(), count(), delete(), deleteAll(), deleteById(), exists()
  • save(S entity)
    • 주어진 Entity를 저장하거나 업데이트(새로운 Entity면 저장, 기존 Entity면 업데이트)
  • findOne(ID primaryKey)
    • 주어진 기본 키(Primary Key)에 해당하는 Entity를 반환
  • findAll()
    • 해당 Entity 타입의 모든 Entity를 반환
  • findById(ID primaryKey)
    • 주어진 기본 키에 해당하는 Entity를 Optional로 감싸서 반환( Entity가 존재하지 않으면 빈 Optional을 반환)
  • existsById(ID primaryKey)
    • 주어진 기본 키에 해당하는 Entity가 존재하는지 여부를 확인
  • count()
    • 해당 Entity의 총 개수를 반환
  • delete(T entity)
    • 주어진 Entity를 삭제
  • deleteAll():
    • 해당 Entity의 모든 Entity를 삭제
  • deleteById(ID primaryKey)
    • 주어진 기본 키에 해당하는 Entity를 삭제
  • exists()
    • 적어도 하나의 Entity가 존재하는지 여부를 확인

 
1:N, N:M 에 대한 정보는 2-2 도메인 분석 설계 참고부탁드립니다

 

 

6-4. 작성을 위한 API

진행 순서 

  1. DTO만들기
  2. Sevice 만들기 
  3. Controller 만들기
  4. Test해보기 (Test는 작성API에서만 자세하게 다루겠습니다)

블로그의 글을 작성하면 추가하는 API를 구성해보도록 하겠습니다.

 

1) DTO

우선 DTO를 먼저 구성해볼 것인데 
DTO(data Transfer Object) 계층끼리 데이터를 교환하기 위해 사용되는 객체입니다 3장에서 한번 다뤄봤습니다.
DTO는 전달자의 역할을 하는 것이기 때문에 로직없이 전달만 담백하게 한다고 생각하면 편하겠습니다.

@NoArgsConstructor //기본생성자 추가
@AllArgsConstructor//모든 필드값을 파라미터로 받는 생성자 추가 
@Getter
public class AddArticleRequest {
    private String title;
    private String content;

    public Article toEntity() { //return값이 Arcicle인 객체를 반환
        return Article.builder()
                .title(title)
                .content(content)
                .build();
    }
}

//제가 위에서 User_id(User라고 가정)를 만들어서 만약 User의 정보를 받아야한다면?
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class AddArticleRequest {
    private String title;

    private String content;

    public Article toEntity(Long user) {
        return Article.builder()
                .title(title)
                .content(content)
                .user(user)
                .build();
    }
}
//이런식으로도 응용이 가능합니다

 

어 ?..  도히님의 다른 포스팅에서는 DTO를 안쓰셨는데요?
다른 포스팅에서는 값을 받아오는 form이라고 이름을 사용했었습니다:)

 
2)Sevice

sevice를 통해 이제 추가 메서드를 작성 해보도록 하겠습니다.

@RequiredArgsConstructor //해당 클래스의 final 또는 @NonNull로 표시된 필드를 가지고 생성자를 자동으로 생성
@Service//Service로 선언 -> Bean으로 등록
public class BlogService {

    private final BlogRepository blogRepository;
	//save : AddArticleRequest에 저장된 값을 DB의 Article에 저장
    public Article save(AddArticleRequest request) {
        return blogRepository.save(request.toEntity());
    }	
    //만약 user_ID를 제작했다면? 
	//public Article save(AddArticleRequest request, Long userId) {
    //    return blogRepository.save(request.toEntity(userId)); }
    
    }

 
3)Controller

@RequiredArgsConstructor
@RestController//HTTP Response Body에 객체 데이터를 JSON형식으로 반환하는 컨트롤러
public class BlogApiController {

    private final BlogService blogService;
//HTTP메서드가 post일 때 해당 URL와 동일한 메서드로 매핑
    @PostMapping("/api/articles")
    public ResponseEntity<Article> addArticle(@RequestBody AddArticleRequest request) {
        Article savedArticle = blogService.save(request);
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(savedArticle);
    }
}

RestController는 전에 그냥 간단하게 @Controller와 @Responebody랑 합친거라고 했었습니다.
Http응답으로 객체 데이터를 JSON으로 반환하게 됩니다.
메소드의 파라미터로 @RequestBody 어노테이션이 적용된 AddArticleRequest 객체가 있습니다. 이 객체는 HTTP 요청의 본문(body)에서 추출한 JSON 데이터를 자동으로 자바 객체로 변환해줍니다.

메소드 내부에서는 blogService.save(request)를 호출하여 새로운 Article을 저장하고,
그 결과로 반환된 savedArticle을 응답으로 클라이언트에게 전송합니다.
여기서 .body(savedArticle)은 ResponseEntity의 메소드 중 하나로, HTTP 응답의 본문에 해당하는 데이터를 설정합니다. 클라이언트는 이 데이터를 받아서 처리할 수 있습니다.

  • ResponseEntity.status(HttpStatus.CREATED)
    • ResponseEntity는 HTTP 응답을 나타내는 객체
    • HttpStatus.CREATED를 사용하여 HTTP 상태 코드 201(Created)를 설정. 즉, 새로운 리소스가 성공적으로 생성됨을 나타냄
    • 200 (ok) : 성공적으로 수행  
    • 400 (Bad Request) : 요청값이 잘못되어 실패
    • 403 (Forbidden) :  권한X   
    • 404(못찾음), 500(서버문제)등 각종있다.
  • body(savedArticle)
    • body() 메소드는 응답의 본문(body)을 설정

 
4) Test해보기 

전에 설치한 Postman 그리고 4장 스프링 부트 3와 테스트에서 배운 개념을 이제 이용합니다! 

  

   4-1) Postman을 이용한 TEST
 일단 main메소드를 실행시켜서 SpringBoot를 실행합니다. 

실행한 상태로 Postman을 켜줍니다.

 

노란색 형광: 작업공간 +로 추가하고 지울수 있습니다.

빨간색 체크 : Post,Get을 설정하고 주소를 입력합니다.

하늘색 형광 : 위에서 raw데이터 JSON으로 설정했기에 형광에 입력한 JSON으로 데이터를 보낼 준비

해당 사진처럼 입력하고 Send를 누를경우 아래처럼 확인이 가능합니다.

파란 형광 : 결과값을 JSON으로 보이게 설정 

초록 형광: 결과값

빨간 볼펜 : 서버의 결과 

 

그리고 한번 H2를 들어가서 한번 확인을 해보겠습니다.

저희가 6장의 개념편에서 H2의 인메모리를 이용하여 서버를 사용한다 했습니다.

아마 그냥 아이콘을 누르고 들어가면 아무런 변화가 안뜹니다.

 

저희는 현재 SpringBoot의 인메모리에 DB를 구축해 놓은 상황이기 때문에
웹 브라우저창에 http://localhost:8080/h2-console/를 입력 해서 들어갑니다

주소 설명을 해드리면

http://localhost:8080/로 SpringBoot의 서버안으로 들어가서 (만약 포트를 바꾸신분은 8080을 바꾸신 포트로 바꿔주세요)

스프링 부터 서버안에 내장 되어 있는 h2 데이터베이스에 접속하고 데이터를 확인할 수 있게됩니다.

Post요청에 의해 데이터가 실제로 저장된 것을 확인 할 수 있습니다:)

 

   4-2) TEST Code를 이용한 Test

Class명에 우클릭 후 Show Context Actions 혹은 Alt + Enter를 눌러줍니다.

설정 창에서는 기본 값그대로 Ok를 눌러줍니다

(간편 메소드는 직접 할예정이라 체크를 안하고 진행 하도록 하겠습니다)

 

@SpringBootTest
@AutoConfigureMockMvc // MockMVC를 자동구성 
class BlogApiControllerTest {
    //MockMVC는 가상의 HTTP요청을 제공 
    @Autowired
    protected MockMvc mockMvc;

	// Json과 java 객체변환(Jackson) 
    //직렬화: java-> 외부파일(json)  , 역직렬화: 외부파일 -> java 
    @Autowired
    protected ObjectMapper objectMapper;

	//MockMvc 설정에 사용
    @Autowired
    private WebApplicationContext context;

    @Autowired
    BlogRepository blogRepository;
	
    //테스트 실행하기전에 환경설정
    @BeforeEach //테스트 전에 실행하는 메서드 선언
    public void mockMvcSetUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .build();
        blogRepository.deleteAll();//초기화
    }

    @DisplayName("addArticle: 추가")
    @Test
    public void addArticle() throws Exception {
        // given 추가에 필요한 객체 만들기
        final String url = "/api/articles";
        final String title = "title";
        final String content = "content";
        final AddArticleRequest userRequest = new AddArticleRequest(title, content);
		
        //writeValueAsString 메서드는 Java 객체를 JSON 문자열 변환
		// 즉 userRequest의 객체를 requestBody에 JSON 대입
        final String requestBody = objectMapper.writeValueAsString(userRequest);

        // when API요청 보내고 JSON으로 보냄
        //MockMvc를 사용하여 HTTP POST 요청을 수행 
        //url은 API 엔드포인트, userRequest 객체는 요청 바디에 담겨질 데이터
        ResultActions result = mockMvc.perform(post(url)
                .contentType(MediaType.APPLICATION_JSON_VALUE)//Json으로 타입을 선언 
                .content(requestBody));

        // then 201인지 확인하고 Blog크기를 조회, 요청값과 데이터와 동일한지 확인
       	result.andExpect(status().isCreated());
        
        //데이터확인
        List<Article> articles = blogRepository.findAll();

        assertThat(articles.size()).isEqualTo(1);
        assertThat(articles.get(0).getTitle()).isEqualTo(title);
        assertThat(articles.get(0).getContent()).isEqualTo(content);
    }

}

 

해당 코드를 돌려보면

성공적으로 돌아간 것을 알 수 있습니다. 

 

이 뜻은 새로 추가한 코드가 이상없이 잘 돌아간다.

라고 생각 하면 됩니다:)

 

CRUD의 Create에 속하는 작성API였습니다

나머지 RUD를 빠르게 코드와 주석으로 실습을 통해 해보도록 하겠습니다.

6-5. 조회,삭제,수정를 위한 API

 

진행순서

  1. DTO만들기
  2. Sevice 만들기 
  3. Controller 만들기

조회에는 전체 조회(findAll)가 있고 한개를 조회(findById)가 있습니다.

삭제는 delete, 수정은 update

동시에 작성을 해보겠습니다.

 

1)DTO

우선 받아올 DTO를 제작을 할 것입니다 

1. 해당 DTO는 Entity를 인수로 받아 글의 구성을 받아옵니다.

//DTO
@Getter
public class ArticleResponse {

    private final String title;
    private final String content;

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

 

2. 해당 DTO는 수정한 제목과 내용을 받아옵니다

@AllArgsConstructor//생성자
@NoArgsConstructor//생성자
@Getter
public class UpdateArticleRequest {
    private String title;
    private String content;
}

2)Sevice

   //Article을 리스트로 모두 불러옴
   public List<Article> findAll() {
        return blogRepository.findAll();
    }
	
    // id로 Article을 불러옴  없을시 오류던짐
    public Article findById(long id) {
        return blogRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("not found : " + id));
    }

	//id로 삭제
    public void delete(long id) {
        blogRepository.deleteById(id);
    }
	
    //
    //트랜잭션 작업을 한단위로 합침 
    //*A와 B작업을 동시에 해야하는데 A는 성공 B는 실패했다면 문제발생 
    //따라서 한 단위로 합쳐서 중간에 실패시 초기화하면 문제발생X
    @Transactional
    public Article update(long id, UpdateArticleRequest request) {
        Article article = blogRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("not found : " + id));

        article.update(request.getTitle(), request.getContent());

        return article;
    }

 

3)Controller

 @GetMapping("/api/articles")
    public ResponseEntity<List<ArticleResponse>> findAllArticles() {
        List<ArticleResponse> articles = blogService.findAll()
                .stream()
                .map(ArticleResponse::new)
                .toList();

        return ResponseEntity.ok()
                .body(articles);
    }
    
    @GetMapping("/api/articles/{id}")
    public ResponseEntity<ArticleResponse> findArticle(@PathVariable long id) {
        Article article = blogService.findById(id);

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

    @DeleteMapping("/api/articles/{id}")
    public ResponseEntity<Void> deleteArticle(@PathVariable long id) {
        blogService.delete(id);

        return ResponseEntity.ok()
                .build();
    }

    @PutMapping("/api/articles/{id}")
    public ResponseEntity<Article> updateArticle(@PathVariable long id,
                                                 @RequestBody UpdateArticleRequest request) {
        Article updatedArticle = blogService.update(id, request);

        return ResponseEntity.ok()
                .body(updatedArticle);
    }

 

 

 


 

 

6-6. 사용한 기술 정리

해당 파트는 여러분들이 실습하다가 궁금하지만 넘어 갔을 것 같은 것들을 모아뒀습니다:)

천천히 한번 읽어보셨으면 좋겠습니다.

 

롬복 - 빌더패턴, Getter&Setter

@Getter
@Setter
public class ExampleLombok{
	private int age;
    private String name;
    
    @Builder
    public ExampleLombok(age,name){
    this.age = age;
    this.name = name;
    }
}

//롬복을 사용하지 않은경우 빌더 , Getter&Setter
public class ExampleClass{
	private int age;
    private String name;
    
    public ExampleClass(age,name){ 
    this.age = age;
    this.name = name;
    }
    
    //Getter Setter만들기 
    public int getAge(){
    return age;
    }
    public void setAge(age){
    this.age = age;
    }
    public int setName(){
    return name;
    }
    public void setAge(name){
    this.name = name;
    }
 }
 
 public class main{
 
 	public static void main(String[] arg){
    	//빌더 패턴
        ExampleLombok EL = ExampleLombok.builder()
                                        .age(99)
                                        .name("A군")
                                        .build();
        //빌더 패턴X                            
     	ExampleClass EC = new ExampleClass(99,"A군");
    }

 
@RequiredArgsConstructor

@RequiredArgsConstructor
public class ExampleClass {
    
    private final String field1;
    private final int field2;
    private String field3;

    // @RequiredArgsConstructor로인해 아래 생성자가 자동으로 생성됨
//public ExampleClass(String field1, int field2) {
//    this.field1 = field1;
//    this.field2 = field2;
//}

}

 
throws Exception

자바에서는 메서드에서 발생한 예외를 던지려면 메서드 선언부에 throws 구문을 사용해야 합니다.

이를 통해 해당 메서드가 어떤 예외를 던질 수 있는지 명시하게 됩니다.

예를 들어, 다음과 같은 메서드에서는 IOException이 발생할 수 있음을 명시하고 있습니다

public void readFile() throws IOException {
    // 파일을 읽는 동작
    // ...
    // 파일을 읽다가 IOException이 발생할 수 있음
}
public void processFile() {
    try {
        readFile(); // readFile 메서드에서 던진 예외를 여기서 처리
    } catch (IOException e) {
        // IOException을 처리하는 코드
    }
}

이 글은 골든래빗 《스프링 부트 3 백엔드 개발자 되기 - 자바 편》의 6장 써머리입니다.

 

728x90

댓글