Day_35 ( 스프링 - 2 / 실습 )

HD.Y·2023년 12월 17일
0

한화시스템 BEYOND SW

목록 보기
30/58
post-thumbnail

🐻 Postman ( API 테스트 툴 )

  • 실습 시 사용 할 Postman( API 테스트 툴 ) 사용법 간단 정리

    1) Collections 탭에서 Add request 를 통해 요청들을 추가해서 만들 수 있으며,
       Add folder 를 통해 요청들을 기능별로 모아둘 폴더를 만들 수도 있다.


    2) 다음으로 요청을 만들면 요청 보낼 URL 을 적고, 이때 요청을 보낼 타입을 지정할 수
      있다. 나는 "GET""POST" 두개만 일단 사용해볼 예정이다.


    3) Environments 탭에서 개발 환경별로 변수에 대한 값을 지정할 수 있다. 예를 들어
      아래와 같이 설정하면 위의 그림에서 적었던 URL 을 내가 지정한 변수값으로
      적을 수 있다.


    4) 본격적으로 데이터를 보내보자. 내가 설정해둔 DEV 환경을 선택하고, 만약 GET
      방식이라면 Params에 데이터를 적고, POST 방식이라면 Body에 데이터를 적는다.
      나는 POST 방식으로 보낼 예정이므로 Body에 데이터로 id 에 test01 ,
      pw 에 qwer1234를 적어서 Send 버튼을 클릭하면 아래와 같이 "로그인 성공" 이라는
      글이 출력되는 것을 볼 수 있다. 여기서 왜 로그인 성공이 출력되는지는 바로 아래에서
      살펴보겠다.


스프링 실습하기 💻

  • 실습은 프론트엔드 서버와 백엔드 서버가 분리되어 있다는 가정하에 진행할 예정이다.

  • 먼저 사용자의 로그인과 회원가입 기능을 구현해본다. 이전 글에서 다뤘던 ControllerService 그리고 데이터를 전송할때 사용할 DTO 객체까지 아래의 코드에서 모두 다룰 예정이다.

  • 먼저 로그인 기능을 구현한다고 할 때, 사용자에게 입력받는 값은 id 와 pw 일 것이다. 그러면 로그인과 관련된 DTO 클래스를 만들어서 클래스에 id 와 pw 변수를 만들면 된다.

    import lombok.*;
     @Getter
     @Setter
     @AllArgsConstructor
     @Builder
     public class UserLoginDto {
         private String id;
         private String pw;
     }
  • 위에서 사용한 4가지 어노테이션들은 가장 많이 사용되는 어노테이션으로 사용할지 안할지는 모르나 사용될 수 있기때문에 달아준다.

  • 다음은 UserController 클래스 부분이다. 먼저 로그인에 대한 요청을 POST 방식으로 Service 에게 보낸다.

    @RestController
    @RequestMapping("/user")
    public class UserController {
    
       private final UserService userService;
    	
       // 생성자를 통한 의존성 주입
       public UserController(UserService userService) {
           this.userService = userService;
       }
    
       @RequestMapping(method = RequestMethod.POST, value = "/login")
       // 이렇게 해주는 이유 : 변수를 받을 값이 많아질 수 있기때문에 별도의 클래스를 만들어서 관리
       public String login(UserLoginDto userLoginDto) {
    
           if(userService.login(userLoginDto)) {
               return "로그인 성공";
           } else {
               return "로그인 실패";
           }
       }

    @Service // 서비스 아키텍처
     public class UserService {
       
       // 로그인
       public boolean login(UserLoginDto userLoginDto) {
           if(userLoginDto.getId().equals("test01")
                   && userLoginDto.getPw().equals("qwer1234")) {
               return true;
           } else {
               return false;
           }
       }
  • Service 클래스에서는 요청받은 데이터에서 만약 id 가 test01 과 같고,
    pw 가 qwer1234와 같다면 "true"를 반환시켜주고, 다르다면 "false" 를 반환시킬 것이다.

  • 그러면 Controller 에서는 반환받은 값을 프론트엔드 서버로 보낼 것이고, 프론트엔드 서버에서는 그것을 토대로 클라이언트에게 화면을 보여줄 것이다.

  • 따라서, Postman 실습 4번에서 데이터를 전송했을 때 id를 test01, pw를 qwer1234로 보냈기 때문에 true가 반환되어 로그인 성공이라는 글이 출력됬던 것이다.


  • 여기서 프론트엔드 서버로 반환할때 단순히 "로그인 성공" 이라는 글이 아닌 상태 코드도 함께 출력되도록 하기위해서 아래와 같이 작성할 수 있다.

      @RequestMapping(method = RequestMethod.POST, value = "/login")
       public ResponseEntity<Object> login(UserLoginDto userLoginDto) {
    
           if(userService.login(userLoginDto)) {
               return ResponseEntity.ok().body("로그인 성공");
           } else {
               return ResponseEntity.badRequest().body("로그인 실패");
           }
       }
  • ResponseEntity 를 사용하면 반환할때 상태와 함께 body 에 작성한 내용이 반환되기 때문에, 단순히 String 으로 반환했을때는 로그인 실패에도 상태가 200 OK 로 떠있으나, ResponseEntity를 사용하면 로그인 실패했을 때 400 Bad Request 라는 상태와 함께 로그인 실패라는 글이 출력되는 것을 볼 수 있다.

    String 사용 시


    ResponseEntity 사용 시


  • 다음으로 이제는 단순히 "로그인 성공/실패" 라는 글이 아닌 JSON 형태의 데이터 포맷으로 통신을 하기위해서는 아래와 같이 body 에 생성해둔 DTO 객체 자체를 넣으면 반환해줄때 입력받은 데이터를 JSON 형태의 포맷으로 알아서 바꿔서 출력해준다.
    @RequestMapping(method = RequestMethod.POST, value = "/login")
    public ResponseEntity<Object> login(UserLoginDto userLoginDto) {

        if(userService.login(userLoginDto)) {
            //return ResponseEntity.ok().body("로그인 성공");
            return ResponseEntity.ok().body(userLoginDto);
        } else {
            //return ResponseEntity.badRequest().body("로그인 실패");
            return ResponseEntity.badRequest().body(userLoginDto);
        }
    }


  • 위에서 로그인 기능을 구현한 것처럼 회원가입 기능을 똑같이 구현해 본다면 아래와 같이 작성해둔 UserController 클래스와 UserService 클래스에 회원가입 기능을 추가하고, 회원가입과 관련된 데이터를 지정할 UserSignUpDto 클래스를 만들면 된다.
    // UserController 클래스
    @RequestMapping(method = RequestMethod.POST, value = "/signup")
    public ResponseEntity<Object> signUp(UserSignUpDto userSignUpDto) {

        if(userService.signUp(userSignUpDto)) {
            return ResponseEntity.ok().body(userSignUpDto);
        }
        return ResponseEntity.badRequest().body(userSignUpDto);
    }
    
    // UserService 클래스
    public boolean signUp(UserSignUpDto userSignUpDto) {
        if(!userSignUpDto.getId().isEmpty() && !userSignUpDto.getPw().isEmpty()
           && !userSignUpDto.getName().isEmpty() && !userSignUpDto.getEmail().isEmpty()) {
            return true;
        } else {
            return false;
        }
    }
    
    // UserSignUpDto 클래스
    @Getter
    @Setter
    @AllArgsConstructor
    @Builder
    public class UserSignUpDto {
        private String id;
        private String pw;
        private String name;
        private String email;
    }

  • Postman 을 사용하여 POST 방식으로 회원가입을 하기 위한 데이터를 서버로 보내면 아래와 같이 출력되는 것을 볼 수 있다.

🐧 웹서버와 DB 연결을 통해 데이터 주고 받기

  • DB는 DB 프로젝트때 실시했던 "LONUA" DB(수정본) 를 가져와서 사용할 예정이다.
  • 먼저 웹서버와 DB를 연결하기 위해서는 인텔리제이에 라이브러리 추가를 해줘야되는데, pom.xml 에 아래와 같이 2개를 추가해주면 된다. ( 처음 스프링 프로젝트 생성 시 추가를 안해줬기 때문 )
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>

		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
		</dependency>
  • 다음으로 resources 폴더 밑에 application.yml 파일 안에 아래와 같이 작성해준다.

    spring:
     datasource:
      url: jdbc:mysql://[IP 주소]/[DB명]
      username: [사용자 이름]
      password: [사용자 패스워드]
      driver-class-name: com.mysql.cj.jdbc.Driver

    만약 application.yml이 아닌 application.properties로 되어있다면 이름을 바꿔줘도 되고, 아니면 properties로 해도 되지만 작성하는 형식이 다르다.

    둘 모두 스프링부트 외적인 시스템과 연동할때 필요한 profile 들을 정의하거나 프로그램이 실행되는데 필요한 속성들을 정의할때 사용한다는점에서는 똑같다.

    하지만 properties 파일은 key=value 형식으로 서술되고, 문자열만 사용가능하며 그외 자료형은 사용할 수 없다. 또 자바에서만 활용되며 하나의 properties 파일에는 하나의 profile만 저장할 수 있다.

    반면에 yml 파일은 properties 파일과 다르게 계층적 구조를 사용할 수 있다. 예를 들어 아래와 같이 DB 셋팅을 한다고 할때 datasource 라는 공통 구조는 상위에 한번만 작성하고 하위에 url, username, password, driver 등의 구조를 선언하여 쓸 수 있고 자바 외에도 파이썬이나 루비에서 활용 가능하며, 하나의 yml 파일에 여러개의 profile을 저장할 수 있다.


  • 이제 본격적으로 DB 와 연결하여 데이터를 CRUD 하는 실습을 해보려 한다. 이때 이제 위에서 했던 실습에 추가하여 DAO 객체를 사용하는 것이다.

  • 그럼 먼저 상품에 대한 리뷰기능을 만들어보는 Create를 실습해보겠다. 실습을 위해 기본적으로 Brand, User, Product 테이블 등에 데이터를 몇개씩 미리 넣어뒀다.

  • 리뷰 테이블에 들어가는 속성들은 아래와 같고, 현재 아무런 데이터도 들어있지 않은 상태이다.

  • 이 리뷰 테이블에 새로운 리뷰를 추가한다고 할때, 우리는 총 4개의 클래스를 만들어야된다. 먼저 기존에 실습했던 Controller, Service, DTO 클래스를 만들고 여기에 추가적으로 DAO 클래스까지 만들어서 데이터를 CRUD 할 수 있는 SQL 쿼리문을 작성하면 된다.

  • 먼저 리뷰 테이블에 들어가는 속성들을 지정할 DTO 클래스부터 만들면 아래와 같이 만들 수 있다. DTO 클래스의 이름도 이제는 POST 방식으로 리뷰 요청을 보내겠다는 의미로 "PostReviewReq" 로 바꿔서 앞으로 진행하겠다.

    @Getter
     @Setter
     @AllArgsConstructor
     @Builder
     public class PostReviewReq {
        // review_idx 는 Auto_Increment를 걸어둬서 데이터가 추가될때마다 자동으로 늘어나서 뺐다.
        private Integer product_idx;
        
        private Integer user_idx;
        
        private String review_content;
        
        private String review_photo;
        
        private Integer evaluation;
        
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        private Date createdAt;
        
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        private Date updatedAt;
        
        private Integer status;
    }
  • 다음으로 Dao 클래스를 만들어야 되는데 DAO 클래스는 "Repository" 역할을 하기 때문에 @Repository 어노테이션을 달아주고, 나머지는 JDBC 템플릿을 사용하여 SQL 쿼리문을 작성해봤다.
    INSERT 의 경우 쿼리문 실행 시 제대로 쿼리문이 동작했다면 1 row OK 이런 식으로 결과가 반환되기 때문에 반환하는 타입은 int 형으로 작성하였다.

    @Repository
     public class ReviewDao {
     private final JdbcTemplate jdbcTemplate;
    
     public ReviewDao(DataSource dataSource) {
         this.jdbcTemplate = new JdbcTemplate(dataSource);
     }
    
     // CREATE
     public int Create(PostReviewReq postReviewReq) {
         String sql = "INSERT INTO Review(product_idx, user_idx, review_content, review_photo, evaluation, createdAt, updatedAt, status)  VALUES(?, ?, ?, ?, ?, ?, ?, ?)";
         Object[] sqlParams = new Object[]{
                 postReviewReq.getProduct_idx(),
                 postReviewReq.getUser_idx(),
                 postReviewReq.getReview_content(),
                 postReviewReq.getReview_photo(),
                 postReviewReq.getEvaluation(),
                 postReviewReq.getCreatedAt(),
                 postReviewReq.getUpdatedAt(),
                 postReviewReq.getStatus()
         };
         return jdbcTemplate.update(sql, sqlParams);
     }
    }

  • 하지만 여기서 ReviewDao 클래스를 생성한 방식은 별로 좋은 방법이 아니다. 왜냐하면 내가 직접 JdbcTemplate 객체를 생성해주고 있기 때문이다.
    이것을 조금더 좋은 방법으로 바꿔보면 아래와 같이 config 클래스를 만들어서 그 안에 JdbcTemplate 메소드를 만드는데 메소드에 @Bean 어노테이션을 달면 된다.
    // config 클래스
    @Configuration
    public class JdbcTemplateConfig {
        // 작성방식은 여러가지가 있다.
        // 1. @Bean 어노테이션: 메소드에 달 수 있음 / 객체의 코드를 내가 변경할 수 없을 때 사용(통상 라이브러리의 객체일 때)
        // 2. @Component 어노테이션: 클래스에 달 수 있음 / 객체의 코드를 내가 변경할 수 있을 때 사용
        @Beanpublic 
        JdbcTemplate jdbcTemplate(DataSource dataSource) {
             return new JdbcTemplate(dataSource);
        }
     }
     
     // ReviewDao 클래스
     private final JdbcTemplate jdbcTemplate;
     
     public ReviewDao(JdbcTemplate jdbcTemplate) {	
            this.jdbcTemplate = jdbcTemplate;
     }

  • 다음으로 Service 클래스를 만들면 아래와 같이 만들어진다.

    @Service
    public class ReviewService {
      ReviewDao reviewDao;
    
      public ReviewService(ReviewDao reviewDao) {
          this.reviewDao = reviewDao;
      }
      
      public int create(PostReviewReq postReviewReq) {
          return reviewDao.Create(postReviewReq);
      }
    }

  • 마지막으로 Controller 클래스를 만들면 아래와 같이 만들어진다.

    @RestController
    @RequestMapping("/review")
    public class ReviewController {
      private final ReviewService reviewService;
    
      public ReviewController(ReviewService reviewService) {
          this.reviewService = reviewService;
      }
      
      @RequestMapping(method = RequestMethod.POST, value = "/create")
      public ResponseEntity<Object> create(PostReviewReq postReviewReq) {
          int result = reviewService.create(postReviewReq);
          return ResponseEntity.ok().body(result);
      }
    }

  • 이렇게 4개의 클래스를 만들면 준비는 끝났다. 클라이언트의 요청이 들어오면 프론트엔드 서버에서 백엔드 서버로 그 요청을 보내주고 백엔드 서버에서는 Controller 에서 Service 로 Service 에서 Dao 로 들어온 데이터를 보내며주면서 결과를 반환해주는 원리이다. 그리고 이때 데이터를 주고받을 때 사용하는 객체가 DTO 객체인 것이다.
    이렇게 계속해서 실습해보니 이 과정이 점점 눈에 들어오기 시작한다.

  • 이제 Postman 을 이용하여 테스트를 해보겠다. 속성에 맞는 데이터 타입으로 데이터를 넣고 Send 해봤더니 "1이 출력" 된 것을 볼 수 있었고, 실제로 데이터가 들어갔는지 Mysql Workbench로 확인해보니 입력한 데이터가 리뷰 테이블에 정상적으로 들어가있는것을 볼 수 있었다.


  • 다음으로 이렇게 리뷰 테이블에 저장되어있는 데이터를 Read 하는 실습을 해보겠다. Controller, Service, DAO 클래스는 그대로 사용하여 Read 기능만 코드를 추가하면 된다. 하지만, DTO는 기존의 것이 아닌 새로 만들어야 한다. 왜냐하면 Read 는 보통 GET 방식을 사용하기 때문이다.

    코드를 보면 들어가있는 변수들은 똑같은데 왜 따로 만드는거지? 라는 생각을 할 수 있는데, 그 이유는 Create 할 때 내가 일부러 모든 속성에 데이터를 넣어주고 싶어서 그렇게 만들었는데, 실제로 not null 이 아닌 속성에는 데이터를 넣어주지 않을 수도 있어서 Create 할때 뺄 수도 있다. 하지만 Read는 그 테이블의 모든 내용을 불러와야 하기때문에 테이블에 있는 모든 속성들을 넣어줘야 하기 때문에 둘은 다를 경우가 있다.

    @Getter
    @Setter
    @AllArgsConstructor
    @Builder
    public class GetReviewRes {
       private Integer product_idx;
       private Integer user_idx;
       private String review_content;
       private String review_photo;
       private Integer evaluation;
       @DateTimeFormat(pattern = "yyyy-MM-dd")
       private Date createdAt;
       @DateTimeFormat(pattern = "yyyy-MM-dd")
       private Date updatedAt;
       private Integer status;
    }

  • 다음은 DAO 클래스에 READ 기능을 추가해 보겠다.
        public List<GetReviewRes> list() {
          String sql = "SELECT * FROM Review";
          
          return jdbcTemplate.query(
                  sql,
                  (rs, rowNum) -> GetReviewRes.builder()
                          .product_idx(rs.getInt("product_idx"))
                          .user_idx(rs.getInt("user_idx"))
                          .review_content(rs.getString("review_content"))
                          .review_photo(rs.getString("review_photo"))
                          .evaluation(rs.getInt("evaluation"))
                          .createdAt(rs.getDate("createdAt"))
                          .updatedAt(rs.getDate("updatedAt"))
                          .status(rs.getInt("status"))
                          .build()
                  );
      }
  • 이렇게 작성한 이유는 Read 할때 Review 테이블에 있는 모든 데이터를 한번에 받아오기 위해서이다. 이때 출력되는 것은 JSON 형태로 출력된다.

  • 다음은 Service 클래스와 Controller 클래스에 Read 기능을 추가한 것이다.

    // Service 클래스
        public List<GetReviewRes> list() {
          return reviewDao.list();
      }
    
    // Controller 클래스
      @RequestMapping(method = RequestMethod.GET, value = "/read")
         public ResponseEntity<Object> list() {
             List<GetReviewRes> getReviewResList = reviewService.list();
             return ResponseEntity.ok().body(getReviewResList);
      }

  • 이제 Postman을 이용하여 테스트해보면 아래와 같이 Review 테이블의 데이터들이 JSON 형태로 순서대로 출력되는 것을 볼 수 있다.

  • Update 와 Delete 는 처음에 했던 Create 와 동일하게 똑같이 Controller 와 Sevice, Dao 클래스에 기능들만 추가해주면 된다.

    // DAO 클래스
        public int Update(PostReviewReq postReviewReq) {
          String sql = "UPDATE Review SET product_idx = ? WHERE user_idx = ?";
          Object[] sqlParams = new Object[]{
                  postReviewReq.getProduct_idx(),
                  postReviewReq.getUser_idx()
          };
          return jdbcTemplate.update(sql, sqlParams);
      }
      
    // Service 클래스
        public int update(PostReviewReq postReviewReq) {
          return reviewDao.Update(postReviewReq);
      }
    
    // Controller 클래스
        @RequestMapping(method = RequestMethod.POST, value = "/update")
        public ResponseEntity<Object> update(PostReviewReq postReviewReq) {
          int result = reviewService.update(postReviewReq);
          return ResponseEntity.ok().body(result);
      }

  • postman 에서 user_idx 가 4인 사람이 남긴 리뷰의 product_idx를 1로 바꿔보겠다.

  • 정상적으로 동작하며 1이 결과로 출력됬고, 아래와 같이 바뀐 것을 확인 할 수 있었다.

  • 마지막으로 DELETE 를 테스트해 본다. DELETE는 GET 방식으로 만들어봤다.

    // DAO 클래스
        public int Delete(GetReviewRes getReviewRes) {
          String sql = "DELETE FROM Review WHERE user_idx = ?";
          Object[] sqlParams = new Object[]{
                  getReviewRes.getUser_idx()
          };
          return jdbcTemplate.update(sql, sqlParams);
        }
      }
    
    // Service 클래스
        public int delete(GetReviewRes getReviewRes) {
          return reviewDao.Delete(getReviewRes);
      }
      
    // Controller 클래스
        @RequestMapping(method = RequestMethod.GET, value = "/delete")
        public ResponseEntity<Object> delete(GetReviewRes getReviewRes) {
          int result = reviewService.delete(getReviewRes);
          return ResponseEntity.ok().body(result);
      }
  • Postman을 이용하여 아래와 같이 요청을 전송했을때 정상적으로 동작하여 1이 출력됬고, 실제로 user_idx 가 4인 사람이 작성한 리뷰가 삭제된 것을 볼 수 있었다.


오늘의 느낀점 👀

  • 오늘은 정말 8시간이 맞나 싶을 정도로 많은 내용을 배운것 같다. 배운걸 정리하는데 무료 주말까지 다 써가며 복습하면서 정리하였는데, 너무나도 유익했던 하루였다. 자료구조와 알고리즘의 늪에서 빠져나와 다시 스프링을 배우면서 재미와 흥미를 느끼기 시작하는 것 같다.

  • 먼저, 가장 좋았던 점은 강사님께서 프론트엔드와 백엔드 서버가 분리된 환경으로 처음부터 시작할 것인지 의견을 물으셨는데, 대부분의 같이 수업듣는 분들께서 나와 같은 생각을 갖고 계셔서 내가 원하는대로 수업을 시작할 수 있어서 좋았다.,

  • 배워가는 과정에서는 실제로 실무에서도 이런식으로 사용하고 프론트엔드 개발자와 이렇게 데이터를 주고받는다 이런 개념이 머리에 조금씩 잡혀가고 있는 것 같다. 특히나 Controller 와 Service / 그리고 Service 와 Repository 그리고 DB 까지 요청이 들어왔을때 DB까지 접근하여 다시 결과를 반환해주는 이 과정이 복습하면서 하나씩 구현해보니 머리에 많이 잡혔다. 어떠한 원리로 데이터를 주고받고 그 데이터가 어떻게 반환되는지에 대한 메커니즘을 조금은 알 것 같았다.

  • 다음주도 그날 배운건 무조건 그날 복습을 끝낸다는 마음가짐으로 열심히 수업 듣고, 혼자공부도 별도로 열심히 해봐야겠다. 나는 이제 배운걸 토대로 다른것도 만들러 가본다.😎

profile
Backend Developer

0개의 댓글