네이버 클라우드 캠프/Spring Boot & React

[SpringBoot & React] 회원가입과 로그인(1)

graph-dev 2023. 7. 29. 15:41
728x90

boot and react logo

 

로그인과 회원가입은 일상에서 자주 보는 화면이자 기능입니다. SpringBoot와 React 어플리케이션으로 간단히 구현해보겠습니다. 완성할 화면은 아래와 같습니다.

회원가입과 로그인

 

사전 작업

먼저 JWT에 대해 정리한 글을 잠시 보고 오겠습니다.

https://graph-dev.tistory.com/64

 

[React] JWT와 CORS

JWT JWT는 JSON Web Token(JSON 웹 토큰) 의 약자로, 정보를 JSON 형식으로 안전하게 전송하는 방법입니다. 이 정보는 토큰 자체에 포함된 플레임 기반의 토큰이며, 일반적으로 인증과 권한 처리에서 주로

graph-dev.tistory.com

 

회원가입

Controller

 Boot에서 사용할 api를 활용한 회원 컨트롤러(controller)를 만들겠습니다. 회원을 대상으로 하므로 MemberController로 만들었습니다. 이 컨트롤러는 entity와 DTO를 통해 회원데이터를 받아와서 memberService에 있는 기능으로 데이터를 주고받을 수 있습니다. 아래는 memberController에서 사용할 서비스, JWT Provider, 비밀번호 암호화 모듈을 가져오는 코드입니다.

@RestController
@RequestMapping("/api/member")
public class MemberController {
    private final MemberService memberService;
    private final JwtTokenProvider jwtTokenProvider;
    private final PasswordEncoder passwordEncoder;

    public MemberController(MemberService memberService, JwtTokenProvider jwtTokenProvider,
                            PasswordEncoder passwordEncoder) {
        this.memberService = memberService;
        this.jwtTokenProvider = jwtTokenProvider;
        this.passwordEncoder = passwordEncoder;
    }

이를 활용해서, 회원가입시 아이디, 비밀번호를 입력하면 회원으로 등록이 되어 데이터베이스에 추가되도록 코드를 작성합니다.

@PostMapping("/join")
    public ResponseEntity<?> join(@RequestBody Member member) {
        ResponseDTO<MemberDTO> response = new ResponseDTO<>();

        try {
            //비밀번호 암호화
            member.setPassword(passwordEncoder.encode(member.getPassword()));
            //회원가입(MemberEntity 리턴하도록)
            Member joinMember = memberService.join(member);
            //DTO로 변환(비밀번호는 "")
            joinMember.setPassword("");
            MemberDTO memberDTO = joinMember.toMemberDTO();
            //ResponsEntity에 DTO 담아서 리턴
            response.setItem(memberDTO);
            response.setStatusCode(HttpStatus.OK.value());

            return ResponseEntity.ok().body(response);
        } catch (Exception e) {
            response.setErrorMessage(e.getMessage());
            response.setStatusCode(HttpStatus.BAD_REQUEST.value());
            return ResponseEntity.badRequest().body(response);
        }
    }

 

 이제 비밀번호를 암호화하고, memberService에 있는 join 메서드로 등록이 됩니다. 등록된 후에는 비밀번호를 다시 빈문자로 초기화하여 노출되지 않도록 방지합니다. 그 다음에 memberDTO로 리턴하고, response 객체에는 memberDTO를 담아서 반환하도록 합니다. 주의할 점은 try ~ catch 메서드를 통한 예외처리를 반드시 해줘야하는 점입니다.

 

딴소리: try~catch 예외처리

 앞으로 이러한 try~catch문을 많이 보게될 것입니다. 그래서 어느정도 정리할 필요가 있어 가져왔습니다.

 try에서는 DTO를 담아서 반환하는데 response값이 온전히 전달되면 올바른 요청을 제공해야합니다. 올바른 요청은 코드 200번을 사용하며, 이러한 OK 상태를 확인해서 Item값에 memberDTO을 넣습니다. 코드는 200번이 나오게 되겠습니다. 반환값은 response에 담아줍니다. 반면, error가 발생하면 catch에서 이를 잡아주고, 에러 메시지를 출력하고 BAD_REQUEST 값이 나올 것입니다. 반환 값은 동일하게 response를 활용합니다.

public ResponseEntity<?> join(...) {
        ResponseDTO<MemberDTO> response = new ResponseDTO<>();

        try {
            //...
            //ResponsEntity에 DTO 담아서 리턴
            response.setItem(memberDTO);
            response.setStatusCode(HttpStatus.OK.value());

            return ResponseEntity.ok().body(response);
        } catch (Exception e) {
            response.setErrorMessage(e.getMessage());
            response.setStatusCode(HttpStatus.BAD_REQUEST.value());
            return ResponseEntity.badRequest().body(response);
        }

 

Service

memberService를 보여줍니다. 서비스는 interface 형식으로 작성하며, 실제 구현부는 implement 클래스 파일에서 작성합니다. 인터페이스는 아래와 같습니다.

public interface MemberService {
    Member join(Member member);
}

 

구현부는 다음과 같습니다. MemberService 인터페이스를 implemetns 명령어로 호출하면, 이제 @Override 어노테이션에 따라 join 메서드를 불러와서 재정의할 수 있습니다. 

@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {
    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;


    @Override
    public Member join(Member member) {
		//...
        return memberRepository.save(member);
    }

유효성 검사를 수행하여 사용자 이름을 입력했는지 먼저 파악하고, 없으면 예외처리를 합니다. 그 다음에 입력했다면 기존에 있는 사용자 이름과 중복되는지 한번 더 검사를 해줍니다. 이 모든 조건을 통과하면 Repository.save() 메서드로 회원으로 데이터베이스에 등록이되게 만듭니다.

//유효성 검사
if(member == null || member.getUsername() == null) {
    throw new RuntimeException("Invalid Argument");
}

//username 중복체크
if(memberRepository.existsByUsername(member.getUsername())) {
    throw new RuntimeException("already exist username");
}

 

Repository

그러면 유효성 검사에서 Repository가 필요하다는 사실을 알 수 있습니다. Repository를 잠깐 살펴보면, 인터페이스로 만들고 JpaRepository를 상속받습니다. 이 때 객체로는 Member 엔티티와 엔티티의 primary key값의 타입인 Long이 들어갑니다.

public interface MemberRepository extends JpaRepository<Member, Long> {

}

 

Entity

엔티티를 살펴보겠습니다. 데이터베이스의 테이블 구조를 여기서 정의합니다. 먼저 테이블은 T_MEMBER 이름을 주고, username은 UK(유일키)로 지정하겠습니다. 그리고 SequenceGenerator를 통해 회원이 추가될때마다 자동으로 번호가 1부터 하나씩 올라가도록 설정합니다.

@Entity
@Table(name="T_MEMBER",
        //username UK로 지정
        uniqueConstraints = {@UniqueConstraint(columnNames = "username")}
      )
@SequenceGenerator(
        name="MemberSeqGenerator",
        sequenceName = "T_MEMBER_SEQ",
        initialValue = 1,
        allocationSize = 1
)

 

이후 각 컬럼별로 정의를 해보겠습니다. Data, NoArgs, AllArgs, Builder 어노테이션을 설정합니다. 이 어노테이션은 lombok 패키지에서 호출합니다. id, username, password, role 네가지 컬럼을 지정합니다.

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member {
    @Id
    @GeneratedValue(
            strategy = GenerationType.SEQUENCE,
            generator = "MemberSeqGenerator"
    )
    private long id;
    @Column(nullable = false)
    private String username;
    @Column(nullable = false)
    private String password;
    @ColumnDefault("'ROLE_USER'")
    private String role;

    public MemberDTO toMemberDTO() {
		//...
    }
}

 

그리고 엔티티에서 DTO로 쉽게 이동할 수 있도록 toMemberDTO 메서드를 만들어줍니다. id, username, passwrod, role 컬럼들을 모두 담아서 build해준 값을 DTO로 보낼 수 있습니다.

public MemberDTO toMemberDTO() {
    return MemberDTO.builder()
            .id(this.id)
            .username(this.username)
            .password(this.password)
            .role(this.role)
            .build();
}

 

로그인부터는 길어지므로 다음 글에서 정리해보겠습니다.