Day 4

Day4

๋“ฑ๋ก/์ˆ˜์ •/์กฐํšŒ API ๋งŒ๋“ค๊ธฐ

๐Ÿ‘‰API๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์ด 3๊ฐœ์˜ ํด๋ž˜์Šค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค

  • Request ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์€ DTO

  • API ์š”์ฒญ์„ ๋ฐ›์„ Controller

  • ํŠธ๋žœ์žญ์…˜, ๋„๋ฉ”์ธ ๊ธฐ๋Šฅ ๊ฐ„์˜ ์ˆœ์„œ๋ฅผ ๋ณด์žฅํ•˜๋Š” Service

โœ…service๋Š” ํŠธ๋žœ์žญ์…˜, ๋„๋ฉ”์ธ ๊ฐ„์˜ ์ˆœ์„œ๋ฅผ ๋ณด์žฅ

Web Layer, Service Layer, Repository Layer, Dtos, Domain Model

  • Web Layer

    • ํ”ํžˆ ์‚ฌ์šฉํ•˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ์™€ JSP/Freemarker ๋“ฑ์˜ ๋ทฐ ํ…œํ”Œ๋ฆฟ ์˜์—ญ

    • ์ด์™ธ์—๋„ @Filter, ์ธํ„ฐ์…‰ํ„ฐ, ์ปจํŠธ๋กค๋Ÿฌ ์–ด๋“œ๋ฐ”์ด์Šค(@ControllerAdvice)๋“ฑ ์™ธ๋ถ€ ์š”์ฒญ๊ณผ ์‘๋‹ต์— ๋Œ€ํ•œ ์ „๋ฐ˜์ ์ธ ์˜์—ญ์„ ์ด์•ผ๊ธฐํ•ฉ๋‹ˆ๋‹ค

  • Service Layer

    • @Service์— ์‚ฌ์šฉ๋˜๋Š” ์„œ๋น„์Šค ์˜์—ญ

    • ์ผ๋ฐ˜์ ์œผ๋กœ Controller์™€ Dao์˜ ์ค‘๊ฐ„ ์˜์—ญ์—์„œ ์‚ฌ์šฉ

    • @Transactional์ด ์‚ฌ์šฉ๋˜์–ด์•ผ ํ•˜๋Š” ์˜์—ญ

  • Repository Layer

    • db์™€ ๊ฐ™์ด ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ์— ์ ‘๊ทผํ•˜๋Š” ์˜์—ญ

    • ๊ธฐ์กด์˜ DAO๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋จ

  • Dtos

    • Dto(Data Transfer Object)๋Š” ๊ณ„์ธต ๊ฐ„์— ๋ฐ์ดํ„ฐ ๊ตํ™˜์„ ์œ„ํ•œ ๊ฐ์ฒด๋ฅผ ์ด์•ผ๊ธฐํ•˜๋ฉฐ

  • Domain Model

    • ๋„๋ฉ”์ธ์ด๋ผ๊ณ  ๋ถˆ๋ฆฌ๋Š” ๊ฐœ๋ฐœ ๋Œ€์ƒ์„ ๋ชจ๋“  ์‚ฌ๋žŒ์ด ๋™์ผํ•œ ๊ด€์ ์—์„œ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๊ณ  ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋‹จ์ˆœํ™” ์‹œํ‚จ ๊ฒƒ

    • @Entity๊ฐ€ ์‚ฌ์šฉ๋œ ์˜์—ญ์€ ๋„๋ฉ”์ธ ๋ชจ๋ธ

    • ๋ฌด์กฐ๊ฑด db์˜ ํ…Œ์ด๋ธ”๊ณผ ๊ด€๊ณ„๊ฐ€ ์žˆ์–ด์•ผ๋งŒ ํ•˜๋Š” ๊ฒƒ์€ ์•„๋‹˜

โœ…web, service, repository, dto, domain์ด 5๊ฐ€์ง€์˜ ๋ ˆ์ด์–ด์—์„œ ๋น„์ง€๋‹ˆ์Šค๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ณณ์€ Domain

โœ…์„œ๋น„์Šค๋Š” ํŠธ๋žœ์žญ์…˜๊ณผ ๋„๋ฉ”์ธ ๊ฐ„์˜ ์ˆœ์„œ๋งŒ ๋ณด์žฅ

๐Ÿ‘‰์ฝ”๋“œ ์ž‘์„ฑ

/src/main/java/com/kyu/book/springboot/service/posts/PostsService.class

package com.kyu.book.springboot.service.posts;

import com.kyu.book.springboot.domain.posts.PostsRepository;
import com.kyu.book.springboot.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;

@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    @Transactional
    public Long save(PostsSaveRequestDto requestDto){
        return postsRepository.save(requestDto.toEntity()).getId();
    }
}

์Šคํ”„๋ง์—์„œ Bean์„ ์ฃผ์ž…๋ฐ›๋Š” ๋ฐฉ์‹

  • @Autowired

  • setter

  • ์ƒ์„ฑ์ž

โœ… ๊ฐ€์žฅ ๊ถŒ์žฅํ•˜๋Š” ๋ฐฉ์‹ : ์ƒ์„ฑ์ž โ‡’ @RequiredArgsConstructor๋กœ ํ•ด๊ฒฐ

@RequiredArgsConstructor๋Š” final์ด ์„ ์–ธ๋œ ๋ชจ๋“  ํ•„๋“œ๋ฅผ ์ธ์ž๊ฐ’์œผ๋กœ ์ƒ์„ฑํ•˜๋Š” ์ƒ์„ฑ์ž

Controller์™€ Service์—์„œ ์‚ฌ์šฉํ•  Dtoํด๋ž˜์Šค

src/main/java/com/kyu/book/springboot/web/dto/PostsSaveRequestDto.java

package com.kyu.book.springboot.web.dto;

import com.kyu.book.springboot.domain.posts.Posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@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ํด๋ž˜์Šค๋Š” db์™€ ๋งž๋‹ฟ์€ ํ•ต์‹ฌ ํด๋ž˜์Šค, Entityํด๋ž˜์Šค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํ…Œ์ด๋ธ”์ด ์ƒ์„ฑ๋˜๊ณ , ์Šคํ‚ค๋งˆ๊ฐ€ ๋ณ€๊ฒฝ๋จ

โœ… View Layer์™€ DB Layer์˜ ์—ญํ•  ๋ถ„๋ฆฌ๋ฅผ ์ฒ ์ €ํ•˜๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Œ : Controller์—์„œ ๊ฒฐ๊ณผ๊ฐ’์œผ๋กœ ์—ฌ๋Ÿฌ ํ…Œ์ด๋ธ”์„ ์กฐ์ธํ•ด์„œ ์ค˜์•ผํ•  ๊ฒฝ์šฐ๊ฐ€ ๋นˆ๋ฒˆํ•˜๊ธฐ ๋–„๋ฌธ์—

โ‡’Entity ํด๋ž˜์Šค์™€ Controller์—์„œ ์“ธ Dto๋Š” ๋ถ„๋ฆฌํ•ด์„œ ์‚ฌ์šฉํ•˜๋ผ

Entityํด๋ž˜์Šค๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ

src/test/java/com/kyu/book/springboot/web/PostsApiControllerTest.java

package com.kyu.book.springboot.web;

import com.kyu.book.springboot.domain.posts.Posts;
import com.kyu.book.springboot.domain.posts.PostsRepository;
import com.kyu.book.springboot.web.dto.PostsSaveRequestDto;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest {
    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    @After
    public void tearDown() throws Exception{
        postsRepository.deleteAll();
    }

    @Test
    public void postEnroll() throws Exception{
        String title = "title";
        String content = "content";
        PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder().title(title).content(content).author("author").build();

        String url = "http://localhost:" + port + "/api/v1/posts";

        ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);

        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<Posts> all = postsRepository.findAll();
        assertThat(all.get(0).getTitle()).isEqualTo(title);
        assertThat(all.get(0).getContent()).isEqualTo(content);
    }
}

์—ฌ๊ธฐ์„œ๋Š” @WebMvcTest๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์€ ์ด์œ  : ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด JPA๊ธฐ๋Šฅ์ด ์ž‘๋™ํ•˜์ง€ ์•Š์Œ

JPA๊ธฐ๋Šฅ๊นŒ์ง€ ํ•œ ๋ฒˆ์— ํ…Œ์ŠคํŠธํ•˜๊ณ  ์‹ถ์„ ๋•Œ๋Š” @SpringBootTest์™€ @TestRestTemplate์„ ์‚ฌ์šฉํ•˜๋ผ

์ˆ˜์ • / ์กฐํšŒ๊ธฐ๋Šฅ

src/main/java/com/kyu/book/springboot/web/PostsApiController.java

package com.kyu.book.springboot.web;

import com.kyu.book.springboot.service.posts.PostsService;
import com.kyu.book.springboot.web.dto.PostsResponseDto;
import com.kyu.book.springboot.web.dto.PostsSaveRequestDto;
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);
    }
}

src/main/java/com/kyu/book/springboot/web/dto/PostsResponseDto.class

package com.kyu.book.springboot.web.dto;

import com.kyu.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();
    }
}

src/main/java/com/kyu/book/springboot/web/PostsUpdateRequestDto.class

package com.kyu.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;
    }
}

src/main/java/com/kyu/book/springboot/domain/posts/Posts ์— ์ด๊ฑฐ ์ถ”๊ฐ€

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

src/main/java/com/kyu/book/springboot/service/posts/PostsService ์— ์ด๊ฑฐ ์ถ”๊ฐ€

@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);
}

update ๊ธฐ๋Šฅ์—์„œ db์— ์ฟผ๋ฆฌ๋ฌธ์„ ์•ˆ๋‚ ๋ฆผ โ‡’ JPA์˜ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ ๋•Œ๋ฌธ์— ํ•„์š” ์—†์Œ

์˜์†์„ฑ ์ปจํ…์ŠคํŠธ : ์—”ํ‹ฐํ‹ฐ๋ฅผ ์˜๊ตฌ ์ €์žฅํ•˜๋Š” ํ™˜๊ฒฝ

ใ„ดJPA์˜ ํ•ต์‹ฌ ๋‚ด์šฉ์€ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์— ํฌํ•จ๋˜์–ด ์žˆ๋ƒ ์•„๋‹ˆ๋ƒ๋กœ ๊ฐˆ๋ฆผ

JPA์˜ ์—”ํ‹ฐํ‹ฐ ๋งค๋‹ˆ์ €๊ฐ€ ํ™œ์„ฑํ™”๋œ ์ƒํƒœ(JPA์‚ฌ์šฉ์‹œ ๊ธฐ๋ณธ ์˜ต์…˜) ์‹œ, ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋ฉด ์ด ๋ฐ์ดํ„ฐ๋Š” ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๊ฐ€ ์œ ์ง€๋œ ์ƒํƒœ์ด๋‹ค

์ด ์ƒํƒœ์—์„œ ํ•ด๋‹น ๋ฐ์ดํ„ฐ์˜ ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๋ฉด ํŠธ๋žœ์žญ์…˜์ด ๋๋‚ ๋•Œ ํ…Œ์ด๋ธ”์— ๋ณ€๊ฒฝ๋ฌธ์„ ๋ฐ˜์˜

์ฆ‰, Entity ๊ฐ์ฒด์˜ ๊ฐ’๋งŒ ๋ณ€๊ฒฝํ•˜๋ฉด ๋ณ„๋„๋กœ Update์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ฆด ํ•„์š”๊ฐ€ ์—†๋‹ค = ๋”ํ‹ฐ ์ฒดํ‚น

update๋ฅผ ์ถ”๊ฐ€ํ–ˆ์œผ๋‹ˆ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑ

src/test/java/com/kyu/book/springboot/web/PostsApiControllerTest ์— ์ถ”๊ฐ€

@Test
public void postUpdates() throws Exception{
    Posts savedPosts = postsRepository.save(Posts.builder().title("title").content("content").author("author").build());

    Long updateId = savedPosts.getId();
    String expectedTitle = "title2";
    String expectedContent = "content2";

    PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder().title(expectedTitle).content(expectedContent).build();

    String url = "http://localhost:" + port + "/api/v1/posts/" + updateId;

    HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);

    ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, Long.class);

    assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
    assertThat(responseEntity.getBody()).isGreaterThan(0L);

    List<Posts> all = postsRepository.findAll();
    assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
    assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
    
}

local์—์„œ๋Š” db๋ฅผ h2๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ๋ฉ”๋ชจ๋ฆฌ์—์„œ ์‹คํ–‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ง์ ‘ ์ ‘๊ทผํ•˜๋ ค๋ฉด ์›น ์ฝ˜์†”์„ ์‚ฌ์šฉํ•ด์•ผํ•จ

  1. application-properties์— ๋‹ค์Œ์„ ์ถ”๊ฐ€

    spring.h2.console.enabled=true
  2. main์„ ์‹คํ–‰ํ•œ ํ›„, http://localhost:8080/h2-console ๋กœ ์ ‘์†

  3. JDBC URL ๋ถ€๋ถ„์„ jdbc:h2:mem:testdb ๋กœ ๋ณ€๊ฒฝํ›„, connect๋ฒ„ํŠผ ํด๋ฆญ

  4. insert into posts(author, content, title) values ('author', 'content', 'title'); ์„ ์ง‘์–ด ๋„ฃ๊ณ  run

  5. ์šฐ๋ฆฌ๊ฐ€ ์ƒ์„ฑํ•œ http://localhost:8080/api/v1/posts/1 ๋กœ ๋“ค์–ด๊ฐ€๋ฉด insertํ•ด๋‘” posts๋“ค์ด ๋‚˜์˜ค๊ฒŒ ๋จ

Last updated

Was this helpful?