User Entity는 회원가입을 할 때 ROLE(역할)이 나눠지며 해당 역할에 따라 이용할 수 있는 Service가 다릅니다.

먼저 회원가입과 로그인에 관한 Repository와 Service, 예외 클래스를 만들겠습니다. 


1. 패키지들 먼저 만들어줍니다.

  1. dto는 API를 통신하며, 사용될 객체가 속하는 패키지입니다.
  2. exception은 로직을 수행하며 예외가 발생했을 때 처리하는 객체가 담긴 패키지입니다. 

2. UserRepository

public interface UserRepository extends JpaRepository<User,Long> {

    Optional<User> findByEmail(String email);
}
JPA를 사용하므로 JpaRepository를 상속합니다. JpaRepository를 상속받으면 메소드 명명법으로 Query를 만들 수 있어 개발 생산성이 올라가며 다양한 활용이 가능합니다.
SimpleJpaRepository는 JpaRepository를 구현하고 있고, @Repository가 붙어있어 ComponentScan 대상이 돼 bean으로 등롭됩니다.  

3. UserDto, UserService, UserServiceImpl

public class UserDto {

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class SignupDto{
        public String email;
        public String password;
        public String name;
        public String role;
        public String city;
        public String street;
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class LoginDto{
        public String email;
        public String password;
    }
}

UserDto를 사용하는 이유는 프로젝트 내부의 Entity를 외부로 노출 시키지 않으며, Entity는 DB에 저장되므로 일정한 형태에서 변형이 불가능하지만, Dto는 오로지 클라이언트와 서버간의 데이터 전송을 위한 목적으로 사용되므로 서로가 원하는 데이터에 알맞게 변형시킬 수 있기 때문에 Dto를 사용합니다. 

 

Dto의 무분별한 사용은 코드의 가독성과 유지보수에 어려움이 동반됩니다. 따라서 하나의 Dto에 static inner class를 만들어서 목적을 분명히하는 Dto를 만드는 것이 좋다고 생각합니다. 

public interface UserService {

    Long signup(String email, String password,String role,String name, Address address);
    SessionDto login(String email, String password);
}

UserServie를 상속받아 UserServiceImpl을 구현합니다. 이와 같이 하는 이유는 Strategy 패턴을 활용하는 방법으로 spring은 DI와 IoC를 활용하므로 유지 보수와 역할을 분리하기 알맞기 때문입니다. 자세한 설명은 아래 링크에서 확인 가능합니다:) 

https://dingdingmin-back-end-developer.tistory.com/entry/spring-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%99%80-bean-%EA%B0%9C%EB%85%90

 

spring 컨테이너와 bean 개념

1. Spring Container란? ▶ Spring Container는 java 객체의 life cycle를 관리하고, 생성된 객체들에게 추가적인 기능을 제공합니다. Spring Container에서 관리하는 객체를 Bean라고 부릅니다. bean에는 IoC, DI..

dingdingmin-back-end-developer.tistory.com

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;

    @Override
    @Transactional
    public Long signup(String email, String password, String role,String name,Address address){
        userRepository.findByEmail(email).ifPresent(
                a -> {
                    throw new UserEmailDuplicateException("이미 존재하는 EMAIL 입니다.");

                });
        ROLE userRole = makeRole(role);
        User member = User.createUser(email,password,name,address,userRole);
        return userRepository.save(member).getId();
    }

    @Override
    @Transactional
    public SessionDto login(String email, String password) {
        User member = userRepository.findByEmail(email).orElseThrow(
                () -> {
                    throw new UserEmailOrPasswordWrongException("아이디 혹은 비밀번호가 틀렸습니다.");
                }
        );
        if (!member.getPassword().equals(password)){
            throw new UserEmailOrPasswordWrongException("아이디 혹은 비밀번호가 틀렸습니다.");
        }
        return new SessionDto(member.getEmail(),member.getId(),member.getName(),member.getRole());
    }

    private ROLE makeRole(String role) {
        if(role.equals("member")){
            return  ROLE.MEMBER;
        }
        else if(role.equals("founder")){
            return  ROLE.TEAM_FOUNDER;
        }
        else{
            return ROLE.LEAGUE_HOST;
        }
    }
}

@Service: ComponentScan의 대상이 되어 Bean으로 등록되게 해준다.

 

@Transactional: 데이터베이스의 상태를 작업 또는 동시에 수행되어야 하는 연산을 처리해줍니다. 또한 예외 발생시 롤백을 해줍니다.  (readonly=ture 속성을 주면 DB를 읽기만 한다는 것을 알려 성능 최적화를 해줍니다.)

 

@RequeiredArgsConstructor: private final로 선언된 필드 변수에 의존성을 주입해줍니다. 여기서는 UserRepository는 spring 에서 관리하는 bean이므로 해당 객체를 주입해줍니다.  

 

1. signup() 메소드는 받은 정보를 사용하여 아직 영속되지 않은 객체와 Entity를 생성합니다. 

  • 이때 user의 email은 아이디로서 unique한 값을 가집니다. 따라서 중복된 이메일로 생성하려 한다면, 예외를 터트려 진행할 수 없게 합니다.
  • 예외가 터지지 않았다면, userRepository.save() 메소드를 호출하여 저장합니다. (entity만 들어갈 수 있습니다.)

2. login() 메소드는 받은 정보를 사용하여 DB에 저장되어 있는 User의 email과 password를 비교합니다.

  • 이때 DB에 가입된 User의 email이 없거나, password가 틀렸을 때 예외를 터트려줍니다.

 

4. Exception 객체

public class UserEmailDuplicateException extends RuntimeException{
    public UserEmailDuplicateException() {
        super();
    }

    public UserEmailDuplicateException(String message) {
        super(message);
    }

    public UserEmailDuplicateException(String message, Throwable cause) {
        super(message, cause);
    }

    public UserEmailDuplicateException(Throwable cause) {
        super(cause);
    }

    protected UserEmailDuplicateException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}
public class UserEmailDuplicateException extends RuntimeException{
    public UserEmailDuplicateException() {
        super();
    }

    public UserEmailDuplicateException(String message) {
        super(message);
    }

    public UserEmailDuplicateException(String message, Throwable cause) {
        super(message, cause);
    }

    public UserEmailDuplicateException(Throwable cause) {
        super(cause);
    }

    protected UserEmailDuplicateException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

 

예외 처리이므로 RuntimeException을 상속받아 Override합니다.

 

5. UserServiceTest

단위 테스트로 UserRepository까지 모두 해야하지만 UserService에 대한 통합테스트만 진행하도록 하겠습니다.
@SpringBootTest
@Transactional
class UserServiceImplTest {

    @Autowired
    UserRepository userRepository;


    @Autowired
    UserService userService;


    @Test
    void userSignupSuccess() {
        Address address = getAddress();
        User user = getuser(address);
        Long signupUserId = userService.signup(user.getEmail(), user.getPassword(), user.getName()
                , "user", address);

        User result = userRepository.findById(signupUserId).get();

        assertThat(result.getEmail()).isEqualTo(user.getEmail());

    }

    @Test
    void userSignupFailDuplicateEmail() {
        Address address = getAddress();
        User user = getuser(address);
        User duplicateUser = getuser(address);
        Long signupUserId = userService.signup(user.getEmail(), user.getPassword(), user.getName()
                , "user", address);

        assertThatThrownBy(() -> userService.signup(user.getEmail(), user.getPassword(), user.getName()
                , "user", address)).isInstanceOf(UserEmailDuplicateException.class);

    }

    @Test
    void userLoginSuccess() {
        Address address = getAddress();
        User user = getuser(address);
        Long signupUserId = userService.signup(user.getEmail(), user.getPassword(), user.getName()
                , "user", address);
        Long signup = userService.signup("rkdlem1", "asdf", "kim", "user", address);
        User result = userRepository.findById(signup).get();

        assertThat(result.getEmail()).isEqualTo("rkdlem1");
    }

    @Test
    void userLoginFailDuplicateEmail() {
        Address address = getAddress();
        User user = getuser(address);
        Long signupUserId = userService.signup(user.getEmail(), user.getPassword(), user.getName()
                , "user", address);
        assertThatThrownBy(() -> userService.signup(user.getEmail(), user.getPassword(), user.getName()
                , "user", address)).isInstanceOf(UserEmailDuplicateException.class);


    }

    private Address getAddress() {
        return Address.builder()
                .street("s")
                .city("a")
                .build();
    }

    private User getuser(Address address) {
        return User.createUser("rkdlem48", "asdf", "kim", address, ROLE.MEMBER);
    }
}

@SpringBootTest: test를 SpringBoot 환경에서 test할 수 있게 해줍니다. 그래서 UserRepository와 UserService에 대한 의존성 주입이 됩니다.

 

@Transactional: 위에 설명드린 것과 같은 역할을 하며, 추가적으로 테스트 하나가 끝날때마다 롤백을 수행해줘 테스트끼리의 충돌을 방지합니다.

 

Assertions: test를 검증할 수 있도록 도와주는 라이브러리입니다. (assertJ 것을 import 해야합니다.)

  • assertThat(값).isEqualTo(우리가 원하는 기대 값)으로 값 검증입니다. 

userSignupSuccess를 실행했을 때 검증 성공 -> 우리가 만든 로직이 틀리지 않았다.

 

userSignupSuccess를 실행했을 때 검증 실패 -> 우리가 만든 로직에 문제가 있다.

실제 값과 우리가 예측한 값이 달라서 test가 실패했다고 알려줍니다. 
  • assertThatThrownBy( () -> 예외가 발생될 것이라고 생각되는 로직).isInstanceOf(발생되는 예외 클래스)는 예외가 적절하게 발생하고 처리하는지에 대한 검증입니다. 

user가 signup을 했을 때 email이 중복됐을 때 예외가 터지는지에 대하여 검증 성공 -> 적절하게 예외 처리했다.

 

user가 signup을 했을 때 예외가 터지지 않거나 예측한 예외가 다를 때 -> 적절하지 못한 예외처리를 했다.

 

UserRepository, Service, 예외 처리에 대하여 작성해봤습니다. 감사합니다. 

모든 코드는 아래 링크에서 확인 가능합니다.

https://github.com/rlaehdals/blogProject

 

GitHub - rlaehdals/blogProject

Contribute to rlaehdals/blogProject development by creating an account on GitHub.

github.com