들어가기
우아한 객체지향을 듣고 느낀점만 많아서는 안되겠다는 생각이 들어 진행중인 프로젝트의 도메인 객체와 서비스 레이어를 유심히 살펴봤습니다.
도메인이 의존해서는 안될 코드를 하나 발견했고 이를 정리해보려 합니다.
도메인이 암호화를 책임지는 코드
아래는 회원을 관리하는 엔티티다.
도메인 객체에 passwordEncode 라는 메서드로 비밀번호를 암호화 하고있는데 처음엔 자신의 상태(password)는 자신이 변화시켜야한다고 생각했기에 도메인 내부에서 처리하고 있었습니다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
private String loginId;
private String password;
private String nickname;
@Enumerated(EnumType.STRING)
private Role role;
@Enumerated(EnumType.STRING)
private SocialType socialType;
private String socialId;
private String refreshToken;
@Builder
public User(String loginId, String password, String nickname, Role role, SocialType socialType, String socialId, String refreshToken) {
this.loginId = loginId;
this.password = password;
this.nickname = nickname;
this.role = role;
this.socialType = socialType;
this.socialId = socialId;
this.refreshToken = refreshToken;
}
// 비밀번호 암호화 메소드
public String passwordEncode(PasswordEncoder passwordEncoder) {
return this.password = passwordEncoder.encode(this.password);
}
// 유저 권한 설정 메소드
public void authorizeUser() {
this.role = Role.USER;
}
public void updateRefreshToken(String updateRefreshToken) {
this.refreshToken = updateRefreshToken;
}
}
하지만 우아한 객체지향에서도 봤듯이 메서드 시그니처에 존재하는 객체의 참조도 하나의 의존성입니다.
도메인 ---> 스프링 시큐리티의 암호화 컴포넌트 로 의존성이 형성돼 있습니다.
PasswordEncode는 스프링 시큐리티에서 제공하는 암호화 인터페이스를 빈으로 등록해 사용하고 있습니다.
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
아래 서비스 레이어에서도 PasswordEncoder를 의존하고 있습니다.
사실 아래 코드는 순수 도메인이 스프링 빈을 의존하게 할 수 없었기에 서비스 레이어가 직접 객체참조를 하고 해당 객체를 도메인에게 넘겨주는 아주 기이한 코드입니다.
@Transactional
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public void signUp(UserSignUpRequestDto userSignUpDto) throws Exception {
validateInputField(userSignUpDto);
User user = User.builder()
.loginId(userSignUpDto.loginId())
.password(userSignUpDto.password())
.nickname(userSignUpDto.nickname())
.role(Role.USER)
.build();
// 기이한 코드
user.passwordEncode(passwordEncoder);
userRepository.save(user);
}
암호화 방식 자체가 변경된다면?
PasswordEncoder는 인터페이스이기 때문에 시큐리티에서 제공하는 다른 구현체를 사용한다면 문제는 없습니다.
하지만 만약 암호화 방식을 커스텀하게 가져가야하는 경우엔 도메인 객체의 변경이 불가피합니다.
PasswordEncoder 인터페이스를 구현해 도메인 변경없이 문제를 풀어낼 수야 있겠지만 잘못 설계된 문제를 임시방편으로 해결한 것 밖에는 되지 않습니다.
간단하게 비밀번호를 암호화한 뒤 암호화된 비밀번호를 빌더에 넣어주면 아주 간단하게 의존성을 끊어낼 수 있습니다.
추가로 매퍼 클래스를 도입해서 서비스레이어에서 조차 PasswordEncoder를 의존하지 않도록 코드를 작성했습니다.
@RequiredArgsConstructor
@Component
public class UserMapper {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public User mapFrom(UserSignUpRequestDto dto) {
String encodedPassword = encryptPassword(dto);
return User.builder()
.loginId(dto.loginId())
.password(encodedPassword)
.nickname(dto.nickname())
.role(Role.USER)
.build();
}
private String encryptPassword(UserSignUpRequestDto dto) {
return passwordEncoder.encode(dto.password());
}
}
이렇게 매퍼를 도입하면 서비스 레이어는 코드가 더 가벼워질 수 있습니다.
@Transactional(readOnly = true)
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
@Transactional
public void signUp(UserSignUpRequestDto userSignUpDto) throws Exception {
validateInputField(userSignUpDto);
User user = userMapper.mapFrom(userSignUpDto);
userRepository.save(user);
}
private void validateInputField(UserSignUpRequestDto userSignUpDto) throws UserException {
if (userRepository.findByLoginId(userSignUpDto.loginId()).isPresent()) {
throw new UserException(UserErrorCode.DUPLICATED_DATA_REQUEST.changeDefaultDescription("이미 존재하는 아이디입니다."));
}
if (userRepository.findByNickname(userSignUpDto.nickname()).isPresent()) {
throw new UserException(UserErrorCode.DUPLICATED_DATA_REQUEST.changeDefaultDescription("이미 존재하는 닉네임입니다."));
}
}
}
'Spring' 카테고리의 다른 글
게시글과 이미지 등록 API 분리로 API Latency 개선기 (2) (0) | 2024.06.19 |
---|---|
게시글과 이미지 등록 API 분리로 API Latency 개선기 (1) (0) | 2024.06.19 |
@Value 감옥에서 벗어나기 [@ConfigurationProperties] (0) | 2024.04.03 |
[Spring Security JWT 로그인 (3)] DelegatingFilterProxy Deep Dive (0) | 2024.03.15 |
[Spring Security JWT 로그인 (1)] 어떤 로그인 방식이 좋을까 (1) (0) | 2024.03.11 |