본문 바로가기
개발서/ToyProject-Smart

[Toy - Smart] Member 리팩토링

by 사서T 2023. 2. 24.

Member 살펴보기

Member는 회원 정보를 저장하기 위한 클래스이다. 특징은 다음과 같다.

 

- Entity 객체이며 Order와 연관관계를 가지고 있다.

- 매개변수가 없는 생성자를 가진다.

- updateMember, updateMemberPassword 메서드가 구현되어 있다.

 

@Entity
@Getter
@NoArgsConstructor
@ToString
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long memberId;
    private String nickName;
    private String email;
    private String password;
    private String phoneNumber;
    private LocalDate birthday;
    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();

    @Builder
    public Member(String nickName, String email, String password, String phoneNumber, LocalDate birthday) {
        this.nickName = nickName;
        this.email = email;
        this.password = password;
        this.phoneNumber = phoneNumber;
        this.birthday = birthday;
    }

    public void updateMember(MemberUpdateDto dto) {
        this.nickName = dto.getNickName();
        this.email = dto.getEmail();
        this.phoneNumber = dto.getPhoneNumber();
        this.birthday = dto.getBirthday();
        updateMemberPassword(dto.getPassword());
    }

    public void updateMemberPassword(String password) {
        if (!password.isEmpty())
            this.password = password;
    }
}

Member 분석하기

1. Anntation

 

- @Entity

기능 : 데이터 모델링의 객체로 DB 테이블 생성시 해당 클래스를 참조한다.

분석 : 해당 클래스는 Entity 객체로 사용하기 위해 구현되었기 때문에 적합하다.

 

- @Getter

기능 : 클래스에 선언된 필드에 대해 get 메서드를 자동 생성한다.

분석 : Entity 클래스의 경우 필드에 대한 접근 제한을 위해 private로 선언되었다. 따라서 필드 값에 접근하기 위한 별도의 방법이 필요하며 @Getter를 사용하는 방식을 채택하였다.

 

- @NoArgsConstructor

기능 : 매개변수가 없는 생성자를 자동 생성한다.

분석 : 클래스에서 따로 생성자를 정의하지 않은 경우 디폴트 생성자가 만들어진다. 따라서 굳이 선언할 필요가 없다. 하지만 JPA에서 필요로 하는 기본 생성자를 위해 선언한 경우 access 옵션을 PROTECTED로 설정하여 해당 Entity의 무분별한 생성을 방지할 수 있다.

 

- @ToString

기능 : 클래스에 선언된 필드를 '변수명=값' 형태의 문자열로 출력한다.

분석 : 해당 Annotation은 Test를 위해서만 사용되고 있다. 빌드시 파일의 용량을 굳이 키울 필요가 없기 때문에 Test에서 해당 기능을 대체할 수 있는 메서드를 구현하고 사용하는 것이 더 적합해 보인다.

 

- @Id

기능 : 해당 필드를 테이블의 기본키로 사용한다.

분석 : 기본키는 테이블 내에서 데이터를 구분할 수 있는 유일한 값으로 필수적으로 정의되어야 한다.

 

- @GeneratedValue(strategy = GenerationType.IDENTITY)

기능 : 기본키 생성 방식을 결정하는 Annotation으로 현재 IDENTITY 방식을 사용하고 있다.

분석 : IDENTITY 방식은 Entity를 DB에 저장 후 기본키를 생성하는 방식으로 트랜잭션의 쓰기 지연 기능을 사용할 수 없다. 즉, 영속성 컨텍스트로 등록하기 위해선 insert가 실행되어야 한다. SEQUENCE 방식은 시퀀스를 미리 메모리에 할당하고 DB에 접근없이 할당된 시퀀스를 기본키로 할당하는 방식이다. 이 경우 쓰기 지연 기능을 사용할 수 있지만 메모리를 차지한다는 단점이 존재한다. 각 방식마다 장단점이 존재하며 현재 서비스 상태에선 크게 고려할 점은 아니라고 판단하여 AUTO로 전략을 수정해두는 것이 적합하다고 판단한다.

 

- @OneToMany(mappedBy = "member") - order

기능 :

  • 연관관계를 설정하여 repository를 통할 필요없이 해당 객체에 접근하는 것으로 데이터를 조회할 수 있다. EAGER로 설정한 경우 접근시가 아니라 order 조회시 join을 이용해 조회한다.
  • mappedBy 옵션을 이용해 외래키를 저장할 테이블을 명확히 한다. (mappedBy로 설정한 테이블이 아닌 테이블이 외래키를 갖는다.)

분석 : Order에서 Member와의 연관관계를 제거하였기 때문에 더 이상 필요없다.

 

- @Builder

기능 : 클래스에 선언하는 경우 모든 필드를 포함하는 builder를 생성하며 매개변수가 선언된 메서드에 선언하면 해당하는 매개변수만을 초기화할 수 있는 builder를 생성한다. 매개변수 순서에 상관없이 필드의 초기화를 진행할 수 있다.

분석 : 클래스에 선언하는 경우 memberId에 대한 접근이 가능해지기 때문에 위험하다. 따라서 초기화 가능한 필드를 매개변수로 받는 생성자를 구현후 해당 메서드에 선언해야 한다. @Builder로 선언한 메서드는 private로 지정하여 인스턴스 생성 방식을 제한해야 한다.


2. Field

 

- private Long memberId

기능 : 기본키로 사용하기 위한 필드이다.

분석 : private는 필드에 대한 접근 제한을 위해 사용하였고, null을 확실히 구분하기 위해 Wrapper Long 사용하였다.

 

- private String nickName

기능 : 회원 아이디를 저장하기 위한 필드이다.

분석 : 필수적으로 입력해야 하는 정보이며 null과 blank가 들어올 수 없다. 해당 유효성 검증은 controller에서 진행된다. Entity에 @NotEmpty 등과 같은 유효성 검증을 넣어야 하는지 고민할 필요가 있다.

 

- private String email

기능 : 회원 이메일을 저장하기 위한 필드이다.

분석 : 필수적으로 입력해야 하는 정보이며 null과 blank가 들어올 수 없다. 해당 유효성 검증은 controller에서 진행된다. Entity에 @NotEmpty 등과 같은 유효성 검증을 넣어야 하는지 고민할 필요가 있다.

 

- private String password

기능 : 회원 비밀번호를 저장하기 위한 필드이다.

분석 : 필수적으로 입력해야 하는 정보이며 null과 blank가 들어올 수 없다. 해당 유효성 검증은 controller에서 진행한다. @NotEmpty 등과 같은 유효성 검증을 넣어야 하는지 고민할 필요가 있다.

 

- private String phoneNumber

기능 : 회원 핸드폰 번호를 저장하기 위한 필드이다.

분석 : 핸드폰 번호에 대한 정보는 선택 사항이기 때문에 null을 표시할 수 있어야 하며 올바른 패턴인지 확인할 필요가 있다. 해당 유효성 검증은 controller에서 진행한다.

 

- private LocalDate birthday

기능 : 회원 생일을 저장하기 위한 필드이다.

분석 : 생일에 대한 정보는 선택 사항이기 때문에 null을 표시할 수 있어야 한다.

 

- private List<Order> orders

기능 : 회원과 연관관계에 있는 주문을 저장하기 위한 필드이다.

분석 : Order Entity에서 Member와의 연관관계를 제거했기 때문에 더 이상 필요없다.


3. Method

 

- public Member(String nickName, String email, String password, String phoneNumber, LocalDate birthday)

기능 : 매개변수로 받은 값을 이용해 생성할 Member 인스턴스의 필드 값을 초기화한다.

분석 : 해당 생성자는 @Builder가 선언된 생성자로 현재 접근 제어자가 public되어 있어 builder를 이용한 생성뿐아니라 new 키워드를 이용한 생성 또한 가능한 상태이다. 인스턴스 생성에 있어 통일성을 주기위해 private로 선언해야 한다.

 

- public void updateMember(MemberUpdateDto dto)

기능 : 매개변수로 받은 MemberUpdateDto를 이용해 Member 인스턴스를 수정한다.

분석 : password 초기화의 경우 따로 선언된 updateMemberPasswor 메서드를 이용해 초기화 한다. 이 과정에서 잘못된 패스워드가 들어온 경우 패스워드를 변경하지 않고 기존 패스워드를 유지한다. 굳이 중복되는 코드를 구현할 필요없기 때문에 해당 로직이 적절하다고 판단하였다.

 

- public void updateMemberPassword(String password)

기능 : 매개변수로 받은 String password를 이용해 Member의 password를 수정한다.

분석 : 입력된 비밀번호의 유효성을 검증한 뒤 초기화를 진행한다. 유효하지 않은 경우에 대해선 수행하지만 않을 뿐이기 때문에 해당하는 예외를 메서드 밖으로 던져주는 것이 좋다고 판단된다.


Member 리팩토링

수정된 경우  색, 추가된 경우  색, 삭제된 경우  색으로 표시하였다. 수정된 코드는 다음과 같다.

 

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

    @Id @GeneratedValue
    private Long memberId;
    private String nickName;
    private String email;
    private String password;
    private String phoneNumber;
    private LocalDate birthday;

    @Builder
    private Member(String nickName, String email, String password, String phoneNumber, LocalDate birthday) {
        this.nickName = nickName;
        this.email = email;
        this.password = password;
        this.phoneNumber = phoneNumber;
        this.birthday = birthday;
    }

    public void updateMember(MemberUpdateDto dto) {
        this.nickName = dto.getNickName();
        this.email = dto.getEmail();
        this.phoneNumber = dto.getPhoneNumber();
        this.birthday = dto.getBirthday();
        updateMemberPassword(dto.getPassword());
    }

    public void updateMemberPassword(String password) {
        if (password.isEmpty()) {
            throw MEMBER_PASSWORD_INVALID.exception();
        }
        this.password = password;
    }

    public MemberInfoDto toInfoDto() {
        return MemberInfoDto.builder()
                .memberId(memberId)
                .nickName(nickName)
                .email(email)
                .phoneNumber(phoneNumber)
                .birthday(birthday)
                .build();
    }
}

 

1. Annotaion

 

- @NoArgsConstructor(access = AccessLevel.PROTECTED)

access 옵션을 추가하여 Member의 생성을 제한하였다. 이제 JPA와 상속 대상만 사용이 가능하며, Member의 생성은 Builder를 통해서만 가능해졌다.

 

- @ToString

해당 기능은 테스트에서만 사용되기 때문에 메인 코드에선 필요없다고 판단하여 제거하였다.

 

- @GeneratedValue

현재 상태에선 시퀀스 효율성에 대해서 배제하고 진행하기로 하였다. 따라서 strategy 옵션을 제거하였다.

 

- @OneToMany(mappedBy = "member")

Member에서 Order의 정보를 저장할 필요가 없기 때문에 제거하였다.


2. Field

 

- private List<Order> orders

Member에서 Order의 정보를 저장할 필요가 없기 때문에 제거하였다.


3. Method

 

- private Member(String nickName, String email, String password, String phoneNumber, LocalDate birthday)

이미 builder를 이용한 인스턴스 생성이 가능하기 때문에 해당 생성자를 이용한 인스턴스 생성은 필요없다고 판단되어 public 접근 제어자를 private로 변경하였다.

 

- public void updateMemberPassword(String password)

비밀번호가 잘못되었을 경우 현재 비밀번호를 수정하지 않고 끝나는 것뿐만 아니라 예외를 던지는 로직도 필요하다. 따라서 새로 정의한 MEMBER_PASSWORD_INVALID 예외를 발생시킨다. 접근제어자가 public으로 선언되어 어디서든 접근하여 비밀번호가 바뀔 수 있지만 메서드명으로 바뀌는 시점을 명확히 하였다는 점에 의의를 두었다.

 

- public MemberInfoDto toInfoDto()

이후 convertor 구현시 해당 메서드는 제거될 수 있다. 하지만 이전 Order와 OrderItem 리팩토링 과정에서의 통일성을 맞추기위해 toInfoDto 메서드를 구현하였다. 기존에 MemberInfoDto의 생성자를 이용하던 방식은 제거될 필요가 있다.

 

public interface OrderRepository extends JpaRepository<Order, Long> {

    List<Order> findAllByMemberId(Long memberId); //memberId와 일치하는 Order를 찾기위해 추가
}
@Override
public List<OrderInfoDto> findAll(Long memberId) {
    //member.getOrder() 방식에서 각각 조회하는 방식으로 수정
    memberRepository.findById(memberId).orElseThrow(MEMBER_NOT_FOUND::exception);
    return orderRepository
            .findAllByMemberId(memberId)
            .stream().map(Order::toInfoDto)
            .collect(Collectors.toList());
}
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    @Override
    public MemberInfoDto findOne(Long memberId) {
        return memberRepository
                .findById(memberId)
                .orElseThrow(MEMBER_NOT_FOUND::exception)
                .toInfoDto();
    }

    public MemberInfoDto findOne(String email) {
        return memberRepository
                .findByEmail(email)
                .orElseThrow(MEMBER_EMAIL_NOT_FOUND::exception)
                .toInfoDto();
    }

    public MemberInfoDto findOne(String nickName, String password) {
        return memberRepository
                .findByNickNameAndPassword(nickName, password)
                .orElseThrow(MEMBER_NOT_FOUND::exception)
                .toInfoDto();
    }

    @Transactional
    @Override
    public Long save(MemberSaveDto dto) {
        memberRepository.findByNickName(dto.getNickName())
                .ifPresent(m -> {throw MEMBER_NICKNAME_DUPLICATE.exception();});
        return memberRepository.save(dto.toEntity()).getMemberId();
    }

    @Transactional
    @Override
    public MemberInfoDto update(MemberUpdateDto dto) {
        Member found = memberRepository.findById(dto.getMemberId())
                .orElseThrow(MEMBER_NOT_FOUND::exception);

        memberRepository.findByNickName(dto.getNickName())
                .ifPresent(m -> {throw MEMBER_NICKNAME_DUPLICATE.exception();});
        found.updateMember(dto);
        return found.toInfoDto();
    }

    @Transactional
    @Override
    public void delete(Long memberId) {
        try {
            memberRepository.deleteById(memberId);
        } catch (EmptyResultDataAccessException ex) {
            throw MEMBER_NOT_FOUND.exception();
        }
    }
}

 

 

Link

댓글