Skip to content
Woopaloopa edited this page Oct 17, 2019 · 1 revision

Spring Boot 기반 back-end 개발

1. Spring Initializr

  1. http://start.spring.io 접속
  2. 프로젝트 생성
    • Project : Maven Project
    • Language : Java
    • Spring Boot : 2.1.3
    • Project Metadata
      • Group : com.samsungsds.backend
      • Artifact : stock
    • Dependencies : Web
  3. 프로젝트 압축 해제
  4. 프로젝트 Import

2. pom.xml 설정 (proxy)

<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>

3. controller 패키지 생성, HelloController 클래스 생성

@RestController
public class HelloController {
 
    @RequestMapping("/hello")
    public String index() {
        return "hello world!";
    }
}

4. 로그 레벨 설정

  • 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

5. Banner 설정

  • 방법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);
        }
    }

6. Rest API 구성

  1. 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 생략 ** 구현해 주세요 **
    }
  2. 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를 선언
  3. 재고 조회를 위한 Rest API 추가
    @GetMapping(value = "/{productId}")
    public Result<Stock> find(@PathVariable String productId) {
        log.info("find : {}", productId);
        return new Result<Stock> ();
    }
  4. 재고 등록, 수정, 삭제를 위한 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();
    }

7. API Document 생성

  1. 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>
  2. 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();
    
        }
    }
  3. http://localhost:8080/swagger-ui.html 접속하여 swagger 확인
  4. 재고 조회/등록/수정/삭제 API 확인 및 테스트
    • API 선택 후 Try it out 버튼 클릭
    • data 입력 후 excute 버튼 클릭
    • response 에서 결과 확인

8. 개발 환경 설정

  1. 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
  2. IDE에서 spring.profiles.active 값을 local로 설정
  3. http://localhost:9012/swagger-ui.html 에 접속하여 정상 동작을 확인한다.

9. 서비스 개발

  1. 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>
  2. 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
  3. 초기 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;
  4. 도메인 클래스 수정
    • @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 생략 ** 구현해 주세요 **
    }
  5. 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 방식
  6. 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());
        }
    
    }
  7. service 패키지 생성 및 StockService 인터페이스 생성
    public interface StockService {
    
        Stock find(String productId);
    
        Stock register(Stock stock);
    
        Stock modify(Stock stock);
    
        void remove(String productId);
    }
  8. 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);
        }
    }
  9. 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;
        }
    }
  10. Swagger를 활용하여 재고 조회/등록/수정/삭제 기능 테스트

10. Service Unit Test 작성

  1. 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());
        }
    }
  2. 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");
        }
    }

11. Controller Unit Test 작성

  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;
    
    }
  2. 서비스 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);
        }
    }
  3. 테스트 생성
    @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.");
        }
    }

12. MockMvc를 활용한 Controller Unit Test 작성

  1. src/test/java의 controller 패키지에 StockControllerMvcMockTest 클래스 생성
    package com.samsungsds.backend.stock.controllers;
    
    @RunWith(SpringRunner.class)
    @WebMvcTest(StockController.class)
    public class StockControllerMockMvcTest {
    
        @Autowired
        MockMvc mockMvc;
    }
  2. 테스트 작성
    @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"))
                ;  
    }
  3. @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");
    }

13. Component Test 작성

  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
  2. 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한 것
  3. 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를 테스트 서버에 보낼 수 있고 결과를 비교하여 테스트