CloneInstargram 트러블 슈팅

이동규·2023년 7월 1일

1. 회원 가입 시 String 형식 검사 이슈

  • 회원 가입 시 client로 부터 받은 데이터를 SignUpRequestDto로 받아서 Service 로직으로 전달하는 과정 사이에 Dto 내부에서 @Validation Jpa 어노테이션을 사용하여 아래와 같이 코드를 구성하였다.
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SignUpRequestDto {
    @Pattern(regexp = "^[a-zA-Z0-9._]+$", message = "유효한 닉네임을 입력해주세요.")
    private String nickName;

    @Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", message = "유효한 이메일 주소를 입력해주세요.")
    @NotNull(message = "E-mail을 입력해주세요.")
    private String email;

    @Size(min = 8, max = 20, message = "비밀번호는 최소 8자에서 20자 사이로만 가능합니다.")
    @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?]).+$", message = "비밀번호는 대문자,소문자,숫자,특수문자만 가능합니다.")
    private String password;
}

하지만, dto 상에서 유효성 검사를 하는 과정에서 옳지 않는 형식일 경우 회원 가입이 허용되지 않는 기능하는 정상 작동하지만, 상태 코드는 200을 내보내면서 로그인 화면으로 넘어가는 이슈가 생겼다.

  • 해결책
    • 유효성 검사를 하는 로직을 Dto 상에서 구성하는 것이 아닌 비지니스 로직 상에서 dto로 받은 필드 값들을 받아 조건문 형식 검사를 한 후 예외 처리를 하도록 변경하였다. 이를 통해 400 에러 코드를 내보내도록 하여 로그인 페이지로 넘어가지 않도록 하는 정상 동작을 성공하였다.

      if(!Pattern.matches("^[a-zA-Z0-9._]+$", nickName)){
                  throw new CustomException(NOT_MATCH_NICKNAME);
              }
              Optional<Member> checkNickName = memberRepository.findByNickName(nickName);
              if (checkNickName.isPresent()) {
                  throw new CustomException(EXIST_NICKNAME);
              }
      
              if(!Pattern.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
      																																						email)){
                  throw new CustomException(NOT_MATCH_EMAIL);
              }
              Optional<Member> checkEmail = memberRepository.findByEmail(email);
              if (checkEmail.isPresent()) {
                  throw new CustomException(EXIST_EMAIL);
              }
      
              if(!Pattern.matches("^[a-zA-Z\p{Punct}0-9]*$", password)){
      					  throw new CustomException(NOT_MATCH_PASSWORD);
              }
              if (password.length() <= 8 || password.length() >= 20) {
                  throw new CustomException(INVALID_PASSWORD_LENGTH);
              }

해시 태그 기능 구현 중 영속성 이슈

  • 해시 태그 기능을 구현하는 과정에서 board 와 HashTag 를 ManyToMany 관계를 맺기 위해 중간 테이블 Tag_Board를 생성해 주었다. 이를 통해 만약 Tag_Board에 board와 HashTag가 존재하지 않는 다면 HashTag 에 태그 String을 저장하고 board와 HashTag의 관계를 Tag_Board에 저장하는 방식으로 구현을 하였다. 여기서 createBoard 메서드 상에서 board를 생성하고 생성 과정에서 적은 tag를 처리하는 로직을 board가 1차 캐시에 저장시키기 전에 처리를 하게 되어 영속성 이슈가 생기는 것을 확인 했다.
  • 해결책
    • board를 1차 캐시에 먼저 저장을 한 후 tag 처리 로직을 수행하도록 변경하였다.

      // JPA에서 관계를 맺고 있는 엔티티의 영속성 처리 문제를 위해
      // 먼저 board를 1차 캐시에 save를 보냄
              boardRepository.save(board);
      
              boardRequestDto.setHashtags(boardRequestDto.getHashtags());
      
              for(String hashTag : boardRequestDto.getHashtags()){
                  String hashTagString = hashTag.substring(1);
                  HashTag existHashTag = hashTagRepository.findByHashTag(hashTagString);
                  if(existHashTag != null){
                      Tag_Board tag_board = new Tag_Board(existHashTag, board);
                      tag_boardRepository.save(tag_board);
                  }else {
                      HashTag hashTagTable = new HashTag(hashTagString);
                      hashTagRepository.save(hashTagTable);
                      Tag_Board tag_board = new Tag_Board(hashTagTable, board);
                      tag_boardRepository.save(tag_board);
                  }
              }

새로 구현 해본 기능

  • Following/Follower 기능 구현
    • 이를 구현 시키기 위해 Follow Entity를 생성한 후 Follow 안에 Member와의 연관 관계 Column을 follower와 following 두가지를 처리하기 위해 2개를 선언해 주었다. 이를 통해 Member와 Follow 간의 OneToMany 연관 관계가 2개가 형성되도록 하여 구현을 하였다.
    • 처음에는 구글링을 통해 셀프 참조 형태를 써보려고 했지만 자기 자신도 팔로워 팔로잉 리스트에 들어가는 트러블이 생겨서 위와 같이 수정하였다. 그렇게 특정 유저가 팔로잉 또는 팔로워 수를 조회하는 과정에서도 정확한 수가 입력되는 것도 확인하였다.
      • 초기 셀프 참조

        @ManyToOne
            @JoinColumn
            private User userFollowing = this;
        
            @ManyToOne
            @JoinColumn
            private User userFollower = this;
        
            @OneToMany(mappedBy = "userFollowing")
            private List<User> followingList = new ArrayList<User>();
        
            @OneToMany(mappedBy = "userFollower")
            private List<User> followerList = new ArrayList<User>();
        
            public void addFollowing(User following) {
                this.followingList.add(following);
        
                if(!following.getFollowerList().contains(this)) {
                    following.getFollowerList().add(this);
                }
                //연관관계의 주인을 통한 확인
                if(!following.getUserFollower().getFollowerList().contains(this)) {
                    following.getUserFollower().getFollowerList().add(this);
                }
            }
            public void addFollower(User follower) {
                this.followerList.add(follower);
        
                if(follower.getFollowingList().contains(this)) {
                    follower.getFollowingList().add(this);
                }
                //연관관계의 주인을 통한 확인
                if(!follower.getUserFollowing().getFollowingList().contains(this)) {
                    follower.getUserFollowing().getFollowingList().add(this);
                }
            }
      • 수정 코드

        @Entity
        @Getter
        @NoArgsConstructor
        public class Follow {
            @Id
            @GeneratedValue(strategy = GenerationType.IDENTITY)
            private Long id;
        
            @ManyToOne(fetch = FetchType.LAZY)
            @JoinColumn(name = "FOLLOWING_ID", referencedColumnName = "id")
            private Member memberFollowing;
        
            @ManyToOne(fetch = FetchType.LAZY)
            @JsonIgnore
            @JoinColumn(name = "FOLLOWER_ID", referencedColumnName = "id")
            private Member memberFollower;
        
            public Follow(Member memberFollowing, Member memberFollower) {
                this.memberFollowing = memberFollowing;
                this.memberFollower = memberFollower;
            }
        }
        
        //Member Entity
        @Builder.Default
            @OneToMany(mappedBy = "memberFollowing", cascade = CascadeType.REMOVE)
            private List<Follow> followingList = new ArrayList<>();
        
            @Builder.Default
            @OneToMany(mappedBy = "memberFollower", cascade = CascadeType.REMOVE)
            private List<Follow> followerList = new ArrayList<>();
        
        //serivce Logic
        @Transactional
        	  public ResponseEntity<ResponseMsgDto<Void>> follow(String nickName,
        																														Member member) {
                Member followMember = memberRepository.findByNickName(nickName)
        																																	.orElseThrow(
                        () -> new CustomException(ErrorCode.NOT_FOUND_USER));
        
                Follow follow = new Follow(member, followMember);
        
                Follow existFollow = followRepository.existFollow(member.getNickName(),
        																											followMember.getNickName());
        
                if (existFollow != null) {
                    followRepository.delete(existFollow);
                    return ResponseEntity.ok(ResponseMsgDto.setSuccess(HttpStatus.OK.
        																										value(), "팔로우 취소", null));
                } else {
                    followRepository.save(follow);
                }
                return ResponseEntity.ok(ResponseMsgDto.setSuccess(HttpStatus.OK.
        																										value(), "팔로우 등록", null));
        
            }

시간이 남았더라면…

  • 좋아요 상태 유지 기능 캐싱 기법 적용
    • 캐시 솔루션은 자주 사용되면서 자주 변경되지 않는 데이터의 경우 캐시 서버에 적용하여 반영할 경우 높은 성능 향상을 이뤄낼 수 있는 특징을 활용해서 좋아요 기능은 자주 사용되는 데이터에 해당하기 때문에 접목이 가능할 것이라고 생각
    • 좋아요 기능은 일단 사용자와 글의 정보 중 중요한 내용이 아니므로 캐시에 저장되는 데이터로 적합하다고 생각
    • Read Through + Write Through 방식을 사용하여 캐시 솔루션이 장애가 발생했을 경우 적절한 대응방안을 구성할 수 있다. 예를 들어 Read Through + Write Through 방식은 데이터를 쓸때는 항상 캐시에서 DB로 보내므로, 캐시에서 장애가 발생해 데이터가 날라갈 때도, DB에 저장되어 있기 때문에 데이터 정합성이 보장할 수 있다. 또한, write through는 캐시를 거쳐 DB로 저장이 되므로, 항상 최신 정보를 가지고 있다.
    • 캐시를 구성하는 목적은 빠른 성능 확보와 데이터 전달에 있으며, 데이터의 영속성을 보장하기 위함이 아니라는 점을 알기에 좋아요 유지 기능에 쓰기에 적합하다고 생각한다.
profile
진짜 개발자가 되고 싶다

0개의 댓글