평화롭게 Egov를 커스텀하던 어느 날, 콘솔창에 다시 빨간 글씨가 나타났다.
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'authUserDetailsService' ... No qualifying bean of type 'tems.core.mapper.usr.UserMapper' available
처음엔 '아, 이거 그냥 MyBatis 매퍼 설정 문제겠지?'라고 생각했다. 하지만 이 오류를 파고들다 보니 예상치 못한 것까지 파고들게 되었다.
이번엔 그 과정을 기록해보고자 한다.
기존에는 이렇게 생긴 XML 설정이 있었다
<!-- 이런 게 context-mapper.xml에 있었음 -->
<bean id="egov.sqlSession" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="egov.dataSource"/>
<property name="configLocation" value="classpath:/egovframework/sqlmap/mapper-config.xml"/>
<property name="mapperLocations">
<list>
<value>classpath:/tems/mapper/**/*.xml</value>
</list>
</property>
</bean>
매퍼도 그냥 심플하게 있었고
// 심플한 UserMapper
public interface UserMapper {
UserEntity findByUserid(String userid);
}
일단 가장 기본적인 해결방법부터 시도해봤다
<mybatis:scan base-package="tems.core.mapper"/>
@Mapper //
public interface UserMapper {
UserEntity findByUserid(String userid);
}
근데 코드를 자세히 보니까 더 큰 문제가 있었다. 특히 이 부분이 눈에 띄었다.
// 기존 코드
@Service
public class AuthUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String userid) {
UserEntity userEntity = userMapper.findByUserid(userid);
// 이게 다 한 클래스에...?
if (userEntity == null) {
throw new UsernameNotFoundException("User not found");
}
String rawPassword = "입력된 비밀번호";
if (!passwordEncoder.matches(rawPassword, userEntity.getPassword())) {
throw new UsernameNotFoundException("Invalid password");
}
return new User(userEntity.getUserid(),
userEntity.getPassword(),
AuthorityUtils.createAuthorityList(userEntity.getRole()));
}
}
이거 보고 '아... 이래서 문제가 생겼구나' 싶었다.
하나의 클래스가 너무 많은 일을 하고 있었던 거다. password 부분은 필요하지도 않음
원래는 xml에 빈만 등록해주면 끝날 문제였지만,
아직은 시간이 남기때문에(?) 추후 재발 방지를 목적으로 이전에 배웠던 SOLID 원칙 중 SRP(단일책임원칙)과 DIP(의존성 역전 원칙)을 적용시켜 개선해 보기로 했다.
@Service
public class UserService {
private final UserMapper userMapper;
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
// 순수한 비즈니스 로직만!
}
@Service
public class AuthUserDetailsService implements UserDetailsService {
private final UserService userService;
public AuthUserDetailsService(UserService userService) {
this.userService = userService;
}
// 인증 관련된 것만!
}
기존에는 이런 단순한 엔티티 하나로 다 했는데
// 심플했던 예전 코드
public class UserEntity {
private Long id;
private String userid;
private String password;
private String role;
}
이제는 역할에 맞게 분리
public class User {
// 비즈니스 정보만
}
public class SecurityUser extends User implements UserDetails {
// 보안 관련된 것만
}
겉으로 보이는 증상만 고치려고 하지 말자
관심사 분리가 진짜 중요하다
가독성도 상승최신 트렌드를 따르자
@Autowired 대신 생성자 주입사실 처음에는 그냥 단순한 오류 해결이라고 생각했다.
하지만 문제를 해결하다보니 전체 애플리케이션 구조를 다시 생각해보는 계기가 됐다.
동작하는 코드도 중요하지만, 잘 설계된 코드가 장기적으로 더 중요하다는 걸 다시 한번 깨달았다.
특히나 현업에서는 협업이 필수이고, SI는 마감기한까지 프로젝트를 끝마쳐야 하기에 서로의 코드를 보는 시간과 버그를 줄이려면 더욱 중요하다고 느껴진다.
바쁘다고 대충대충 짜다간 오늘과 같은 현상을 계속 만나서 생산성이 더 떨어질 것 같다.
작동과 클린코드 사이의 협의점을 찾는게 개발자에겐 제일 어려운 숙제가 아닐까............
현업에서 마감기간 안에 말씀하신 내용들을 지키며 작업 하는게 생각보다 더 어려운 것 같더라구요 ㅜㅜ 대단하십니다 ㅎㅎ