Life

[개발Life] RESTFUL 하다?

Bay Im 2024. 4. 3. 22:16
REST API를 개발하면서 드는 생각들

개념 정리 

 

이전까지는 @RequestParam으로 사용자 입력 값을 가져오고, Model을 사용하여 addAtribute로 데이터를 보내서 출력했다.

하지만 REST API를 사용한다면 JSON 형식의 데이터를 주고 받는다.

@RequestBody로 사용자 입력 값을 가져오고 @ResponseBody로 데이터를 보내준다.

 

그리고 View만 출력하는 ViewController와 REST API 메서드만 모여있는 ApiController를 구분해서 구현한다.

ApiController 클래스는 @RestController 어노테이션을 준다. (@RestController = @Controller + @ResponseBody)

RestController의 주요 메소드는 GET, POST(create), DELETE, PUT(update)이다.

ApiController 메서드의 return 값은 DTO 클래스를 생성하여 DTO에 담아서 보낸다. 또한 Map, List도 담아서 보낼 수 있다. 

 

 

그래서 내 현재 고민은?

 

return 값을 어느 데이터 타입으로 작성해서 보내야 정석인지 모르겠다.

DTO를 만들어서 담아서 보낼지, Map 아니면 List로 담아서 보낼지!

 

주변에 현업 백엔드 개발자기 없어서 데이터 엔지니어에게 조언을 구해봤는데 지금 내가 하고있는 고민은 RESTFUL에 대한 고민이 아니라 했다.

RESTFUL의 의미는 GET, POST, DELETE, PUT으로 나누는 의미가 크다는 것!

return 값의 형태는 내가 말한 3가지 방법 모두 RESTFUL한 방법이라고 했다.

 

맞긴 하다.. 내가 고민한 내용에 대하여 아무리 구글링을 해도 RestController 메소드의 return 값의 형태가 다 달랐고, 정석이라고 말하는 사람이 없었기 때문이다.

내가 생각하기에 Request DTO랑 Response DTO를 따로따로 만들어서 담아서 보내는 게 나을 것같다고 생각했는데,

또 메소드마다 담을 Param의 개수가 다르니까 그만큼 DTO 클래스를 만들어 하나?

너무 DTO 클래스가 많아지는데.. 부터 생각해서 어떤 데이터 타입으로 return 해야 정석인가 까지 와버렸다..

 

Model을 사용하면서 개발하다가 현업에서 사용하는 방식이라는 REST API를 알게된 후 내가 여태까지 했던 방식은 뭐였나..하는 현타가 왔었다.

그래서 더욱 개발 하기도 전에 방식부터 찾고 생각이 많아졌던 것 같다.

하지만 찾아봐도 정석은 안나오고, 찾아봐도 개발만 늦어질 것 같으니까 우선 DTO 클래스가 많이 생겨도 DTO를 사용하는 걸로 결정..!

 

아직 배우는 단계인데 너무 정석적인 깔끔한 코드를 짜고 싶다는 생각부터 하는거 아닌가 싶고... 코드 짜는게 성격이랑도 연결되나..ㅠ

교육 과정 다 끝난 여름쯤에는 클린 코드 책을 읽어보고 싶다.

 

 

Model에서 REST API로 변환하기

  • Model
    • 이전 메소드 (@Controller)
      •     private List<Member> memberList = new ArrayList<>();
            
            // 회원가입
            @PostMapping("/join")
            public String join(@RequestParam("inputName") String inputName,
                               @RequestParam("inputEmail") String inputEmail,
                               @RequestParam("inputPw") String inputPw,
                               Model model) {
                Member member = new Member();
                member.setUsername(inputName);
                member.setEmail(inputEmail);
                member.setPassword(inputPw);
                member.setJoindate(LocalDate.now());
        
                memberList.add(member);
                model.addAttribute("memberList", memberList);
                System.out.println("가입한 회원 정보: " + memberList);
                return "redirect:/";
            }
            
            // 로그인
            @PostMapping("/")
            public String login(@RequestParam("inputName") String inputName,
                                @RequestParam("inputPw") String inputPw,
                                Model model) {
                String message = "로그인 실패!";
        
                for(Member member: memberList) {
                    if(member.getUsername().equals(inputName) && member.getPassword().equals(inputPw)) {
                        message = "로그인 성공!";
                        break;
                    }
                }
        
                model.addAttribute("message", message);
                return "login";
            }
  • REST API로 변환
    • 회원가입
      • DTO 생성
        • @Data
          public class JoinReqDTO {
              private String inputName;
              private String inputEmail;
              private String inputPw;
          }
      • 메소드 Rest API로 변환
        •     private List<Member> memberList = new ArrayList<>();
              
              // 회원가입
              @PostMapping("/join")
              @ResponseBody
              public List<Member> join(@RequestBody JoinReqDTO joinReqDTO) {
                  Member member = Member.builder()
                          .username(joinReqDTO.getInputName())
                          .email(joinReqDTO.getInputEmail())
                          .password(joinReqDTO.getInputPw())
                          .joindate(LocalDate.now())
                          .build();
          
                  memberList.add(member);
                  return memberList;
              }
      • html에서 스크립트 코드 작성
        •     const join = () => {
                  const inputName = document.getElementById("inputName").value;
                  const inputEmail = document.getElementById("inputEmail").value;
                  const inputPw = document.getElementById("inputPw").value;
          
                  fetch("/join", {
                      method: "POST",
                      headers: {
                          "Content-Type": "application/json"
                      },
                      body: JSON.stringify({
                          inputName: inputName,
                          inputPw: inputPw,
                          inputEmail: inputEmail
                      })
                  })
                      .then(response => {
                          if (response.ok) {
                              window.location.href = "/";
                          } else {
                              console.error("회원가입 실패");
                          }
                      })
                      .catch(error => {
                          console.error("오류 발생: ", error);
                      })
              }
      • 결과
       
    • 로그인
      • DTO 생성
        • @Data
          public class LoginReqDTO {
              private String inputName;
              private String inputPw;
          }
      • 메소드 RestController로 변환
        •     @PostMapping("/")
              @ResponseBody
              public Map<String, String> login(@RequestBody LoginReqDTO loginReqDTO) {
                  String message = "로그인 실패!";
                  String inputName = loginReqDTO.getInputName();
                  String inputPw = loginReqDTO.getInputPw();
          
                  for(Member member: memberList) {
                      if(member.getUsername().equals(inputName) &&
                              member.getPassword().equals(inputPw)) {
                          message = "로그인 성공!";
                          break;
                      }
                  }
          
                  Map<String, String> loginResMap = new HashMap<>();
                  loginResMap.put("message", message);
          
                  return loginResMap;
              }
      • html에서 스크립트 코드 작성
        •     const login = () => {
                  const inputName = document.getElementById("inputName").value;
                  const inputPw = document.getElementById("inputPw").value;
          
                  fetch("/", {
                      method: "POST",
                      headers: {
                          "Content-Type": "application/json"
                      },
                      body: JSON.stringify({
                          inputName: inputName,
                          inputPw: inputPw
                      })
                  })
                      .then(response => response.json())
                      .then(data => {
                          document.getElementById("alert").innerText = data.message;
                      })
                      .catch(error => {
                          console.error('Error:', error);
                      });
              }
      • 결과
         

 

그래서 다시한번 Rest API 구현 순서는?
  1. ViewController와 ApiController는 따로 나눠서 생성한다.
  2. ViewController를 생성한다.
    • 클래스에 @Controller 어노테이션을 준다.
    • main이나 Form 출력같이 로직과 전달 값 없고, 오로지 View만 출력하는 메소드로만 구성한다.
    • 해당 메소드의 return 값은 이동할 html 파일명을 주는 @Getmapping 메소드이다.
    • 예시
      • public class ViewController {
            // 메인 출력
            @GetMapping("/")
            public String main() {
                return "login";
            }
        
            // 회원가입 폼 출력
            @GetMapping("/join")
            public String viewJoin() {
                return "join";
            }
        }
  3. RequestDTO/ResponseDTO 클래스를 생성한다.
    • ApiController 메소드에서 Request할 값이나, Reponse할 값이 있다면 그 값을 담아서 넘겨줄 DTO 클래스를 생성한다.
    • DTO 클래스 어노테이션 주입
      • @Getter, @Setter, @AllArgsConstructor, @NoArgsConstructor, @Builder
    • DTO → Entity 변환 메소드 생성
    • Entity → DTO 변환 메소드 생성
    • REST API를 구성할 때 return 값 데이터 형태는 DTO, Map, List 형태 모두 가능하다.
      • 하지만 나는 왠만해서 DTO를 만들어서 사용하려고 했다.
      • 그런데 넘겨줄 값이 한 개이면 그냥 Map으로 선언하여 넘겨주었다! (고민: 값 1개를 넘겨줘도 DTO를 만들어서 사용하는게 나을지?)
    • 예시
      • // ApiController의 join 메소드 사용 
        @Data
        public class JoinReqDTO {
            private String inputName;
            private String inputEmail;
            private String inputPw;
        }
        -------------------------------------
        // ApiController의 login 메소드 사용
        @Data
        public class LoginReqDTO {
            private String inputName;
            private String inputPw;
        }
  4. ApiController를 생성한다.
    • 클래스에 @RestController 어노테이션을 준다.
      • 아직 전체 메소드를 RESTFUL하게 만들지 못하였다면 먼저 @Controller 어노테이션을 주입하고 REST 메소드에 @ResponseBody를 붙여서 점점 만들어 나간다.
        • 예시
          •     // 회원가입
                @PostMapping("/join")
                @ResponseBody
                public List<Member> join(@RequestBody JoinReqDTO joinReqDTO) {
                    Member member = Member.builder()
                            .username(joinReqDTO.getInputName())
                            .email(joinReqDTO.getInputEmail())
                            .password(joinReqDTO.getInputPw())
                            .joindate(LocalDate.now())
                            .build();
            
                    memberList.add(member);
                    return memberList;
                }
    • REST API 메소드를 생성한다.
      • 매개변수에서 받아올 값은 @RequestBody와 위에서 만든 RequestDTO를 사용하여 JSON 형태로 값을 받아온다.
        • 예시
          • join(@RequestBody JoinReqDTO joinReqDTO)
      • 엔티티에 저장할 값(POST)이 있다면 builder()를 사용하여 값을 넣어준다.
        • 해당 엔티티 클래스에는 @Builder 어노테이션이 붙여진 상태여야 한다.
        • 예시
          • Member member = Member.builder()
                    .username(joinReqDTO.getInputName())
                    .email(joinReqDTO.getInputEmail())
                    .password(joinReqDTO.getInputPw())
                    .joindate(LocalDate.now())
                    .build();
      • 보낼 값(Reponse)의 데이터 타입은 위에서 생성한 DTO나 Map, List 형태 모두 가능하다.
        • 메소드에 @ResponseBody 어노테이션을 붙여주거나, @RestController가 붙은 컨트롤러 클래스라면 자동으로 3가지의 데이터를 모두 JSON으로 자동변환하여 넘겨주기 때문이다.
      • 예시
        •     // Map 형태 
              @PostMapping("/")
              public Map<String, String> login(@RequestBody LoginReqDTO loginReqDTO) {
                  String message = "로그인 실패!";
                  String inputName = loginReqDTO.getInputName();
                  String inputPw = loginReqDTO.getInputPw();
          
                  for(Member member: memberList) {
                      if(member.getUsername().equals(inputName) &&
                              member.getPassword().equals(inputPw)) {
                          message = "로그인 성공!";
                          break;
                      }
                  }
          
                  Map<String, String> loginResMap = new HashMap<>();
                  loginResMap.put("message", message);
          
                  return loginResMap;
              }
          
          	// List 형태 
              // 회원가입
              @PostMapping("/join")
              public List<Member> join(@RequestBody JoinReqDTO joinReqDTO) {
                  Member member = Member.builder()
                          .username(joinReqDTO.getInputName())
                          .email(joinReqDTO.getInputEmail())
                          .password(joinReqDTO.getInputPw())
                          .joindate(LocalDate.now())
                          .build();
          
                  memberList.add(member);
                  return memberList;
              }
  5. html에서 fetch()를 사용하여 스크립트 함수를 작성한다.
    • Request만 있고 Response 값이 없을 때 예시
      •     const join = () => {
                const inputName = document.getElementById("inputName").value;
                const inputEmail = document.getElementById("inputEmail").value;
                const inputPw = document.getElementById("inputPw").value;
        
                fetch("/join", {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json"
                    },
                    body: JSON.stringify({
                        inputName: inputName,
                        inputPw: inputPw,
                        inputEmail: inputEmail
                    })
                })
                    .then(response => {
                        if (response.ok) {
                            window.location.href = "/";
                        } else {
                            console.error("회원가입 실패");
                        }
                    })
                    .catch(error => {
                        console.error("오류 발생: ", error);
                    })
            }​
        • 값을 보낼 때 input 태그에 있던 id의 값을 document.getElementById(”id값").value; 을 변수에 담는다.
        • 이후 위의 변수를 body에서 JSON 형태로 보낸다. {변수명: 변수명, ….}
        • 실행 후 경로 이동이 있다면 window.location.href = "경로"; 의 형태로 작성한다.
    • Request와 Response 값이 모두 있을 때 예시
      •     const login = () => {
                const inputName = document.getElementById("inputName").value;
                const inputPw = document.getElementById("inputPw").value;
        
                fetch("/", {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json"
                    },
                    body: JSON.stringify({
                        inputName: inputName,
                        inputPw: inputPw
                    })
                })
                    .then(response => response.json())
                    .then(data => {
                        document.getElementById("alert").innerText = data.message;
                    })
                    .catch(error => {
                        console.error('Error:', error);
                    });
            }
      • Request는 위의 방법과 같고, Reponse 값은 .then() 안에서 data(json)를 가져온 후, data.키이름 형식으로 값을 가져온다.
        • 예시
          • .then(data => {document.getElementById("alert").innerText = data.message;})
      • div 태그의 값을 Reponse한 값으로 변경하려면 document.getElementById("id명").innerText 로 변경한다.

 

 

정리를 마치면서..

 

내가 여태까지 배우고 이해한 것만으로 정리한 거라서 잘못된 부분이나, 더 좋은 방법이 있을 수 있다!

앞으로 RESTFUL의 대한 고민은 더 많아질 것같다..

모든 메소드를 REST API로 작성해야 할지, 아니면 REST API로 구현이 어려운 메소드는 Model로 작성해도 될지?

Model과 REST API 사용 메소드를 섞어서 개발해도 될지..

 

하지만 먼저 급한건 스크립트 함수 더 잘짜기...

Postman으로 확인하면 값이 잘 넘어가는데 Front 짤 때마다 익숙하지 않다..흐극

역시 풀스택은 어렵지만 난 둘 다 잘하고 싶으니까 JavaScript도 더 열심히 하기!