- Chapter 1 프로젝트 리팩토링
- 정적 팩토리 메서드
- @Setter 사용 지양
- Enum으로 메세지 관리
- 삭제 처리(soft delete)한 데이터에 대한 처리
정적 팩토리 메서드란?
객체 생성의 역할을 하는 클래스 메서드
그렇다면 정적 팩토리 메서드를 사용하는 이유는 무엇일까?
리팩토링 전 코드는 다음과 같다.
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 객체를 생성하도록 구현했었는데, 위와 같은 방식으로 수정했다.
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으로 관리하는 것이 좋다는 결론을 내렸다.
@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으로 관리하기로 결정했다.
삭제 처리한 데이터는 일반 사용자는 볼 수 없어야 한다.
그렇다면 관리자(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);
}