#5 회원 rud 개발

seojin's 개발블로그·2023년 8월 3일
0

영화 사이트 제작

목록 보기
5/19

오늘의 작업 목록:
1. 회원 정보 단건조회(내 정보 확인)
2. 회원 정보 수정(비밀번호, 휴대폰 번호)

📌 회원 정보 단건조회(내 정보 확인)

저번의 회원가입에 이어 이번엔 회원정보 조회 api를 만들어봤다.

1. 시퀀스 다이어그램


먼저 시퀀스 다이어그램을 먼저 그려주었다.
로그인된 유저의 Id값을 전송하는것을 가정하였고 로그인된 유저이지만 내 정보 조회시 오류가 생기는 상황을 가정해 조회 실패시의 예외처리도 구상을 했다.

2. API 명세

response로는 성공, 실패 여부 코드와 메세지 그리고 데이터를 반환하기로 했다.

3. 구현

I. Controller

@GetMapping("/api/member/getMyInfo/{memberId}")
    public ResponseEntity<ApiResponse<Map<String, Object>>> getMyInfo(@PathVariable Long memberId) {
        try {
            Member foundMember = memberService.findById(memberId);
            Map<String, Object> responseData = new HashMap<>();
            responseData.put("member", foundMember);
            return ResponseEntity.ok().body(new ApiResponse<>(1, "회원 정보 조회 성공", responseData));
        } catch (NoSuchMemberException ex) {
            Map<String, Object> errorData = new HashMap<>();
            errorData.put("errCode", "member_not_found");
            errorData.put("errMsg", ex.getMessage());
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ApiResponse<>(0, "회원 정보 조회 실패", errorData));
        }
    }

반환은 공통으로 이용하는 ApiResponse를 이용하였고 service에서 데이터를
정상적으로 반환하느냐 예외를 반환하느냐에 따라 리턴값이 달라지게 구현을 했다.
아직 메세지, 데이터를 더 짧은 코드로 만드는 법을 찾는중이다,,,,

II. Service

public Member findById(Long id) {
        return memberRepository.findById(id)
                .orElseThrow(() -> new NoSuchMemberException("Member not found with ID: " + id));
}

단순히 레포지토리에 findId를 날리고 결과에 따라 예외처리가 생기는 코드
컨트롤러와 마찬가지로 더 좋은 코드를 짜고 싶어서 여러 코드를 참고하는중이다.
필요하다면 후에 리팩토링 예정

4. 테스트

I. 실패 테스트

존재하지 않는 id를 입력했을때의 테스트
실패 메세지가 전송이된다.

II. 성공 테스트

존재하는 회원Id를 넣을시 출력되는 값 조회 성공메세지와 회원의 정보가 전송된다.

📌 회원 정보 수정(비밀번호, 휴대폰 번호)

1. 시퀀스 다이어그램


로그인된 유저의 ID와 수정을 원하는 값을 전송하여 변경을 하는 과정을 그렸고
비밀번호, 휴대폰 번호 둘다 현재는 알고리즘이 다르지 않다고 생각하여 회원정보 변경이라는 시퀀스 다이어그램으로 작성을 하였다.

2. API 명세

현재는 둘 다 파라미터, api이름등만 다르다. 후에 security가 추가되면 변경의 여지가 있다.

3. 구현

I. Controller

@PatchMapping("/api/member/ModifyMyPassword/{memberId}")
public ResponseEntity<ApiResponse<Map<String, Object>>> modifyMyPassword(@PathVariable Long memberId,
                                                                             @RequestBody @Valid ModifyPasswordRequestDto requestDto,
                                                                             Errors errors){

        if (errors.hasErrors()) {
            return ResponseEntity.badRequest().body(new ApiResponse<>(0, "비밀번호 형식이 올바르지 않습니다.", null));
        }

        try {
            Member modifiedMember = memberService.modifyPassword(memberId, requestDto);
            Map<String, Object> responseData = new HashMap<>();
            responseData.put("member", modifiedMember);
            return ResponseEntity.ok().body(new ApiResponse<>(1, "비밀번호 변경 성공", responseData));
        } catch (NoSuchMemberException ex) {
            Map<String, Object> errorData = new HashMap<>();
            errorData.put("errCode", "member_not_found");
            errorData.put("errMsg", ex.getMessage());
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ApiResponse<>(0, "회원 정보 조회 실패", errorData));
        }

    }


    @PatchMapping("/api/member/ModifyMyPhoneNumber/{memberId}")
    public ResponseEntity<ApiResponse<Map<String, Object>>> modifyMyPhoneNumber(@PathVariable Long memberId,
                                                                             @RequestBody @Valid ModifyPhoneNumberDto requestDto,
                                                                             Errors errors){
        if (errors.hasErrors()) {
            return ResponseEntity.badRequest().body(new ApiResponse<>(0, "휴대폰 번호 형식이 올바르지 않습니다.", null));
        }

        try {
            Member modifiedMember = memberService.modifyPhoneNumber(memberId, requestDto);
            Map<String, Object> responseData = new HashMap<>();
            responseData.put("member", modifiedMember);
            return ResponseEntity.ok().body(new ApiResponse<>(1, "휴대폰 번호 변경 성공", responseData));
        } catch (NoSuchMemberException ex) {
            Map<String, Object> errorData = new HashMap<>();
            errorData.put("errCode", "member_not_found");
            errorData.put("errMsg", ex.getMessage());
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ApiResponse<>(0, "회원 정보 조회 실패", errorData));
        }
    }

두 코드가 유사하다 정보 수정을 위해 데이터를 dto로 받고 데이터의 형식을 검증한 뒤 정보 수정에 들어가는 코드이다.

검증의 경우에는 아래의 dto 설정들을 기준으로 한다.

@AllArgsConstructor
@NoArgsConstructor
@Getter
public class ModifyPasswordRequestDto {

    @NotBlank(message = "비밀번호를 입력해주세요.")
    @Size(min = 6, max = 20, message = "비밀번호는 6자 이상 20자 이하여야합니다.")
    private String password;
}
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class ModifyPhoneNumberDto {
    @NotBlank(message = "전화번호를 입력해주세요.")
    @Pattern(regexp = "^\\d{3}-\\d{3,4}-\\d{4}$", message = "올바른 휴대폰 번호 형식이 아닙니다. (예: 010-1234-5678)")
    private String phoneNumber;
}

Validator를 이용해 입력되는 값에 대한 제약을 만들어두었다.

II. Service

@Transactional
public Member modifyPassword(Long id, ModifyPasswordRequestDto requestDto){
        Member member = memberRepository.findById(id).
                orElseThrow(() -> new NoSuchMemberException("Memeber not found with ID:" + id));

        member.updatePassword(requestDto.getPassword());

        return member;
    }

@Transactional
public Member modifyPhoneNumber(Long id, ModifyPhoneNumberDto requestDto){
        Member member = memberRepository.findById(id).
                orElseThrow(() -> new NoSuchMemberException("Memeber not found with ID:" + id));

        member.updatePhoneNumber(requestDto.getPhoneNumber());

        return member;
    }

@Transactional 어노테이션을 통해 오류가 생길시의 원자성을 보장해주었고
findById로 데이터를 조회하고 있다면 해당 객체를 생성후 객체의 정보를 수정하고 해당 객체를 반환하여준다.
update의 경우 @Transactional 어노테이션의 영향으로 작업 완료시 값을 자동으로 커밋하여줘서 따로 저장하는 코드는 구현하지 않았다.

4. 테스트

I. 실패 테스트

validtor 설정대로 비밀번호가 6자리 이상 20자리 미만이 아니라면 예외가 발생한다. 휴대폰 번호도 마찬가지로 3 - 3,4 - 4 자릿수가 맞지않을시 에러가 발생한다.

II. 성공 테스트

정상값이 입력되었을때의 결과들 code 1 과 성공메세지가 전송된다. 데이터베이스에도 정상적으로 반영이 되었다.

📌 발생했던 문제

1. 내 정보 조회 api 호출시 406 에러가 발생

-> 에러 캡처 이미지를 실수로 모두 삭제했다 ㅠ

  • 포스트맨에서 api를 호출했으나 406 에러가 발생
  • spring 실행기록에서는 json타입의 에러라고 안내

406 에러란?
처음보는 에러라서 당황스러웠다
내용을 보면 클라이언트에서 허용되는 값과 서버에서 제공하는 값이 호환이 안되어 발생하는 에러인데 직감적으로 평범한 ResponseEntity가 아니라서 생기는 문제라는 생각이 들어서

ResponseEntity<ApiResponse<Map<String, Object>>> 이 부분을
ResponseEntity<Member>로 수정을 하였다.

수정후 코드가 정상 작동되는것을 확인하고 다른 계층문제가 아닌 반환 타입의 문제라는것을 확인했으나 어제 회원가입 기능에서는 정상 동작하던 ApiResponse타입에서 에러가 생기는 이유가 의문이었다.
내가 잘못 아는 부분이 있는걸까? 해서 남들은 어떻게 메세지를 반환하는지를 조사하다가 해결법을 발견했다.
RestApi 만들기 (2) JSON 형식 리턴

@Getter
@Setter
public class ApiResponse<T> {

어제 실수로 setter를 지워었나보다,,, setter 어노테이션이 없어서
메세지의 내용이 기록이되지 않아 발생하는 에러였고 어노테이션을 달아주자 바로 해결이 되었다.

2. password를 null로 받아오는 에러

비밀번호 변경 api에서 발생한 에러
비밀번호 변경 api로 password값을 넣어주었는데 500번 에러가 발생했고

실행 기록에 다음과 같은 에러가 남아 있었다.
어제도 발생했던 에러,,,,
또 @RequestBody 어노테이션을 빼먹어서 dto로 값을 받지 못하는것이 원인이었다. 어제도 겪었던 에러라 번뜩하고 바로 해결을 하였다.

3. password 입력값 검증에러

6자리 미만이라 실패해야 하는데 변경을 성공했다고 한다.
왜지?

@NotBlank(message = "비밀번호를 입력해주세요.")
@Size(min = 6, max = 20, message = "비밀번호는 6자 이상 20자 이하여야합니다.")
private String password;

dto의 내용 가이드대로 작성을 했는데 에러가 발생하니 내가 이해를 잘못했다고 생각을하고 Difference Between @NotNull, @NotEmpty, and @NotBlank 참고했던 자료와 다른 블로그들을 확인한 결과
dto에 문제가 있는것이 아니라 @Valid와 @Validated를 이용한 유효성 검증

modifyMyPhoneNumber(@PathVariable Long memberId,
                    @RequestBody @Valid ModifyPhoneNumberDto requestDto                      Errors errors){

@RequestBody뒤에 @valid 어노테이션을 붙이지 않아서 였다.
valid 어노테이션을 붙여야 dto내부에 valid 옵션이 붙은 값들에 대한 유효성 검증이 가능해지는데 내가 이 개념을 까먹고 있었다.
어노테이션을 붙이자 바로 해결이 되었다.

📌 후기

사실 회원탈퇴 기능까지 만들었지만 후에 데이터베이스에 트리거를 사용한 백업을 구현후 포스팅을 하려고 한다.

그리고 이번에 발생한 오류들은 거의다 어노테이션을 빼먹거나 지워서 생긴 에러들이었는데 내가 가이드나 참고자료를 볼 때 정독을 하지 않아서 생긴 문제라고 생각한다.

다행히 참고자료 링크를 모두 저장해놔서 금방 다시 찾고 해결할 수 있었다.
이전처럼 주먹구구식으로 개발하던 버릇이 많이 고쳐진것 같다.

내일은 더 잘해야겠다.

profile
개발 공부하는 블로그

0개의 댓글