- 단위 테스트 : 애플리케이션의 개별 모듈을 독립적으로 테스트
- 통합 테스트 : 애플리케이션을 구성하는 다양한 모듈을 결합해 전체적인 로직이 의도하는 대로 동작 하는지 테스트 하는 방식
(1) 단위 테스트 특징
단위테스트는 메서드 단위로 테스트를 수행하게 되며, 메서드 호출을 통해 의도한 값이 나오는지 확인합니다.
(2) 통합 테스트 특징
통합 테스트는 여러 모듈을 함께 테스트해서 정상적인 로직 수행이 가능한지 확인합니다.
단위 테스트는 DB나 Network같은 외부 요인을 제거하고 진행하는 데 반해 통합 테스트는 외부 요인들을 포함하고 테스트를 진행하므로 애플리케이션이 온전히 동작하는지 테스트하게 됩니다.
테스트 비용이 크다는 단점이 있습니다.
(1) Given-when-Then 패턴
- Given : 테스트를 수행하기 전에 필요한 환경을 설정하는 단계이고, 테스트에 필요한 변수 정의, Mock객체를 통해 특정 상황에 대한 행동을 정의합니다.
- When : 테스트의 목적을 보여주는 단계, 실제 테스트 코드 포함되며, 테스트를 통한 결과값을 가져옵니다.
- Then : 테스트의 결과를 검증하는 단계, When 단계에서나온 결과값 검증합니다. 결과값이 아니더라도 테스트를 통해 나온 결과를 검증해야 하는 부분 또한 포함합니다.
- BDD(Behavior-Driven-Development, 행위주도개발)를 통해 탄생한 테스트 접근 방식
(2) 좋은 테스트 코드의 5가지 속성(F,I,R,S,T)
보통 단위테스트에 적용할 수 있는 규칙
자바에서 사용되는 대표적인 테스트 프레임워크(단웨 테스트 및 통합 테스트 위한 도구 제공)입니다.
어노테이션 기반의 테스트 방식을 지원합니다.
단정문(Assert)를 통해 테스트 케이스의 기대값이 정상적으로 도출되었는지 검토할 수 있다는 장점이 있습니다.
(1) JUnit의 세부 모듈
Jupiter, Platfrom, Vintage
테스트 엔진은 테스트를 발견하고 테스트를 수행하여, 결과를 보고하는 역할입니다.
Junit은 하나의 Platform모듈을 기반으로 Jupiter, Vintage 모듈이 구현체 역할을 수행합니다.
(2) 스프링 부트 프로젝트 생성
DAO 레이어 제외하고 서비스 레이어에서 바로 리포지토리를 사용하는 구조로 진행하겠습니다.
1. ProductServiceImpl 클래스 수정
@Service
public class ProductServiceImpl implements ProductService {
private final Logger LOGGER= LoggerFactory.getLogger(ProductServiceImpl.class);
private final ProductRepository productRepository;
@Autowired
public ProductServiceImpl(ProductRepository productRepository){
this.productRepository=productRepository;
}
@Override
public ProductResponseDto getProduct(Long number) {
LOGGER.info("[getProduct] input number : {}", number);
Product product = productRepository.findById(number).get();
LOGGER.info("[getProduct] product number : {}, name : {}", product.getNumber(), product.getName());
ProductResponseDto productResponseDto = new ProductResponseDto();
productResponseDto.setNumber(product.getNumber());
productResponseDto.setName(product.getName());
productResponseDto.setPrice(product.getPrice());
productResponseDto.setStock(product.getStock());
return productResponseDto;
}
@Override
public ProductResponseDto saveProduct(ProductDto productDto) {
LOGGER.info("[getProduct] productDTO : {}", productDto.toString());
Product product = new Product();
product.setName(productDto.getName());
product.setPrice(productDto.getPrice());
product.setStock(productDto.getStock());
Product saveProduct = productRepository.save(product);
LOGGER.info("[saveProduct] saveProduct : {}", saveProduct);
ProductResponseDto productResponseDto = new ProductResponseDto();
productResponseDto.setNumber(saveProduct.getNumber());
productResponseDto.setName(saveProduct.getName());
productResponseDto.setPrice(saveProduct.getPrice());
productResponseDto.setStock(saveProduct.getStock());
return productResponseDto;
}
@Override
public ProductResponseDto changeProductName(Long number, String name) throws Exception {
Product foundProduct = productRepository.findById(number).get();
foundProduct.setName(name);
Product changeProduct = productRepository.save(foundProduct);
ProductResponseDto productResponseDto = new ProductResponseDto();
productResponseDto.setNumber(changeProduct.getNumber());
productResponseDto.setName(changeProduct.getName());
productResponseDto.setPrice(changeProduct.getPrice());
productResponseDto.setStock(changeProduct.getStock());
return productResponseDto;
}
@Override
public void deleteProduct(Long number) throws Exception {
productRepository.deleteById(number);
}
}
2. Product 엔터티 클래스 수정
@Entity
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString(exclude = "name")
@Table(name = "product")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long number;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer price;
@Column(nullable = false)
private Integer stock;
private LocalDateTime createdAt;
private LocalDateTime updateAt;
}
(3) 스프링 부트의 테스트 설정
- spring-boot-starter-test 프로젝트를 pom.xml 파일에 관련 의존성을 추가해 줍니다.
< 스프링 부트에서 제공하는 spring-boot-starter-test 라이브러리 >
(4) JUnit의 생명주기
< 관련 어노테이션>
컨트롤러는 클라이언트로 부터 요청을 받아 요청에 걸맞은 서비스 컴포넌트로 요청을 전달하고 그 결과값을 가공해서 클라이언트에게 응답을 하는 역할입니다.
@RestController
@RequestMapping("/product")
public class ProductCotroller {
private final ProductService productService;
@Autowired
public ProductCotroller(ProductService productService){
this.productService=productService;
}
@GetMapping()
public ResponseEntity<ProductResponseDto> getProduct (Long number){
ProductResponseDto productResponseDto = productService.getProduct(number);
return ResponseEntity.status(HttpStatus.OK).body(productResponseDto);
}
}
- ProductController는 ProductService의 객체를 의존성 주입을 받습니다.
- 테스트하는 입장에서 ProductController만 테스트 하고 싶다면 ProductService는 외부 요인에 해당합니다.
-> 독립적인 테스트 코드 작성을 위해서는 Mock 객체를 활용 해야 합니다.
2. getProduct() 메서드에 대한 테스트 코드 작성하기
@WebMvcTest(ProductCotroller.class)
public class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
ProductServiceImpl productService;
@Test
@DisplayName("MockMvc를 통한 Product 데이터 가져오기 테스트")
void getProductTest() throws Exception {
given(productService.getProduct(123L)).willReturn(
new ProductResponseDto(123L, "pen", 5000, 2000)
);
String productId = "123";
mockMvc.perform(
get("/product?number=" + productId))
.andExpect(status().isOk())
.andExpect(jsonPath(
"$.number").exists())
.andExpect(jsonPath("$.name").exists())
.andExpect(jsonPath("$.price").exists())
.andExpect(jsonPath("$.stock").exists())
.andDo(print());
verify(productService).getProduct(123L);
}
}
@WebMvcTest 어노테이션을 사용하는 테스트는 슬라이스 테스트라 부릅니다.
슬라이스 테스트: 단위 테스트와 통합 테스트의 중간 개념으로 , 레이어드 아키텍처를 기준으로 각 레이어별로 나누어 테스트 진행하는 의미입니다.
단위 테스트를 수행하기 위해서는 모든 외부 요인을 차단하고 테스트를 진행해야 하지만 컨트롤러는 개념상 웹과 맞닿은 레이어로서 외부 요인을 차단하고 테스트하면 의미가 없기에 슬라이스 테스트로 많이 진행합니다.
<코드적용>
3. ProductController의 createProduct() 메서드 작성하기
@PostMapping()
public ResponseEntity<ProductResponseDto> createProduct (@RequestBody ProductDto productDto){
ProductResponseDto productResponseDto = productService.saveProduct(productDto);
return ResponseEntity.status(HttpStatus.OK).body(productResponseDto);
}
@Test
@DisplayName("Product 데이터 생성 테스트")
void createProductTest() throws Exception {
//Mock객체에서 특정 메서드가 실행되는 경우 실제 return을 줄 수 없기 때문에 아래와 같이 가정 사항을 만들어줌
given(productService.saveProduct(new ProductDto("pen", 5000, 2000))).willReturn(
new ProductResponseDto(12315L, "pen", 5000, 2000));
ProductDto productDto = ProductDto.builder()
.name("pen")
.price(5000)
.stock(2000)
.build();
Gson gson = new Gson();
String content = gson.toJson(productDto);
//Mock객체에서 특정 메서드가 실행되는 경우 실제 return을 줄 수 없기 때문에 아래와 같이 가정 사항을 만들어줌
given(productService.saveProduct(new ProductDto("pen", 5000, 2000))).willReturn(
new ProductResponseDto(12315L, "pen", 5000, 2000));
ProductDto productDto = ProductDto.builder()
.name("pen")
.price(5000)
.stock(2000)
.build();
Gson gson = new Gson();
String content = gson.toJson(productDto);
mockMvc.perform(
post("/product")
.content(content)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath(
"$.number").exists())
.andExpect(jsonPath("$.name").exists())
.andExpect(jsonPath("$.price").exists())
.andExpect(jsonPath("$.stock").exists())
.andDo(print());
verify(productService).saveProduct(new ProductDto("pen", 5000, 2000));
}
}
- ProductDto
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ProductDto {
private String name;
private int price;
private int stock;
}
Gson은 구글에서 개발한 JSON 파싱 라이브러리로서 자바 객체를 JSON 문자열로 변환하거나 JSON 문자열을 자바 객체로 변환하는 역할을 합니다.
(7) 서비스 객체의 테스트
<아무 의존성을 주입받지 않은 상태에서 단위 테스트를 작성하였을 때>
public class ProductServiceTest {
private ProductRepository productRepository=Mockito.mock(ProductRepository.class);
private ProductServiceImpl productService;
@BeforeEach
public void setUpTest(){
productService = new ProductServiceImpl(productRepository);
}
@Test
void getProductTest(){
Product givenProduct = new Product();
givenProduct.setNumber(123L);
givenProduct.setName("펜");
givenProduct.setPrice(1000);
givenProduct.setStock(1234);
Mockito.when(productRepository.findById(123L))
.thenReturn(Optional.of(givenProduct));
ProductResponseDto productResponseDto = productService.getProduct(123L);
Assertions.assertEquals(productResponseDto.getNumber(), givenProduct.getNumber());
Assertions.assertEquals(productResponseDto.getName(), givenProduct.getName());
Assertions.assertEquals(productResponseDto.getPrice(), givenProduct.getPrice());
Assertions.assertEquals(productResponseDto.getStock(), givenProduct.getStock());
verify(productRepository).findById(123L);
}
@Test
void saveServiceTest(){
Mockito.when(productRepository.save(any(Product.class)))
.then(returnsFirstArg());
ProductResponseDto productResponseDto = productService.saveProduct(
new ProductDto("펜",1000,1234)
);
Assertions.assertEquals(productResponseDto.getName(), "펜");
Assertions.assertEquals(productResponseDto.getPrice(), 1000);
Assertions.assertEquals(productResponseDto.getStock(), 1234);
verify(productRepository).save(any());
}
}
< getProductTest에 대한 설명>
<saveProduct()에 대한 설명>
< Mock객체를 직접 생성하지 않고 @MockBean 어노테이션을 사용해 스프링 컨테이너에 Mock객체를 주입받는 방법>
@ExtendWith(SpringExtension.class)
@Import({ProductServiceImpl.class})
class ProductServiceTest2 {
@MockBean
ProductRepository productRepository;
@Autowired
ProductService productService;
...
}
이 전에는 Mockito를 통해 리포지토리를 Mock객체로 대체하는 작업을 수행하고 서비스 객체를 직접 초기화하였습니다.
반면 위에서는 테스트 어노테이션을 통해 Mock 객체를 생성하고 의존성을 주입받고 있습니다.
(8) 리포지토리 객체의 테스트
리포지토리는 개발자가 구현하는 레이어 중 가장 DB와 가깝습니다.
JpaRepository를 상속받아 기본적인 쿼리 메서드 사용할 수 있어, 리포지토리 테스트는 구현 목적에 대해 특별히 고민하고 작성해야 합니다.
findById(), save()와 같은 기본 메서드에 대한 테스트는 이미 검증을 마치고 제공되는 것이므로 별 의미 없습니다.
DB는 외부 요인인데 만약 단위 테스트를 고려한다면 제외하기도 하고 테스트 용으로 다른 DB를 사용하기도 합니다.
-> DB를 사용하는 테스트는 테스트 과정에서 DB에 테스트 데이터가 적재되기 때문입니다.
그래서 DB를 연동한 테스트는 테스트 데이터를 제거하는 코드까지 포함해서 작성하는 것이 좋습니다.
하지만 코드가 실행되면서 발생하는 사이드 이펙트 까지 고려시 연동없이 테스트 하는 것이 좋을수도 있습니다.
우선 DB를 제외한 테스트 상황을 가정해 테스트 데이터베이스로 H2 DB 사용했습니다.
보통 테스트 환경에서는 별도의 설정이 없다면 임베디드 DB 사용합니다.
1. DB에 대한 의존성 추가
H2를 사용하기 위해 pom.xml에 의존성을 추가합니다.
2. 데이터베이스 테스트 작성
<H2를 이용한 테스트 작성>
@DataJpaTest
public class ProductRepositoryTestByH2 {
@Autowired
private ProductRepository productRepository;
@Test
void saveTest(){
//given
Product product = new Product();
product.setName("펜");
product.setPrice(1000);
product.setStock(1000);
//when
Product saveProduct = productRepository.save(product);
//then
assertEquals(product.getName(), saveProduct.getName());
assertEquals(product.getPrice(), saveProduct.getPrice());
assertEquals(product.getStock(), saveProduct.getStock());
}
@Test
void selectTest(){
//given
Product product = new Product();
product.setName("펜");
product.setPrice(1000);
product.setStock(1000);
Product saveProduct = productRepository.saveAndFlush(product);
//when
Product foundProduct = productRepository.findById(saveProduct.getNumber()).get();
//then
assertEquals(product.getName(), foundProduct.getName());
assertEquals(product.getPrice(), foundProduct.getPrice());
assertEquals(product.getStock(), foundProduct.getStock());
}
}
<@DataJpaTest의 기능>
<저장 테스트>
1) Given구문: 테스트에서 사용할 Product를 만듭니다.
2) When구문: 생성된 엔터티를 기반으로 save()메서드를 호출해서 테스트 진행합니다.
3) Then구문: assertEquals()를 통해 save()메서드의 리턴 객체와 Given에서 생성한 엔터티 객체의 값이 일치하는지 검증합니다.
<조회테스트>
1) Given구문: 테스트에서 사용할 Product를 만듭니다.
2) When구문: 생성된 엔터티를 기반으로 save()메서드를 호출해서 테스트 진행합니다.
3) Then구문: assertEquals()를 통해 save()메서드의 리턴 객체와 Given에서 생성한 엔터티 객체의 값이 일치하는지 검증합니다.
< 기존의 마리아DB를 이용한 테스트 작성>
@DataJpaTest
@AutoConfigureTestDatabase(replace= AutoConfigureTestDatabase.Replace.NONE)
public class ProductRepositoryTest {
@Autowired
private ProductRepository productRepository;
@Test
void save(){
Product product = new Product();
product.setName("펜");
product.setPrice(1000);
product.setStock(1000);
Product savedProduct = productRepository.save(product);
assertEquals(product.getName(), savedProduct.getName());
assertEquals(product.getPrice(), savedProduct.getPrice());
assertEquals(product.getStock(), savedProduct.getStock());
}
}
< @DataJpaTest를 사용하지 않고 @SpringBootTest 어노테이션으로 테스트 작성>
@SpringBootTest
public class ProductRepositoryTest2 {
@Autowired
ProductRepository productRepository;
@Test
public void basicCRUDTest() {
/* create */
// given
Product givenProduct = Product.builder()
.name("노트")
.price(1000)
.stock(500)
.build();
// when
Product savedProduct = productRepository.save(givenProduct);
// then
Assertions.assertThat(savedProduct.getNumber())
.isEqualTo(givenProduct.getNumber());
Assertions.assertThat(savedProduct.getName())
.isEqualTo(givenProduct.getName());
Assertions.assertThat(savedProduct.getPrice())
.isEqualTo(givenProduct.getPrice());
Assertions.assertThat(savedProduct.getStock())
.isEqualTo(givenProduct.getStock());
/* read */
// when
Product selectedProduct = productRepository.findById(savedProduct.getNumber())
.orElseThrow(RuntimeException::new);
// then
Assertions.assertThat(selectedProduct.getNumber())
.isEqualTo(givenProduct.getNumber());
Assertions.assertThat(selectedProduct.getName())
.isEqualTo(givenProduct.getName());
Assertions.assertThat(selectedProduct.getPrice())
.isEqualTo(givenProduct.getPrice());
Assertions.assertThat(selectedProduct.getStock())
.isEqualTo(givenProduct.getStock());
/* update */
// when
Product foundProduct = productRepository.findById(selectedProduct.getNumber())
.orElseThrow(RuntimeException::new);
foundProduct.setName("장난감");
Product updatedProduct = productRepository.save(foundProduct);
// then
assertEquals(updatedProduct.getName(), "장난감");
/* delete */
// when
productRepository.delete(updatedProduct);
// then
assertFalse(productRepository.findById(selectedProduct.getNumber()).isPresent());
}
}
(1) JaCoCo 플러그인 설정
1. JaCoCo의 플러그인 설정을 위해 의존성을 pom.xml에 추가합니다.
2. pom.xml에 JaCoCo플러그인을 추가합니다.
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<!-- 예제 7.20 -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<!-- 예제 7.21 -->
<configuration>
<excludes>
<exclude>**/ProductServiceImpl.class</exclude>
</excludes>
</configuration>
<executions>
<!-- 예제 7.22 -->
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>jacoco-report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>jacoco-check</id>
<goals>
<goal>check</goal>
</goals>
<!-- 예제 7.23 -->
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
<element>METHOD</element>
<limits>
<limit>
<counter>LINE</counter>
<value>TOTALCOUNT</value>
<maximum>50</maximum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
Configuration 태그의 설정은 일부 클래스를 커버리지 측정 대상에서 제외하는 것입니다.
경로와 무관하게 ProductServiceImpl.class를 커버리지 측정 대상에서 제외하였습니다.
< excution>
기본적으로 < goal > 을 포함하며, 설정한 값에 따라 추가 설정이 필요한 내용을 < configuration > 과 < rule > 을 통해 작성합니다.
< goal > 속성 값
Rule 설정
< configuration > 태그 안에 설정
Element는 코드 커버리지를 체크하는데 필요한 범위 기준 설정
BUNDLE은 Element를 기준으로 < limit> 태그 내 < counter> 과 < value > 를 활용해 커버리지 측정 단위와 방식 설정
Counter는 커버리지를 측정하는 데 사용하는 지표
Value 태그로는 커버리지 지표 설정
<코드 적용>
1) 패키지 번들 단위로 바이트코드 명령 수를 기준으로 커버리자가 최소한 80프로 달성하는것을 Limit으로 설정합니다.
2) 메서드 단위로 전체 라인 수를 최대 50라인줄로 설정하였습니다.
-> 기준들을 벗어날 시 에러가 발생합니다.
(2) JaCoCo 테스트 커버리지 확인
JaCoCo plugin으로 테스트 커버리지를 측정하려면 메이븐의 테스트 단계가 선행되어야 합니다.
메이븐 생명 주기에서 JaCoCo는 test 단계 뒤에 있는 package 단계에서 실행 가능합니다.
package를 더블 클릭해서 빌드를 진행하면 타겟 폴더에 폴더가 생성됩니다.
TDD(Test-Driven Development)란 반복 테스트를 이용한 소프트웨어 개발 방법론으로서 테스트 코드를 먼저 작성한 후 테스트를 통과하는 코드를 작성하는 과정을 반복하는 소프트웨어 방식입니다.
에자일 방법론 중 하나인 익스트림 프로그래밍의 Test-First 개념에 기반을 둔, 개발 주기가 짧은 개발 프로세스로 단순한 설계를 중시합니다.
에자일: 신속한 개발 프로세스를 통해 수시로 변하는 고객의 요구사항에 대응해서 제공하는 서비스의 가치 극대화
(1) 테스트 주도 개발의 주기
(2) 테스트 주도 개발의 효과
1. 애플리케이션을 구성하는 다양한 모듈을 결합해 전체적인 로직이 의도하는 대로 동작 하는지 테스트 하는 방식은?
통합테스트
2. given-when-then 패턴에서 테스트의 목적을 보여주는 단계, 실제 테스트 코드 포함되며, 테스트를 통한 결과값을 가져오는 부분은? when
3. 테스트 엔진 API의 구현체 포함하고 Jupiter 기반 테스트를 실행하기 위한 테스트 엔진을 가지는 JUnit 모듈은? JUnit Jupiter
4. Junit은 하나의 Platform모듈을 기반으로 Jupiter, Vintage 모듈이 구현체 역할을 수행합니다. O
5. JUnit 생명주기에서 관련 어노테이션 중 테스트 코드를 포함한 메서드 정의하는 어노테이션은? @Test
6. 테스트하는 입장에서 ProductController만 테스트 하고 싶다면 ProductService는 외부 요인에 해당합니다. 독립적인 테스트 코드 작성을 위해서는 어떤 객체를 활용 해야 할까요? Mock 객체
7. 단위 테스트와 통합 테스트의 중간 개념으로 , 레이어드 아키텍처를 기준으로 각 레이어별로 나누어 테스트 진행하는 의미를 가지는 테스트는? 슬라이스 테스트
8. @MockBean 어노테이션을 사용해 스프링 컨테이너에 Mock객체를 주입받는 방법 으로 테스트를 작성했습니다. [ .. ]에 들어갈 어노테이션은?
@ExtendWith(SpringExtension.class)
@[ ... ]({ProductServiceImpl.class})
class ProductServiceTest2 {
@MockBean
ProductRepository productRepository;
@Autowired
ProductService productService;
...
}
9. 기존의 DB를 이용하여 테스트를 진행하려고 합니다. [...]에 들어갈 어노테이션은?
@DataJpaTest
@[ .... ](replace= AutoConfigureTestDatabase.Replace.NONE)
public class ProductRepositoryTest {
@Autowired
private ProductRepository productRepository;
@Test
void save(){
Product product = new Product();
product.setName("펜");
product.setPrice(1000);
product.setStock(1000);
Product savedProduct = productRepository.save(product);
assertEquals(product.getName(), savedProduct.getName());
assertEquals(product.getPrice(), savedProduct.getPrice());
assertEquals(product.getStock(), savedProduct.getStock());
}
}
해당 포스트는 장정우님,
[스프링부트 핵심가이드 : 스프링 부트를 활용한 애플리케이션 개발 실무] 를 참고하여 작성하였습니다.
아리
[스프링 1팀] 9장 연관관계 매핑 (1) | 2023.12.20 |
---|---|
[스프링 1팀] 8장 Spring Data JPA 활용 (2) | 2023.11.24 |
[스프링1] 6. 데이터베이스 연동 (0) | 2023.11.10 |
[스프링 1팀] 5-6장. API를 작성하는 다양한 방법 및 데이터베이스 연동 (0) | 2023.11.03 |
[스프링 1팀] 1장 ~ 4장. 스프링 부트란? + 개발에 앞서 알면 좋은 기초 지식 + 개발환경 구성 + 스프링부트 애플리케이션 개발하기 (0) | 2023.10.13 |