대외활동/UMC SERVER

7주차 실습 - API 코딩 후 배포하기

Debin 2022. 5. 6.
반응형

이번 시간에는 7주차에 진행한 API 코딩 진행과정과 배포 진행 과정에 대해 정리해보겠습니다.

연합 동아리 주최자인 컴공 선배 레포지토리를 클론 했으므로 모든 아키텍쳐를 서술하기는 어려워서 작성한 API와 

사용한 클래스 위주로 정리하겠습니다. 이제 본격적으로 시작하겠습니다!!

 

API 코딩

코딩할 API는 유저 삭제 API다. 먼저 논리적인 삭제(상태 컬럼을 변경)가 아니라 실제로 행을 삭제하는 물리적 연산을 진행했다.

따라서 DELETE 메소드를 사용했다. 먼저 Controller, Service, Dao 순으로 나열하겠다.

정말 기초적인 수준의 API로 Validation도 가볍게 진행했다.

우선은 사용할 DTO와 사용자 정의 Exception, 최종 응답 DTO에 대해 알아보자.

@Getter
@Setter
@AllArgsConstructor
public class DeleteUserReq { //delete api에 요청되는 전달 dto
    private int userIdx;
}

@Getter
@Setter
@AllArgsConstructor
public class DeleteUserRes { //delete api를 통해 리턴될 전달 dto
    private String name;
    private String nickName;
    private String phone;
    private String email;
    private String password;
}


@Getter
@Setter
@AllArgsConstructor
public class BaseException extends Exception { //사용자 정의 API
    private BaseResponseStatus status;
}


@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
public class BaseResponse<T> {
    @JsonProperty("isSuccess")
    private final Boolean isSuccess;
    private final String message;
    private final int code;
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private T result;

    // 요청에 성공한 경우
    public BaseResponse(T result) {
        this.isSuccess = SUCCESS.isSuccess();
        this.message = SUCCESS.getMessage();
        this.code = SUCCESS.getCode();
        this.result = result;
    }

    // 요청에 실패한 경우
    public BaseResponse(BaseResponseStatus status) {
        this.isSuccess = status.isSuccess();
        this.message = status.getMessage();
        this.code = status.getCode();
    }
}

 

먼저 컨트롤러다. 2가지 유저 삭제 API를 구현했다. 

첫 번째 과정은 몽땅 try catch 문 하나로 잡아버리는 것이다.

두 번째 과정은 나름 Validation을 적용했습니다. 우선은 ControllerAdvice와 ExceptionHandler를 적용하지 않았습니다.

package com.example.demo.src.user;

import com.example.demo.config.BaseException;
import com.example.demo.config.BaseResponse;
import com.example.demo.src.user.model.*;
import com.example.demo.utils.JwtService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import static com.example.demo.config.BaseResponseStatus.POST_USERS_EMPTY_EMAIL;
import static com.example.demo.config.BaseResponseStatus.POST_USERS_INVALID_EMAIL;
import static com.example.demo.utils.ValidationRegex.isRegexEmail;

@RestController
@RequestMapping("/users")
public class UserController {

    final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final UserProvider userProvider;
    private final UserService userService;
    private final JwtService jwtService;

    @Autowired
    public UserController(UserProvider userProvider, UserService userService, JwtService jwtService){
        this.userProvider = userProvider;
        this.userService = userService;
        this.jwtService = jwtService;
    }
    
    /*
     *
     * 다른 메소드들은 생략
     *
     */
    
    
    @DeleteMapping("/{userIdx}")
    public BaseResponse<DeleteUserRes> deleteByIdx(@PathVariable int userIdx){
        DeleteUserReq deleteUserReq = new DeleteUserReq(userIdx);
        try {
            DeleteUserRes deleteUserRes = userService.deleteByUserIdx(deleteUserReq);
            return new BaseResponse<>(deleteUserRes);
        }
        catch(BaseException exception){
            return new BaseResponse<>(exception.getStatus());
        }
    }

    @DeleteMapping("/v2/{userIdx}")
    public BaseResponse<DeleteUserRes> deleteByIdxV2(@PathVariable int userIdx){

        DeleteUserReq deleteUserReq = new DeleteUserReq(userIdx);
        try {
            DeleteUserRes deleteUserRes = userService.deleteByUserIdxV2(deleteUserReq);
            return new BaseResponse<>(deleteUserRes);
        }
        catch(BaseException exception){
            return new BaseResponse<>(exception.getStatus());
        }
    }
}

다음은 서비스 코드입니다.

package com.example.demo.src.user;


import com.example.demo.config.BaseException;

import com.example.demo.src.user.model.*;
import com.example.demo.utils.JwtService;
import com.example.demo.utils.SHA256;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import static com.example.demo.config.BaseResponseStatus.*;

@Service
public class UserService {
    final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final UserDao userDao;
    private final UserProvider userProvider;
    private final JwtService jwtService;
    
    /*
     *
     * 다른 코드는 생략
     *
     */

    @Autowired
    public UserService(UserDao userDao, UserProvider userProvider, JwtService jwtService) {
        this.userDao = userDao;
        this.userProvider = userProvider;
        this.jwtService = jwtService;

    }

    public DeleteUserRes deleteByUserIdx(DeleteUserReq deleteUserReq)throws BaseException{
        try{
            DeleteUserRes deleteUserRes = userDao.deleteByUserIdx(deleteUserReq);
            return deleteUserRes;

        }catch(Exception exception){
            throw new BaseException(DATABASE_ERROR);
        }
    }

    public DeleteUserRes deleteByUserIdxV2(DeleteUserReq deleteUserReq)throws BaseException{

        int checkId = userDao.checkId(deleteUserReq.getUserIdx());

        if(checkId == 0){
            throw new BaseException(USERS_INVALID_USER_ID);
        }

        try{
            DeleteUserRes deleteUserRes = userDao.deleteByUserIdx(deleteUserReq);
            return deleteUserRes;
        }
        catch(Exception exception){
            throw new BaseException(DATABASE_ERROR);
        }
    }
}

단순한 deleteByUserIdx는  다시 try-catch 문으로 진행한다.

그에 비해 deleteByUserIdxV2는 Validation이 존재한다.

만약 userIdx가 없으면 if문을 사용해 BaseException을 통해 예외를 발생시킨다. 

이 밖에 Dao 메소드를 사용하면서 발생한 예외도 try-catch문으로 처리한다.

모두 동일한 Dao 메소드로 유저 삭제를 진행한다.

 

다음은 Dao 코드다. 물리적인 삭제를 진행하므로 먼저 삭제할 유저를 찾아서 변수에 저장하고, 삭제를 진행한 후, 삭제한 유저를 리턴해준다. 이런 흐름으로 API를 작성했다.

package com.example.demo.src.user;


import com.example.demo.src.user.model.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import javax.sql.DataSource;
import java.util.List;

@Repository
public class UserDao {

    private JdbcTemplate jdbcTemplate;

    @Autowired
    public void setDataSource(DataSource dataSource){
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    
    /*
     *
     * 다른 코드들은생략
     *
     */

    public DeleteUserRes deleteByUserIdx(DeleteUserReq deleteUserReq){
        String selectUserIdxQuery = "select  nickName, name, email, phone, password from User where userIdx=?";
        String deleteUserIdxQuery = "delete from User where userIdx=?";
        int idxParams = deleteUserReq.getUserIdx();

        DeleteUserRes deleteUserRes = this.jdbcTemplate.queryForObject(selectUserIdxQuery,
                (rs, rowNum) -> new DeleteUserRes(
                        rs.getString("name"),
                        rs.getString("nickName"),
                        rs.getString("phone"),
                        rs.getString("email"),
                        rs.getString("password")),
                idxParams);

        this.jdbcTemplate.update(deleteUserIdxQuery,idxParams);

        return deleteUserRes;
    }

    //아이디가 존재하는지 체크
    public int checkId(int userIdx){
        String checkUserIdxQuery = "select exists(select userIdx from User where userIdx = ?)";
        int checkUserIdxParams = userIdx;
        return this.jdbcTemplate.queryForObject(checkUserIdxQuery,
                int.class,
                checkUserIdxParams);

    }

}

아래는 API가 정상적으로 동작한 모습이다.

유저 삭제 API 동작 성공

아래는 본인이 간단하게 작성한 API 명세서다.

이렇게 유저 삭제 API 명세서 작성까지 마무리했다. 이제 코딩을 하면서 본인이 느낀점에 대해 적어보겠다.

  • 개인적으로 생각하기에는 성능에 문제가 있지 않으면 Dao OR Repository가 엔티티를 리턴하고 서비스와 컨트롤러 단에서 엔티티를 DTO로 변환해 주는 것이 좋다고 생각한다. 나중에 JPA를 사용할 때 코드가 지저분해지는 것도 그렇고, DTO를 리턴한다면 엔티티를 조회, 수정, 삭제, 생성 시에 각 API에 해당하는 DTO마다 의존성이 생기기 때문이다. 그래서 본인의 생각은 퓨어하게 DAO, Repository가 엔티티에만 의존하는 것이 좋다고 생각한다.
  • 이번에는 BaseException으로 메시지를 변경하면서 예외를 관리했다.  그러나 사실은 모든 예외를 개발자가 Exception을 상속해서 재정의해서 사용하는 것이 좋다고 한다. 사실 코딩을 하다 보면 예외 처리에 70% 정도의 노력을 투자한다는데, 이 예외 처리에 대해서도 더 진지한 고민이 많이 필요할 것 같다.
  • ControllerAdvice와 ExceptionHandler도 간단하게 사용해봤다. 역시 편리하다. 조만간 복습할겸 이에 관한 포스팅을 올려봐야겠다.
  • JDBC Template 기초적인 부분에 대해서 학습할 수 있어서 좋았다. 역시 JPA가 편하구만...

이제 배포 진행 과정에 대해 정리해보겠다.

웹 API 배포 진행하기

우선 우리 EC2 우분투에 접속한다. 그리고 아마 자바가 다운로드 되어 있지 않을 것이므로, 먼저 자바를 다운로드 받는다.

sudo apt install default-jdk

이제 파일 경로를 이동한다.

cd /var/www

이후 우리의 프로젝트가 올라가 있는 곳에서 git clone를 진행한다.

git clone 레포지토리경로.git

이제 그리고 nginx 설정 파일을 수정한다. 해당 명령어를 치고 아래와 같이 수정하자.

vim /etc/nginx/sites-available/default

root에 우리 서버 파일의 경로를 적어 넣는다.

이제 서버 파일로 이동해서 아래와 같은 명령어를 입력한다. 꽤 시간이 오래걸린다. 

반드시 서버 파일 경로로 이동하자!!!!

필자는 도중에 멈춰서 인스턴스를 재부팅하고 다시 시작했다.

./gradlew clean build

그러면 한참후에 완료가 되는데 이제 nginx를 재시작하고 아래와 같은 입력어를 넣는다.

service nginx restart  //nginx 재시작
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar //스프링 실행

그럼 정상적으로 동작한다!!!! 배포를 일단 완료했다.

배포 성공!!

마지막으로 프록시 패스를 설정해서 포트를 명시하지 않아도 되고 보안이 좀 더 좋아지게 만들어보자.

아래와 같이 다시 nginx 설정 파일로 들어가서 설정을 해주자.

sudo vim /etc/nginx/sites-available/default

proxy_pass 설정

이제 nginx를 재시작하고 아래와 같은 명령어를 입력하자.

nohup은 무중단 서비스를 진행시킨다. 자바가 중단하지 않고 무중단 서비스로 배포가 된다.

sudo service nginx restart
nohup java -jar build/libs/demo-0.0.1-SNAPSHOT.jar

이렇게 우리는 배포까지 완료했다. 와우!!!

 

꽤 뿌듯하다. 배포할 때 꽤 떨렸다. 오류가 발생할까봐.. 다행인지는 모르겠지만 마무리까지 잘 끝났다.

https도 이전에 certbot으로 적용해놨더니 잘 통신이 된다.. 이에 대해서는 좀 더 공부가 필요한 것 같다.

 

벌써 7주차까지 마무리했다! 3주 밖에 남지 않았다. 앞으로도 더더 열심히하자!!!!

 

반응형

댓글