forked from ExplorerJun/java-baseball
-
Notifications
You must be signed in to change notification settings - Fork 0
Home
Woopaloopa edited this page Oct 17, 2019
·
1 revision
- http://start.spring.io 접속
- 프로젝트 생성
- Project : Maven Project
- Language : Java
- Spring Boot : 2.1.3
- Project Metadata
- Group : com.samsungsds.backend
- Artifact : stock
- Dependencies : Web
- 프로젝트 압축 해제
- 프로젝트 Import
<repositories>
<repository>
<id>public-repository</id>
<url>http://70.121.224.52:8081/nexus/content/groups/public</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public-repository</id>
<url>http://70.121.224.52:8081/nexus/content/groups/public</url>
</pluginRepository>
</pluginRepositories>
@RestController
public class HelloController {
@RequestMapping("/hello")
public String index() {
return "hello world!";
}
}
- 브라우저에서 http://localhost:8080/hello 접속
- HelloController 클래스 Logger 추가
@RestController public class HelloController { private static final Logger logger = LoggerFactory.getLogger(HelloController.class); @RequestMapping("/hello") public String index() { logger.debug("debug Hello"); logger.info("info Hello"); logger.warn("warn Hello"); logger.error("error Hello"); return "hello world!"; } }
- application.properties 로그 레벨 설정 추가
logging.level.root=INFO logging.level.com.samsungsds.backend.stock=DEBUG
- 방법1. StockApplication.java의 main() 수정
@SpringBootApplication public class StockApplication { public static void main(String[] args) { new SpringApplicationBuilder(StockApplication.class) .banner(new Banner() { @Override public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) { out.print("Stock Service"); } }) .build() .run(args); } }
- 방법2. banner.txt 파일 생성 (classpath root)
@SpringBootApplication public class StockApplication { public static void main(String[] args) { SpringApplication.run(StockApplication.class, args); } }
- domain 패키지 생성, Stock 클래스 및 Result 클래스 생성
public class Stock { private String productId; private int amount; // constructor, getter, setter 생략 ** 구현해 주세요 ** }
public class Result<T> { private int errorCode= 200; private String errorMessage = "success"; private T result; // constructor, getter, setter 생략 ** 구현해 주세요 ** }
- controller 패키지에 StockController 클래스 생성
@RestController public class StockController { }
- 어노테이션으로 RestController임을 명시
@RestController @RequestMapping("/api/v1/stocks") public class StockController { }
- context root path를 @RequestMapping 어노테이션으로 명시
@CrossOrigin @RestController @RequestMapping("/api/v1/stocks") public class StockController { }
- CORS를 위해 @CrossOrigin 어노테이션으로 명시
import org.slf4j.Logger; import org.slf4j.LoggerFactory; @CrossOrigin @RestController @RequestMapping("/api/v1/stocks") public class StockController { private static Logger log = LoggerFactory.getLogger(StockController.class); }
- 서비스 로깅을 위해 아래와 같이 Logger를 선언
- 재고 조회를 위한 Rest API 추가
@GetMapping(value = "/{productId}") public Result<Stock> find(@PathVariable String productId) { log.info("find : {}", productId); return new Result<Stock> (); }
- 브라우저에서 정상 동작 여부 확인
- 재고 등록, 수정, 삭제를 위한 Rest API 추가
@PostMapping public Result<String> register(@RequestBody Stock newStock) { log.info("register : {}", newStock); return new Result<String> (); } @PutMapping public Result<String> modify(@RequestBody Stock newStock) { log.info("modify : {}", newStock); return new Result<String> (); } @DeleteMapping(value = "/{productId}") public Result remove(@PathVariable String productId) { log.info("remove : {}", productId); return new Result(); }
- Swagger 사용을 위한 Maven Dependency 추가
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.8.0</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.8.0</version> </dependency>
- config 패키지 생성, SwaggerConfig 클래스 생성
@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.any()) .paths(PathSelectors.ant("/api/v1/**")) .build(); } }
- http://localhost:8080/swagger-ui.html 접속하여 swagger 확인
- 재고 조회/등록/수정/삭제 API 확인 및 테스트
- API 선택 후 Try it out 버튼 클릭
- data 입력 후 excute 버튼 클릭
- response 에서 결과 확인
- src/main/resources에 application.yml 파일 생성
- 프로파일 별 환경 설정
- local : 9012 포트, dev : 8080 포트
spring: application: name: stocks-service logging: level: root: INFO com.samsungsds.backend.stock: DEBUG --- spring: profiles: local server: port: 9012 --- spring: profiles: dev server: port: 8080
- IDE에서 spring.profiles.active 값을 local로 설정
- http://localhost:9012/swagger-ui.html 에 접속하여 정상 동작을 확인한다.
- JPA, H2 DB 사용을 위해 Maven Dependency 추가
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency>
- application.yml 파일에 H2 환경 설정 추가
spring: application: name: sotcks-service datasource: driver-class-name: org.h2.Driver url: jdbc:h2:file:~/h2/DT;AUTO_SERVER=TRUE username: dtuser password: h2: console: enabled: true # 관리콘솔 사용 path: /h2console # http://localhost:9012/h2console 로 접속하여 관리 콘솔 사용 가능 (default: /h2-console) jpa: database: H2 generate-ddl: true # schema.sql 파일을 사용하여 Table을 생성하도록 설정 hibernate: ddl-auto: update
- 초기 Schema 생성을 위해 src/main/resources에 schema.sql 파일 생성
CREATE TABLE IF NOT EXISTS `stock` ( `product_id` varchar(36) NOT NULL, `amount` int NOT NULL, PRIMARY KEY (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 도메인 클래스 수정
- @Entity : JPA entity로 지정
- @Table : JPA entity와 STOCK 테이블과 매핑
- @Id : JPA에서 해당 Object의 ID로 인식
import javax.persistence.Entity; import javax.persistence.Table; import javax.persistence.Id; @Entity @Table(name = "STOCK") public class Stock implements Serializable { @Id private String productId; private int amount; // Constructor, Getter, Setter 생략 ** 구현해 주세요 ** }
- repository 패키지 생성 및 StockRepository 인터페이스 생성
@Repository public interface StockRepository extends CrudRepository<Stock, String> { Stock findByAmount(int amount); }
- JPA Query Method 방식
@Repository public interface StockRepository extends CrudRepository<Stock, String> { @Query(value="SELECT * FROM stock WHERE product_id=?1", nativeQuery=true) Stock getStockUsingSql(String productId); @Query(value="SELECT * FROM stock WHERE product_id=:productId", nativeQuery=true) Stock getStockUsingSqlWithNamedParam(@Param("productId") String productId); @Query(value="SELECT s FROM Stock s WHERE productId=?1", nativeQuery=false) Stock getStockUsingJpql(String productId); }
- JPA Native Query 방식
- Repository 테스트
- src/test/java에 repository 패키지 생성 및 StockRepositoryTest 클래스 생성
@RunWith(SpringRunner.class) @SpringBootTest public class StockRepositoryTest { @Resource StockRepository stockRepository; @Test public void save() { Stock newStock = new Stock(); newStock.setProductId("1"); newStock.setAmount(10); Stock stock = stockRepository.save(newStock ); assertEquals(10, stock.getAmount()); } @Test public void findById() { Stock stock = stockRepository.findById("1").orElse(null); assertEquals(10, stock.getAmount()); } @Test public void findByAmount() { Stock stock = stockRepository.findByAmount(10); assertEquals("1", stock.getProductId()); } @Test public void getStockUsingSql() { Stock stock = stockRepository.getStockUsingSql("1"); assertEquals(10, stock.getAmount()); } @Test public void getStockUsingSqlWithNamedParam() { Stock stock = stockRepository.getStockUsingSqlWithNamedParam("1"); assertEquals(10, stock.getAmount()); } @Test public void getStockUsingJpql() { Stock stock = stockRepository.getStockUsingJpql("1"); assertEquals(10, stock.getAmount()); } }
- service 패키지 생성 및 StockService 인터페이스 생성
public interface StockService { Stock find(String productId); Stock register(Stock stock); Stock modify(Stock stock); void remove(String productId); }
- service.impl 패키지 생성 및 StockServiceImpl 클래스 생성
@Service public class StockServiceImpl implements StockService { @Autowired private StockRepository stockRepository; @Override public Stock find(String productId) { return stockRepository.findById(productId).orElse(null); } @Override public Stock register(Stock stock) { if( find(stock.getProductId()) != null ) { return null; } return stockRepository.save(stock); } @Override public Stock modify(Stock stock) { Stock result = find(stock.getProductId()); if (result == null) { return null; } return stockRepository.save(stock); } @Override public void remove(String productId) { stockRepository.deleteById(productId); } }
- StockController 클래스에서 StockService를 사용하여 기능 구현
@CrossOrigin @RestController @RequestMapping("/api/v1/stocks") public class StockController { private static Logger log = LoggerFactory.getLogger(StockController.class); private StockService stockService; @Autowired public StockController(StockService stockService) { this.stockService = stockService; } @GetMapping(value = "/{productId}") public Result<Stock> find(@PathVariable String productId) { log.info("find : " + productId); Result<Stock> result = new Result<>(); Stock stock = stockService.find(productId); if (stock == null) { result.setErrorCode(HttpStatus.NOT_FOUND.value()); result.setErrorMessage("Not found."); } else { result.setResult(stock); } return result; } @PostMapping public Result<String> register(@RequestBody Stock newStock) { log.info("register : " + newStock); Result<String> result = new Result<>(); Stock resultStock = stockService.register(newStock); if (resultStock == null) { result.setErrorCode(HttpStatus.BAD_REQUEST.value()); result.setErrorMessage("Already Exists."); } else { result.setResult(resultStock.getProductId()); } return result; } @DeleteMapping(value = "/{productId}") public Result remove(@PathVariable String productId) { log.info("remove : " + productId); stockService.remove(productId); return new Result(); } @PutMapping public Result<String> modify(@RequestBody Stock newStock) { log.info("modify : " + newStock); Result<String> result = new Result<>(); Stock resultStock = stockService.modify(newStock); if (resultStock == null) { result.setErrorCode(HttpStatus.NOT_FOUND.value()); result.setErrorMessage("Not found."); } else { result.setResult(resultStock.getProductId()); } return result; } }
- Swagger를 활용하여 재고 조회/등록/수정/삭제 기능 테스트
- src/test/java에 service.impl 패키지 생성 및 StockServiceImplTest 클래스 생성
@RunWith(SpringRunner.class) @SpringBootTest public class StockServiceImplTest { @Resource StockService stockService; @Test public void find() { Stock stock = stockService.find("1"); assertEquals("1", stock.getProductId()); assertEquals(1000, stock.getAmount()); } }
- Mock-up 테스트로 변경
@RunWith(SpringRunner.class) @SpringBootTest public class StockServiceImplTest { @Resource StockService stockService; @MockBean StockRepository stockRepository; @Test public void find() { Stock returnStock = new Stock(); returnStock.setProductId("1"); returnStock.setAmount(1000);; when(stockRepository.findById("1")).thenReturn(Optional.of(returnStock)); Stock stock = stockService.find("1"); assertEquals("1", stock.getProductId()); assertEquals(1000, stock.getAmount()); verify(stockRepository, times(1)).findById("1"); } }
- src/test/java에 controller 패키지 생성 및 StockControllerTest 클래스 생성
package com.samsungsds.backend.stock.controllers; import org.springframework.context.ApplicationContext; @RunWith(SpringRunner.class) public class StockControllerTest { @Autowired private ApplicationContext applicationContext; }
- 서비스 mocking 및 Object 생성
@RunWith(SpringRunner.class) public class StockControllerTest { @Autowired private ApplicationContext applicationContext; private StockController stockController; private StockService mockStockService; @Before public void setUp() throws Exception { mockStockService = mock(StockService.class); stockController = new StockController(mockStockService); } }
- 테스트 생성
@RunWith(SpringRunner.class) public class StockControllerTest { @Autowired private ApplicationContext applicationContext; private StockController stockController; private StockService mockStockService; @Before public void setUp() throws Exception { mockStockService = mock(StockService.class); stockController = new StockController(mockStockService); } @Test public void givenProductId_whenFind_thenReturnStock() { //given String productId = "productId"; Stock stock = new Stock(productId, 0); when(mockStockService.find(productId)).thenReturn(stock); //when Result<Stock> result = stockController.find(productId); //then verify(mockStockService, times(1)).find(productId); assertThat(result.getErrorCode()).isEqualTo(HttpStatus.OK.value()); assertThat(result.getResult()).isEqualTo(stock); } @Test public void givenNonExistedProductId_whenFind_thenReturnError() { //given String productId = "productId"; when(mockStockService.find(productId)).thenReturn(null); //when Result<Stock> result = stockController.find(productId); //then verify(mockStockService, times(1)).find(productId); assertThat(result.getErrorCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); assertThat(result.getErrorMessage()).isEqualTo("Not found."); } }
- src/test/java의 controller 패키지에 StockControllerMvcMockTest 클래스 생성
package com.samsungsds.backend.stock.controllers; @RunWith(SpringRunner.class) @WebMvcTest(StockController.class) public class StockControllerMockMvcTest { @Autowired MockMvc mockMvc; }
- 테스트 작성
@Test public void find() throws Exception { mockMvc.perform(get("/api/v1/stocks/1")) .andExpect(status().isOk()) .andExpect(content().json("{\"errorCode\":200,\"errorMessage\":\"success\",\"result\":{\"productId\":\"1\",\"amount\":10}}")) .andExpect(jsonPath("$.result.productId").value("1")) .andExpect(jsonPath("$.result.amount").value("10")) ; }
- @MockBean을 사용하여 StockService를 mocking한 후 stub 작성
@Autowired MockMvc mockMvc; @MockBean StockService stockService; @Test public void find() throws Exception { Stock stock = new Stock("1", 10); when(stockService.find("1")).thenReturn(stock); mockMvc.perform(get("/api/v1/stocks/1")) .andExpect(status().isOk()) .andExpect(content().json("{\"errorCode\":200,\"errorMessage\":\"success\",\"result\":{\"productId\":\"1\",\"amount\":10}}")) .andExpect(jsonPath("$.result.productId").value("1")) .andExpect(jsonPath("$.result.amount").value("10")) ; verify(stockService, times(1)).find("1"); }
- src/test에 resources 폴더를 생성하고 테스트 환경을 위한 application.yml 생성
spring: profiles: active: test # test 환경으로 프로파일 적용 application: name: stocks-service datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:testdb # In memory DB 설정 username: dtuser password: jpa: database: H2 generate-ddl: true hibernate: ddl-auto: update
- src/test/java의 controller 패키지에 StockControllerFuncTest 클래스 생성
@RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class StockControllerFuncTest { }
- @RunWIth, @SpringBootTest 어노테이션으로 스프링 컨텍스트와 서블릿 컨테이너를 띄워 실제 서버에 테스트 하는 환경을 구성
@RunWith(SpringJUnit4ClassRunner.class) @AutoConfigureMockMvc @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class StockControllerFuncTest { @Autowired MockMvc mockMvc; }
- MockMvc 객체는 브라우저에서의 Request와 Response를 mocking한 것
- Component Test 완성
@RunWith(SpringJUnit4ClassRunner.class) @AutoConfigureMockMvc @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class StockControllerFuncTest { @Autowired MockMvc mockMvc; @Autowired StockRepository stockRepository; @Test public void givenProductId_whenFind_thenReturnStock() throws Exception { // data clear stockRepository.deleteAll(); // insert init data Stock stock = new Stock("productId", 10); stockRepository.save(stock); mockMvc.perform(get("/api/v1/stocks/productId")) .andExpect(status().isOk()) .andExpect(content().json("{\"errorCode\":200,\"errorMessage\":\"success\",\"result\":{\"productId\":\"productId\",\"amount\":10}}")) .andExpect(jsonPath("$.result.productId").value("productId")) .andExpect(jsonPath("$.result.amount").value("10")) ; } @Test public void givenNonExistedProductId_whenFind_thenReturnError() throws Exception { // data clear stockRepository.deleteAll(); mockMvc.perform(get("/api/v1/stocks/productId")) .andExpect(status().isOk()) .andExpect(content().json("{\"errorCode\":404,\"errorMessage\":\"Not found.\",\"result\":null}")) .andExpect(jsonPath("$.errorCode").value(404)) .andExpect(jsonPath("$.result").doesNotExist()) ; } }
- StockRepository를 Autowired하여 원하는 데이터로 초기화
- MockMvc.perform 메서드를 통해 원하는 Request를 테스트 서버에 보낼 수 있고 결과를 비교하여 테스트