아임제니퍼 2024. 12. 18. 10:13

User Entity

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
@Entity
@Table(name = "users")
public class User {
	
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    .
    .
    .

    @Builder
    private User(String email, String password, String nickname, String image, String provider, String providerId,
        UserType userType, LocalDateTime createdAt,LocalDateTime updatedAt, LocalDateTime deletedAt) {
        this.email = email;
        this.password = password;
        this.nickname = nickname;
        this.image = image;
        this.provider = provider;
        this.providerId = providerId;
        this.userType = userType;
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
        this.deletedAt = deletedAt;
    }

    public static User create(UserServiceCreateRequest request, String password) {
        return User.builder()
            .email(request.email())
            .password(password)
            .nickname(request.nickname())
            .build();
    }
    
    public void updateNickname(String nickname) {
        this.nickname = nickname;
    }

	public void updateImage(String storedName) {
		this.image = storedName;
	}
}

 

UserCreateRequest

@Builder
public record UserCreateRequest(
	String email,
	String password,
	String nickname
	// String image
) {
	public UserServiceCreateRequest toServiceRequest() {
		return UserServiceCreateRequest.builder()
			.email(email)
			.password(password)
			.nickname(nickname)
			.build();
	}
}

 

UserUpdateRequest

public record UserUpdateRequest(
	String nickname,
	@Schema(description = "프로필 이미지", type = "string", format = "binary")
	MultipartFile image
) {
	public static UserUpdateServiceRequest toServiceRequest(String nickname, MultipartFile image) {
		return UserUpdateServiceRequest.of(nickname, image);
	}
}

 

UserServiceCreateRequest

@Builder
public record UserServiceCreateRequest(
        String email,
        String password,
        String nickname
        // String image
) {
}

 

UserUpdateServiceRequest

public record UserUpdateServiceRequest(
	String nickname,
	MultipartFile image
) {
	public static UserUpdateServiceRequest of(String nickname, MultipartFile image) {
		return new UserUpdateServiceRequest(nickname,image);
	}
}

 

Controller

@PostMapping("/signup")
public ResponseEntity<SimpleApiResponse> signup(
    @RequestBody UserCreateRequest userCreateRequest) {
    userService.createUser(userCreateRequest.toServiceRequest());
    return new ResponseEntity<>(SimpleApiResponse.of("사용자 생성 성공"), HttpStatus.CREATED);
}

@PutMapping(consumes = "multipart/form-data")
public ResponseEntity<UserInfoResponse> updateUser(
    @Parameter(hidden = true) @Authorize PrincipalDetails user,
    @ModelAttribute UserUpdateRequest request) throws IOException {
    UserInfoResponse response = userService.updateUser(user.getUser(), request);
    return new ResponseEntity<>(response, HttpStatus.OK);
}

 

UserService

@RequiredArgsConstructor
@Transactional
@Service
public class UserService {

	private final UserRepository userRepository;
	private final PasswordEncoder passwordEncoder;
	private final S3Service s3Service;

	public User createUser(UserServiceCreateRequest request) {
		String encodePassword = passwordEncoder.encode(request.password()); // 해싱하는 부분
		User user = User.create(request, encodePassword);
		return userRepository.save(user);
	}
}

public UserInfoResponse updateUser(User user, UserUpdateRequest request) throws IOException {
    Optional.ofNullable(request.nickname()).ifPresent(user::updateNickname);
    if (request.image() != null) {
        String storedName = s3Service.upload(request.image(), request.image().getOriginalFilename());
        user.updateImage(storedName);
    }
    return UserInfoResponse.of(user);
}

 

UserInfoResponse

public record UserInfoResponse(
	Long id,
	String email,
	String nickname,
	String image,
	LocalDateTime createdAt,
	LocalDateTime updatedAt
) {
	public static UserInfoResponse of(User user) {
		return new UserInfoResponse(user.getId(), user.getEmail(), user.getNickname(), user.getImage(),
			user.getCreatedAt(), user.getUpdatedAt());
	}
}

 

UserResponse

package com.fesi.mukitlist.domain.service.auth.response;

import com.fesi.mukitlist.core.auth.application.User;

public record UserResponse(
	Long id,
	String nickname,
	String image
) {
	public static UserResponse of(User user) {
		return new UserResponse(user.getId(), user.getNickname(), user.getImage());
	}
}

 

만약 하나의 User Request를 사용할 경우, UserRequest를 사용하는 POST 로직

image가 들어옴 → 그런데 유저를 생성하는 로직에서는 image를 지정하지 않음 → 최초로 UserRequest를 사용한 사람은 이러한 로직이 당연하다고 느끼지만, 이러한 맥락을 접하지 않는 새로 들어온 (혹은 동료) 개발자는 이상하게 느낌 → 엇... 왜 image가 없는데 유저를 생성할 때 image를 넣어야 하는건가? 뭐지? → image가 없으면 왜 이렇게 DTO를 합쳐둔거지? 무슨 이유가 있나?

 

코드를 보며 직관적이지 않고 100% 이해되지 않는 부분이 있다면 해당 부분을 변경하는데 혹시 모를 버그를 만들 수도 있기에 코드 변경에 있어 거부감이 들게 되고, 더 나은 코드로 리팩토링 할 수 없게 됩니다.

 

UserRequest를 사용하는 PUT 로직

이런 일은 update 로직에서도 동일하게 발생합니다. DTO에는 email, password, nickname이 있죠. 그런데 정작 업데이트 되는 항목은 'nickname'입니다. 그럼 왜 password는 업데이트 하지 않는 것인지, 이것이 요구사항인지 버그인지 혼란이 생기게 됩니다.

 

현재 상태뿐 아니라 미래에 대해서도 유지보수성을 고려해야 합니다!

 

예를 들어 create를 할 때, 요구사항이 변경되어 emailnickname뿐만 아니라 주소, 성별 등 다양한 필드가 추가될 겁니다. 이때  UserRequest로 DTO가 통일되어 있다면 UserRequest에는 다양한 필드가 추가될 겁니다. 또한, 그러한 필드들에 대해 검증도 해야합니다. 예를 들어 주소가 비어 있다면 에러(Exception)를 내보내야 할겁니다.

 

그런데 update 할 때도 이런 DTO를 사용한다면, 로직에 분기가 들어가게 됩니다. create를 할 때 사용된 DTO는 필드값 검증을 하고, update를 할 때 사용한 DTO는 검증을 하지 않는 방식으로 말이죠.

 

또한, 처음에는 비슷해 보였던 create / update DTO 필드가 점점 달라지며 극단적으로는 겹치는 필드가 하나도 없게 바뀌게 될 수도 있습니다.

 

따라서 현재 시점으로 보나, 미래 시점으로 보나 처음부터 DTO를 분리해 두는 것이 유지보수성이 좋기 때문에 보통은 API 별로 DTO를 분리하는게 좋습니다. 추가로, 응답 DTO의 경우는 함께 사용하는 경우가 간혹 있습니다만, 설사 필드가 완전히 겹치더라도 최종 DTO를 분리해두면 변경되는 요구사항에 대해 조금 더 유연하게 대응할 수 있습니다.

 

결론 : DTO를 분리해야 유지보수성이 좋다
<도움 받은 블로그>


DTO request와 response

https://www.inflearn.com/community/questions/1276981/dto-request%EC%99%80-response?srsltid=AfmBOoqvuIXyiFHd_GMs0tz0coJ1wnNoKiijX25VyX7Kk597KhZ5H12F