20241112 TIL : 정적 팩토리 메서드, @Setter 사용 지양

MCS·2024년 11월 12일

TIL

목록 보기
2/45

오늘 진행한 학습

  • Chapter 1 프로젝트 리팩토링
    • 정적 팩토리 메서드
    • @Setter 사용 지양
    • Enum으로 메세지 관리
    • 삭제 처리(soft delete)한 데이터에 대한 처리

Chapter 1 프로젝트 리팩토링

정적 팩토리 메서드

정적 팩토리 메서드란?

객체 생성의 역할을 하는 클래스 메서드

그렇다면 정적 팩토리 메서드를 사용하는 이유는 무엇일까?

  • 이름을 가질 수 있다.
  • 호출할 때마다 새로운 객체를 생성할 필요가 없다.
  • 하위 자료형 객체를 반환할 수 있다.
  • 객체 생성을 캡슐화할 수 있다.

리팩토링 전 코드는 다음과 같다.

public Address(AddressRequestDto requestDto, User user) {
        this.user = user;
        this.zipNum = requestDto.getZipNum();
        this.city = requestDto.getCity();
        this.district = requestDto.getDistrict();
        this.streetName = requestDto.getStreetName();
        this.streetNum = requestDto.getStreetNum();
        this.detailAddr = requestDto.getDetailAddr();
    }

객체를 직접 생성자를 통해 생성하는 방식이다. 아래는 리팩토링 후 코드이다.

public static Address from(AddressRequestDto dto, User user) {
        return Address.builder()
                .zipNum(dto.getZipNum())
                .city(dto.getCity())
                .district(dto.getDistrict())
                .streetName(dto.getStreetName())
                .streetNum(dto.getStreetNum())
                .detailAddr(dto.getDetailAddr())
                .user(user)
                .build();
    }

정적 팩토리 메서드를 선언하면 메서드의 이름을 통해 객체를 생성할 때 의도를 드러낼 수 있다. 그리고 이전에 학습했던 @builder를 적용해 보았다.
기존의 코드와 지금 코드 모두 캡슐화는 이루어지고 있다고 보았지만, 정적 팩토리 메서드를 호출함으로서 의도를 드러내고, 메서드에서 사용할 생성자는 default로 선언해 외부 패키지에서 접근할 수 없도록 만들어 불필요한 객체의 생성이 이루어지지 않도록 만들었다.

User의 정보를 가져오는 Service 메서드에서도 비슷한 방식으로 ResponseDto의 생성자에 user 객체를 파라미터로 주어 dto 객체를 생성하도록 구현했었는데, 위와 같은 방식으로 수정했다.

@setter 사용 지양

Entity에 Setter의 사용을 지양해야 하는 이유는 다음과 같다.

  • 사용한 의도를 쉽게 파악하기 어렵다.
  • 일관성을 유지하기 어렵다.

기존의 사용자 정보 수정 메서드는 다음과 같다.

 @Transactional
    public UsernameResponseDto updateUser(@Valid UpdateUserRequestDto requestDto, String username) {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new NullPointerException(ExceptionMessage.USER_NOT_FOUND.getMessage()));

        user.setPassword(passwordEncoder.encode(requestDto.getPassword()));
        user.setEmail(requestDto.getEmail());
        user.setPhoneNumber(requestDto.getPhoneNumber());
        user.setImageUrl(requestDto.getImgUrl());
        user.setPublicProfile(requestDto.isPublicProfile());
        user.setRole(requestDto.getRole());

        userRepository.save(user);

        return new UsernameResponseDto(user.getUsername());
    }

dirty checking을 위해 setter를 사용해 user의 값을 변경하고 있다. 이러한 방식은 의도를 정확하게 파악하기 어렵고, user 데이터의 일관성을 유지하기 어렵다. 이 메서드 외에서 user의 값이 의도치 않게 수정될 수 있는 위험성이 존재한다.
다음은 리팩토링을 진행한 후의 결과이다.

 public void update(UpdateUserRequestDto requestDto, PasswordEncoder passwordEncoder) {
        this.email = requestDto.getEmail();
        this.password = passwordEncoder.encode(requestDto.getPassword());
        this.phoneNumber = requestDto.getPhoneNumber();
        this.role = requestDto.getRole();
        this.publicProfile = requestDto.isPublicProfile();
        this.imageUrl = requestDto.getImgUrl();
    }
@Transactional
public UsernameResponseDto updateUser(@Valid UpdateUserRequestDto requestDto, String username) {
        User user = userRepository.findById(username)
                .orElseThrow(() -> new NullPointerException(ExceptionMessage.USER_NOT_FOUND.getMessage()));
		
        user.update(requestDto, passwordEncoder);
        
        userRepository.save(user);
}

필드의 업데이트는 update 메서드를 통해서만 진행한다. 이렇게 되면 메서드의를 호출한 의도(user 필드의 수정)를 정확하게 파악할 수 있고, 일관성이 깨질 위험성이 줄어든다. 기존 코드에 비해서 확실히 가독성도 증가했고, 의도 파악이 쉽다.

위의 두 리팩토링 과정을 통해 얻은 결론은 다음과 같다.

  • 메서드의 의도를 정확하게 드러낸다.
  • 객체의 캡슐화 수준을 높인다.
  • 객체의 일관성을 유지해야 한다.

Enum으로 메세지 관리

예외를 던질 때마다 메세지를 직접 작성하게 되면 메세지를 수정해야 할 때 과정이 번거롭다.
메세지와 같은 매직리터럴은 런타임 동안 수정될 수 있는 변수가 아닌 상수이다.
따라서 매직리터럴을 제거하고, 상수는 Enum으로 관리하는 것이 좋다는 결론을 내렸다.

@Getter
@RequiredArgsConstructor
public enum ExceptionMessage {
    NOT_ALLOWED_METHOD("허용되지 않은 메서드 호출입니다."),
    LOGIN_NOT_FOUND("로그인 정보가 없습니다."),
    ALREADY_LOGGED_IN("이미 로그인된 사용자입니다."),
    USER_NOT_FOUND("사용자를 찾을 수 없습니다."),
    DUPLICATED_USERNAME("중복된 사용자가 존재합니다."),
    DUPLICATED_EMAIL("중복된 이메일이 존재합니다."),
    DUPLICATED_PHONENUMBER("중복된 전화번호가 존재합니다."),
    NOT_ALLOEWD_ROLE("일반 사용자는 CUSTOMER 또는 OWNER로만 가입할 수 있습니다."),
    NOT_ALLOWED_API("접근 권한이 없습니다."),
    ADDRESS_NOT_FOUND("해당 주소가 존재하지 않습니다."),
    ADDRESS_DELETED("삭제된 주소입니다.");

    private final String message;
}

현재 status code는 GlobalExeptionHandler에서 지정해 관리하고 있고, 성공 status code는 ResponseEntity<>를 return하면서 .status()를 통해 직접 지정하고 있어 메세지만 Enum으로 관리하기로 결정했다.

삭제 처리(soft delete)한 데이터에 대한 처리

삭제 처리한 데이터는 일반 사용자는 볼 수 없어야 한다.
그렇다면 관리자(MANAGER, MASTER)의 경우는?

관리자는 삭제 여부와 관계없이 모든 데이터를 볼 수 있어야 한다고 결론을 내렸다.

따라서 요청한 사용자의 role을 검증해 MANAGER와 MASTER 외에는 삭제된 데이터에 접근하면 예외를 발생시키도록 처리했다.

 public UserResponseDto getUserInfo(String username, User loggedInUser) {
        User user = userRepository.findById(username)
                .orElseThrow(() -> new NullPointerException(ExceptionMessage.USER_NOT_FOUND.getMessage()));

        // 요청한 사용자의 역할이 MANAGER나 MASTER가 아닌 경우 탈퇴한 사용자인지 확인
        // MANAGER나 MASTER인 경우에는 탈퇴한 사용자의 정보도 볼 수 있음
        if (loggedInUser.getRole() != UserRoleEnum.MANAGER && loggedInUser.getRole() != UserRoleEnum.MASTER) {
            checkDeletedUser(user);
        }

        return UserResponseDto.from(user);
    }
profile
백엔드를 잘 하고 싶은 사람

0개의 댓글