Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags more
Archives
Today
Total
관리 메뉴

작은 지식주머니

개인 토이 프로젝트 (2) JPA, JWT를 이용한 간단한 RESTful 백엔드 API만들어보기 본문

spring

개인 토이 프로젝트 (2) JPA, JWT를 이용한 간단한 RESTful 백엔드 API만들어보기

작지 2022. 2. 20. 14:51

개인 토이 프로젝트입니다. 만약 초보자분들이시라면 따라하지마세요. 분명 하자가 있습니다.

 

 

 

 

저번 글에 이어 Repository패키지를 만들고 그 안에 

JpaDataSpring을 사용하여 MemberRepository를 만들었습니다.

 

필요한 메서드를 적당히 만들고 대부분 간단한 쿼리는 전부 Jpa데이터가 만들어주므로 사용하면 됩니다.

package com.todo.todoP.Repository;

import com.todo.todoP.Entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface MemberRepository extends JpaRepository<Member,Long> {

    List<Member> findByName(String name);
    List<Member> findAll();
	
    Boolean existsByEmail(String email);
    
    Member findMemberByName(String name);
    Member findMemberById(Long id);
    Member findMemberByEmail(String email);

}

 

또한 해당 서비스를 구현할 MemberService또한 만들어야합니다.

 

package com.todo.todoP.Service;

import com.todo.todoP.DTO.Member.MemberDTO;
import com.todo.todoP.DTO.Basic.ResponseDTO;
import com.todo.todoP.Entity.Member;
import com.todo.todoP.Repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Transactional
public class MemberService {

    private final MemberRepository memberRepository;

    public List<Member> save(Member member){
        if(member == null || member.getEmail() == null){
            throw new RuntimeException("Invalid arguments");
        }

        if (memberRepository.existsByEmail(member.getEmail())){
            throw new RuntimeException("Email already exists");
        }

        memberRepository.save(member);
        return memberRepository.findByName(member.getName());
    }

    public List<MemberDTO> update(Member member){
        validation(member);
        Optional<Member> original = memberRepository.findById(member.getId());

        original.ifPresent(m ->{
            if (member.getName() != null){
                m.setName(member.getName());
            }
            if (member.getEmail() != null){
                m.setEmail(member.getEmail());
            }
            if (member.getPassword() != null){
                m.setPassword(member.getPassword());
            }

            memberRepository.save(m);
        });

        return original.stream().map(MemberDTO::new).collect(Collectors.toList());

    }
	
    //수정 해야함.
    public void remove(Member member){
        memberRepository.delete(member);
    }
	
    public ResponseDTO<MemberDTO> findAll(){
        List<MemberDTO> members = findAllDTO();
        return ResponseDTO.<MemberDTO>builder().data(members).build();
    }

    //편의 메서드
    public List<MemberDTO> findAllDTO(){
        List<Member> allMembers = memberRepository.findAll();
        return allMembers.stream()
                .map(MemberDTO::new)
                .collect(Collectors.toList());
    }

    //==증명 메서드==//
    private void validation(Member member){
        if (member == null){
            throw new RuntimeException("Entity cannot be null");
        }

        if (member.getName() == null){
            throw new RuntimeException("Unknown user");
        }
    }
	
    private void existsMember(Member member) {
        if(member == null || member.getEmail() == null){
            throw new RuntimeException("Invalid arguments");
        }

        if (memberRepository.existsByEmail(member.getEmail())){
            throw new RuntimeException("Email already exists");
        }
    }
    
}

 

너무 난잡하니 save부터 보겠습니다.

 

아주 간단한 증명 로직과 증명이 끝난 후에는

memberRepository.save(member)로 EntityManager에서 Persist를 날려줍니다.

그 다음 저희는 Member의 내용물을 읽어 표시해야하기 떄문에 List로 받아왔습니다.

 

public List<Member> save(Member member){
        existsMember(member);
        memberRepository.save(member);
        return memberRepository.findByName(member.getName());
    }

private void existsMember(Member member) {
        if(member == null || member.getEmail() == null){
            throw new RuntimeException("Invalid arguments");
        }

        if (memberRepository.existsByEmail(member.getEmail())){
            throw new RuntimeException("Email already exists");
        }
    }

 

해당 Service를 RESTController로 확인해보겠습니다.

 

api라는 컨트롤러를 만들었고 

 

import com.todo.todoP.DTO.Member.MemberCreateDTO;
import com.todo.todoP.DTO.Member.MemberDTO;
import com.todo.todoP.DTO.Basic.ResponseDTO;
import com.todo.todoP.DTO.Team.TeamDTO;
import com.todo.todoP.Entity.Member;
import com.todo.todoP.Security.TokenProvider;
import com.todo.todoP.Service.MemberService;
import com.todo.todoP.Service.TeamService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequiredArgsConstructor
public class TestApi {

    private final MemberService memberService;

    @PostMapping("/api/signup")
    public ResponseEntity<?> createMember(@Valid @RequestBody MemberCreateDTO memberDTO){
        try {
            Member entity = MemberCreateDTO.toEntity(memberDTO);
            List<Member> member = memberService.save(entity);

            List<MemberDTO> dto = member.stream().map(MemberDTO::new)
                    .collect(Collectors.toList());

            ResponseDTO<MemberDTO> response = ResponseDTO.<MemberDTO>builder()
                    .data(dto).build();
            return ResponseEntity.ok().body(response);
        } catch (Exception e){

            String error = e.getMessage();
            ResponseDTO<MemberDTO> response = ResponseDTO.<MemberDTO>builder().etc(error).build();
            return ResponseEntity.badRequest().body(response);
        }
    }
}

 

굉장히 컨트롤러에서 난잡하게 적혀있는데 처음부터 확인해보겠습니다.

 

@RequestBody부터 보겠습니다. request의 body에서 오브젝트 형태의 데이터를 받아올 떄 사용하는 어노테이션입니다.

 

해당 어노테이션에서는 MemberCreateDTO를 사용하는데 왜 Member Entity를 그대로 사용하지 않는가 라고 한다면

Entitiy는 절대로 바깥으로 노출되서는 안되기 때문에 캡슐화를 시키기 위함입니다.

 

또한 두번쨰 이유로는 Entitiy가 가지고 있지 않은 정보를 담기위한 그릇이라고 생각하시면 좋겠습니다.

 

그래서 MemberCreateDTO에는 뭐가 담겨져 있는가 한다면.

package com.todo.todoP.DTO.Member;

import com.todo.todoP.Entity.Member;
import lombok.Data;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Data
public class MemberCreateDTO {
	
    @NotBlank(message = "Check your name")
    @Length(max = 20)
    private String name;

    @NotBlank(message = "Check your Email")
    private String email;

    @NotBlank(message = "Check your Password")
    private String password;


    public static Member toEntity(final MemberCreateDTO dto){
        return new Member(dto.getName()
                ,dto.getPassword())
                ,dto.getEmail());
    }
}

오브젝트로는 이름, 메일, 패스워드를 받아오도록 세가지만 설정해두었습니다.

 

또한 @NotBlank 어노테이션이 들어가있는데
"", " ", null 을 전부 거부하는 어노테이션입니다.

@NotNull, @NotEmpty도 있으니 적절히 넣으면 됩니다.

 

다시 컨트롤러로 돌아가 Entity로 변환된 오브텍트는 다시한번 MemberDTO로 변환됩니다.

왜 이런 번거로운 짓을? 이라고 물으실 수 있습니다.

제가 이렇게 한 이유는 MemberEntity 안에 있는 Team 컬렉션에 대한 문제가 존재했기 떄문입니다.

 

우선 MemberDTO를 봐주십시요

package com.todo.todoP.DTO.Member;

import com.todo.todoP.Entity.Member;
import lombok.*;

import java.util.List;
import java.util.stream.Collectors;

@Data
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class MemberDTO {

    private String token;
    private Long id;
    private String name;
    private String email;
    private String password;
    private List<TeamsDTO> team;

    public MemberDTO(Member member){
        id = member.getId();
        name = member.getName();
        email = member.getEmail();
        password = member.getPassword();
        team = member.getParent().stream()
                .map(TeamsDTO::new)
                .collect(Collectors.toList());
    }

}

 

가장 중요하게 봐야하는 부분은 team입니다.

해당 dto에서 Member_Team_Parent Entity를 가져오는것이 아니라 TeamsDTO로 변환하여 가져오는 방식을 취했습니다.

 

package com.todo.todoP.DTO.Member;

import com.todo.todoP.Entity.Member_Team_Parent;
import lombok.Data;

@Data
public class TeamsDTO {

    private String title;
    private Long team_id;

    public TeamsDTO(Member_Team_Parent parent){
        title = parent.getTeam().getTitle();
        team_id = parent.getTeam().getId();
    }

}

 

해당 DTO를 걸쳐 JPA의 지연로딩으로 하나씩 가져오는 방법을 사용하였습니다.

(MEMBER -> PARENT -> TEAM) 으로 총 세번을 걸쳐 데이터를 불러옵니다.

 

여기서 잘 아시는 분들은 그렇게 된다면 N+1 문제가 발생하는 것이 아니냐 라는 의문점이 나오실 텐데

fetch join을 하는게 좋지 않느냐? 라고 생각하실수 있지만 

 

해당 문제에서는 저는 apllication.yml에서 default_batch_fetch_size 설정을 해 둠으로서 해결을 하였습니다.

이 설정을 한다면 쿼리에서 자동으로 IN절을 100개 포함을 하기 떄문에 쿼리가 1+1이라는문제가 발생하지만

N+1보다는 쾌적하게 돌아가기 떄문에 이 방법을 사용하였습니다.

default_batch_fetch_size: 100

 

 

또 다시;;; RESTController를 확인하겠습니다.

 

ResponseDTO라는 객체가 담겨져있는데 이 놈이 무슨 DTO를 3개나쓰냐? 라고 생각할 수 있습니다.

저도 좀 애매한 부분이지만 배열 그대로를 노출하는것은 좋지 않을것이라 판단하였습니다.

 

 

ResponseDTO<MemberDTO> response = ResponseDTO.<MemberDTO>builder()
        .data(dto).build();
package com.todo.todoP.DTO.Basic;

import lombok.*;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
public class ResponseDTO<T> {

    private String etc;
    private List<T> data;

}

 

주 내용을 담을 DATA와, 나머지 ERROR등을 담을 ETC객체를 만들었습니다.

제가 저장한 객체를 DATA에 담아 ResponseEntity에 담아 보내보겠습니다.

 

또한 에러가 발생한다면 etc에 에러 메세지를 담아 badRequest를 담아 보내겠습니다.

 

return ResponseEntity.ok().body(response);

return ResponseEntity.badRequest().body(response);

 

POSTMAN을 실행하여 테스트 해보았습니다.

 

Body에서 raw JSON 문자열로 DTO형식에 맞춰 내용을 보내보겠습니다.

 

비밀번호는 현제는 해시코드로 변환이 되는 상태이므로 무시하셔도 좋습니다.

 

그런데 @NotBlank를 설정하고 나서 null값을 보낸다면 에러가 나오게 되는데 이 떄

ResponseDTO에 제대로 담겨서 나오지 않는 사태가 일어납니다.

 

 

생각가 다르게 나오는 모습인데

이럴때에는 validationException을 따로 관리하는 클래스를 만들어 관리를 하는 편이 좋다고 저는 판단했습니다.

 

다만 이 방법보다 extends를 사용하지않고 만드는 방법이 있으니 따로 방법을 구해보는것이 좋을 거라 생각합니다.

package com.todo.todoP.Exception;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@ControllerAdvice
public class ValidationAdvisor extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", new Date());
        body.put("status", status.value());

        List<String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(e-> e.getDefaultMessage())
                .collect(Collectors.toList());

        body.put("errors",errors);

        return new ResponseEntity<>(body, headers, status);
    }
}

다시 null값을 입력해 postman에 전달하면?

 

저희가 생각한 에러 메세지가 나오게 됩니다.

 

이렇게 SIGNUP에 대한 설명이 끝났는데 내용이 쓸데없이 길어져 다음 글에서 이어가겠습니다.

Comments