오늘, 카테캠 3단계 프로젝트 팀원이 "supplier를 사용하신 이유가 뭐에요?"라는 질문이 들어왔다.
대면으로 질문이 들어왔었는데, 그 자리에서 깔끔하게 설명하지 못 한게 아쉬워서 이 글을 통해 설명을 다시하고자 정리한다.
아래와 같이 나는 NicknameSupplier라는 인터페이스를 만들었다.
package com.kakao.golajuma.auth.web.supplier;
public interface NicknameSupplier {
String getNickname();
}
그리고 이를 ValidNicknameRequest에서 implements하여 사용한다.
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder(toBuilder = true)
public class ValidNicknameRequest implements NicknameSupplier {
@NotBlank(message = ValidExceptionMessage.EMPTY_MESSAGE)
String nickname;
}
그리고 이는 ValidNicknameRequest를 response body에서 받아서 이에 해당하는 service로 호출하는 코드이다.
@PostMapping("/nickname-check")
public ApiResponse<SuccessBody<Void>> validNickname(
@RequestBody @Valid ValidNicknameRequest request) {
validNicknameService.execute(request);
return ApiResponseGenerator.success(HttpStatus.CREATED, MessageCode.CREATE);
}
여기서, "특이점" 이라고 한다면,
execute()의 파라미터로 ValidNicknameRequest를 받는 게 아니라 NicknameSupplier 를 받는 것이라고 할 수 있다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ValidNicknameService {
private final UserRepository userRepository;
public void execute(NicknameSupplier supplier) {
boolean exists = userRepository.existsByNickname(supplier.getNickname());
if (exists) {
throw new DuplicatedNicknameException();
}
}
}
이 글을 보는 모든 분들이 똑같은 의문이 들 것이다.
implements NicknameSupplier
를 하지 않고, 파라미터로 ValidNicknameRequest 타입을 전달받아도 되었을 텐데,
왜 나는 implements NicknameSupplier
를 사용했으며, NicknameSupplier를 파라미터 타입으로 전달받았을까?
인터페이스를 파라미터로 타입으로 선언을 하면, 재사용성이 높아진다!
만약, ValidNicknameRequest를 파라미터의 타입으로 선언을 해두었다면,
해당 메서드에는 ValidNicknameRequest 클래스 아니면 ValidNicknameRequest를 상속한 클래스밖에 오지 못 한다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ValidNicknameService {
private final UserRepository userRepository;
public void execute(ValidNicknameRequest request) {
boolean exists = userRepository.existsByNickname(request.getNickname());
if (exists) {
throw new DuplicatedNicknameException();
}
}
}
그래서,
만약, ValidNicknameRequest에서도 valid작업을 우선해야하기 때문에 위와 동일한 로직을 수행할 텐데, 그럼 이때는 ValidNicknameRequest타입을 파라미터로 받는 메서드를 하나 더 추가해야 한다.
(물론, 이때 그냥 String을 넘기면 되는 거 아니야? 라고 생각할 수가 있다. 지금은 String만 넘겨도 되지만.. 언젠가는 또 객체 형태도 있지 않을까?)
이처럼 추상화를 통해 내부에 어떤 멤버변수를 갖고 있던 관련 없이 필요한 정보만 건네줄 수 있다. 라는 장점이 있다.(즉 필요한 정보가 객체 사이에 계약을 한다. => 복잡도를 낮춤)
추가적으로, 코드를 읽는 사람 입장에서도 인터페이스만 보고도 어떤 정보를 사용해야하는지 파악하기가 쉽다.라는 장점이 있을 것 같다.
++) 추가적인 장점이 있을 것 같은데.. 이는 이번 프로젝트에 적용을 해보면서 알아보려고 한다!(to be continue..)
이 질문을 한 팀원이 일단은 사용을 하시고 질문을 해주셨다!
그래서 실제로 사용한 코드가 있으니! 그 코드를 통해 알아보도록 하자~
@Service
@RequiredArgsConstructor
@Transactional
public class UpdateUserNickNameService {
private final UserRepository userRepository;
private final ValidNicknameService validNicknameService;
public UpdateNickNameResponse execute(UpdateUserNickNameRequest requestDto, Long userId) {
validNicknameService.execute(requestDto);
/**
* 닉네임을 업데이트 하는 로직
*/
}
만약, 우리가 ValidNicknameService의 execute로 EmailSupplier를 받지 않고 Dto를 받았더라면 또 UpdateUserNickNameRequest를 통해 nickname을 검증하는 로직을 짰어야 할 것이다.
하지만, 우리가 EmailSupplier를 파라미터로 받는 덕분에
EmailSupplier를 implements한 ValidNicknameRequest, UpdateUserNickNameRequest를 모두 전달할 수 있다는 장점이 있다.