지금 다루고자 하는 부분은 회원 관련 서비스 계층의 회원가입을 다루고 있는 비즈니스 로직이다.
현재 프로젝트에서는 로그인 타입별로 다른 테이블로 설계가 되어 있다.
소셜로그인을 위한 인증 테이블, 이메일 인증을 통해서 우리 서비스에 가입한 논소셜로그인을 위한 인증 테이블 둘로 나뉘어져 있다.
그리고, 사용자들의 Public한 정보가 담긴 유저 테이블의 PK(유저 고유 ID)를 인증 테이블들이 FK로 가지고 있는 상태이다.
그래서 로그인 타입별로, 회원정보를 저장하는것이 다른 DB에 담긴다.
그래서 MemberRepository
라는 Repository 인터페이스를 설계하고, 이를 구현한 NonSocialRepository
와 SocialRepository
를 구현하였다.
하지만, 문제는 서비스계층에서 MemberRepository
라는 역할의 인터페이스만 바라보게 의존성주입을 하려고 하면, 두 레포지토리를 모두 주입하는 꼴이 되므로, NoUniqueBeanDefinitionException
의 런타임 오류가 발생한다.
따라서 두가지 레포지토리를 주입시키기 위한 방법을 찾으려고 노력했다.
NonSocial 멤버와 SocialMember의 서비스로직을 아예 분리해버리면 세상 매우 편할 것이다.
하지만 그럼 결국 MemberRepsitory
의 인터페이스를 충분히 활용할 수 있음에도 버리는 꼴이 되버린다.
나는 이것이 싫었고, 최대한 살려보는 방향에서 리팩토링하기 시작했다.
말로 풀어쓰니 헷갈릴 수 있는데, MemberRepository
인터페이스를 구현하고, @Repository 어노테이션으로 등록한 NonSocialMemberRepository
와 SocialMemberRepository
모두를 Map 이나 List로 한꺼번에 Dependency Injection할 수 있다.
private final Map<String,MemberRepository>
이렇게 되면 Map에는 key값으로는 등록된 스프링 빈 이름이, value값으로는 MemberRepository구현체가 주입이 된다.
그럼 key값으로 꺼내면, 내가 원하는 MemberRepository 구현체를 사용할 수 있다.
@Slf4j
@Service
public class MemberService{
//생성자로 같은 타입의 클래스(MemberRepository) 다수 조회 후, Map으로 조회
private final Map<String,MemberRepository> repositoryMap;
private MemberRepository memberRepository;
private final MyRepository myRepository;
public MemberService(Map<String, MemberRepository> repositoryMap,MyRepository myRepository) {
this.repositoryMap = repositoryMap;
this.myRepository = myRepository;
log.info("Different LogintType Supported by Repositories m= {} ", repositoryMap);
}
HashMap<LoginType, String> loginTypeMap = new HashMap<>();
{
loginTypeMap.put(LoginType.NON_SOCIAL,"nonSocialMemberRepository");
loginTypeMap.put(LoginType.SOCIAL, "socialMemberRepository");
}
@Transactional
public Long join (MemberSaveDto memberSaveDto) throws NoSuchAlgorithmException {
Long user_id = -1L;
LoginType loginType = memberSaveDto.getLoginType();
log.info("loginType = {}",loginType);
memberRepository = repositoryMap.get(loginTypeMap.get(loginType));
//중복 처리 한번더 검증
if(!isValidEmail(memberSaveDto.getUserEmail(),loginType).isValid()){
throw new IllegalStateException("이미 가입된 회원 이메일입니다.");
}
if(! isValidName(memberSaveDto.getUserName(),loginType).isValid()){
throw new IllegalStateException("이미 가입된 닉네임입니다.");
}
switch (loginType) {
case NON_SOCIAL:
NonSocialMember member = new NonSocialMember();
member.setUserName(memberSaveDto.getUserName());
member.setLoginType(loginType);
member.setUserEmail(memberSaveDto.getUserEmail());
member.setUserPw(memberSaveDto.getUserPw());
NonSocialMember saveMember = (NonSocialMember) memberRepository.save(member);
user_id = saveMember.getUserId();
myRepository.createHistory(user_id.intValue());
return user_id;
case SOCIAL: //social 회원가입의 경우 -> 요청 필요
{
throw new NotYetImplementException("해당 요청은 아직 구현되지 않았습니다.");
}
}
return user_id; // non valid request, return -1
}
}
처음에 이렇게 구현했을 때, 굉장히 뿌듯했었다.
하지만 코드리뷰를 받는 와중에, 여기에 엄청난 문제가 있다는 사실을 알게 되었다.
Singleton객체의 근본을 깨트렸다.
스프링을 공부했던 사람 모두는 들어본 말이 있다. Singleton 패턴은 해당 객체 인스턴스를 단 하나만 생성해서 쓰레드별로 공유하므로, 항상 Stateless 하게 설계해야한다는 사실이다.
지금 현재코드는 주입받는 repositoryMap 말고, memberRepository가 선언이되어있고, 이는 join
메서드 안에서 로그인 타입별로 다른 repository가 참조된다. 이는 결국 다른 클라이언트가 해당 서비스로직을 실행할때, 문제를 야기한다.
예를들어 어떤 클라이언트가 논소셜 회원가입중이어서 memberRepository의 구현체인 NonSocialMemberRepository를 참조하는데, 갑자기 소셜로그인으로 가입하려는 사람이 중간에 개입하면서 memberRepository의 구현체가 SocialMemberRepository로 변경되고, 결국 소셜로그인 가입으로 변경이 되는 경우가 생겨버린다.
그럼 이를 어떻게 해결하면 좋을까?
@Slf4j
@Service
public class MemberService {
//생성자로 같은 타입의 클래스(MemberRepository) 다수 조회 후, Map으로 조회
private final Map<String,MemberRepository> repositoryMap;
private final MyRepository myRepository;
public MemberService(Map<String, MemberRepository> repositoryMap,MyRepository myRepository) {
this.repositoryMap = repositoryMap;
this.myRepository = myRepository;
}
HashMap<LoginType, String> loginTypeMap = new HashMap<>();
{
loginTypeMap.put(LoginType.NON_SOCIAL,"nonSocialMemberRepository");
loginTypeMap.put(LoginType.SOCIAL, "socialMemberRepository");
}
@Transactional
public SaveMemberResponseDto requestMemberRegistration (MemberSaveDto memberSaveDto){
LoginType loginType = memberSaveDto.getLoginType();
log.debug("Save Member Email = {}, loginType = {}",memberSaveDto.getUserEmail(), loginType);
//중복 처리 한번더 검증
if(!isValidEmail(memberSaveDto.getUserEmail(),loginType).isValid()){
throw new IllegalStateException("이미 가입된 회원 이메일입니다.");
}
if(!isValidName(memberSaveDto.getUserName(),loginType).isValid()){
throw new IllegalStateException("이미 가입된 닉네임입니다.");
}
switch (loginType) {
case NON_SOCIAL:
MemberRepository selectedMemberRepository = repositoryMap.get(loginTypeMap.get(loginType));
NonSocialMember member = NonSocialMember.createNonSocialMember(memberSaveDto);
SaveMemberResponseDto savedMember = selectedMemberRepository.save(member);
myRepository.createHistory(savedMember.getId());
return savedMember;
case SOCIAL: //social 회원가입의 경우 -> 요청 필요
{
throw new NotYetImplementException("해당 요청은 아직 구현되지 않았습니다.");
}
}
throw new InvalidParameterException("!~ 수정");
}
}
2번과 마찬가지로 같은 @Repository
어노테이션으로 등록된 빈 이름과 해당 클래스를 주입받을 수 있게, repositoryMap 으로 Dependency Injection을 한다.
2번이 문제가되는 이유는 MemberService 내에 MemberRepository를 선언해 놓은 후 해당 상태를 바꾸기 때문인데,
이를 지역변수로 빼버리면 문제가 되지 않을 것이다.
쓰레드별로 함수호출이 각각 이루어질 것이기 때문이다.