[Spring] 나만의 게시판 만들기 2 - User

최진민·2022년 2월 3일
0

게시판 만들기

목록 보기
2/9
post-thumbnail

개발을 시작하기 앞서, 1편에서는 하지 않았지만 도메인이 필요한 변수들은 요구사항을 설계할 때, 미리 설계했습니다. 이 점, 참고해주세요!

Model


  • User
    @Entity
    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public class User {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "user_id")
        private Long user_id;
    
        @Column(name = "email")
        private String email;
    
        @Column(name = "password")
        private String password;
    
        @Column(name = "name")
        private String name;
    
        @Column(name = "device_token")
        private String deviceToken;
    
        @Enumerated(value = EnumType.STRING)
        private UserRole userRole;
    }
    • @Entity : 클래스를 엔티티로 지정합니다.
    • @Getter : 멤버 변수들의 getter를 자동적으로 생성합니다. (실무에서 setter는 지양합니다.)
    • @NoArgsConstructor : 파라미터가 없는 생성자를 생성합니다.
      • access : 생성자의 접근 제한자를 지정합니다.
    • 클래스 내에 필요한 변수들을 기입합니다.
      • 1 - 구조편에서 설계한 API에 의하면 도메인 별로 연관관계가 주어져있기 때문에, 도메인을 개발해나가며 필요한 연관관계들을 설정해줍니다.
  • UserRole
    public enum UserRole {
    		NORMAL, ADMIN
    }
    • 해당 프로젝트에서 ADMIN과 관련된 개발은 없습니다.

Repository


  • UserRepository
    public interface UserRepository extends JpaRepository<User, Long> {
    
    		// 메서드를 통한 단일 데이터(User) 조회
        Optional<User> findByEmail(String email);
    }
    • Spring data Jpa를 활용한 레포지토리 생성방법입니다.
      • JPA와 Spring data jpa는 따로 공부하는 것을 권장합니다. (굉장히 중요)
      • Optional 또한 따로 공부하는 것을 권장합니다.

Service


사용자의 회원가입(삽입), 조회, 수정, 삭제를 Spring Data Jpa의 CRUD를 통해 구현합니다.


UserFindService

@Service
@RequiredArgsConstructor
public class UserFindService {
    private final UserRepository userRepository;

    @Transactional(readOnly = true)
    public User findById(Long userId) {
        return userRepository.findById(userId)
                .orElseThrow(() -> new NotFoundUserException(String.format("There is no Id : %s", userId)));
    }

    @Transactional(readOnly = true)
    public User findByEmail(String email) {
        return userRepository.findByEmail(email)
                .orElseThrow(() -> new NotFoundUserException(String.format("There is no email : %s, You need to SignUp", email)));
    }

    @Transactional(readOnly = true)
    public List<User> findAll() {
        return userRepository.findAll();
    }
}
  • @Service : 서비스 클래스를 가리키기 위함입니다.
  • @RequiredArgsConstructor : @NonNullfinal 변수에 해당하는 생성자를 생성하여 자동적으로 빈컨테이너로부터 해당 클래스로 주입받을 수 있도록 합니다.
  • ⭐️ @Transactional(readOnly = true)
    • 트랜잭션 내에서의 작업을 보장합니다.
    • 특히, readOnly 옵션을 true로 설정하면, 해당 메서드는 데이터 조회만 가능하며 성능을 높일 수 있습니다. (참고 : seungh0님의 블로그)
  • 반환 타입에 맞도록 단일 조회, 전체 조회 메서드를 레포지토리의 메서드를 호출하여 Optional의 예외처리에 맞도록 구현합니다.
    • NotFoundUserException
      package me.jinmin.boardver2.user.exception;
      
      public class NotFoundUserException extends RuntimeException {
          public NotFoundUserException(String msg) {
              super(msg);
          }
      }
      • RuntimeException을 상속받은 custom 예외 클래스입니다. 가장 기본적인 형식은 지양해야 하고 필요에 따라 예외처리를 하는 것이 좋습니다.

UserSignService

@Service
@RequiredArgsConstructor
public class UserSignService {

    private final UserRepository userRepository;
    private final UserFindService userFindService;

    @Transactional
    public Long signUp(UserSignUpRequest userSignUpRequest) {
        checkDuplicatedEmail(userSignUpRequest.getEmail());
        User user = User.builder()
                .email(userSignUpRequest.getEmail())
                .password(userSignUpRequest.getPassword())
                .name(userSignUpRequest.getName())
                .userRole(UserRole.NORMAL)
                .build();
        User savedUser = userRepository.save(user);
        return savedUser.getUser_id();
    }

    @Transactional
    public Long logIn(UserLogInRequest userLogInRequest) {
        User user = userFindService.findByEmail(userLogInRequest.getEmail());
        checkMatchedPassword(userLogInRequest.getPassword(), user.getPassword());
        user.modifyDeviceToken(userLogInRequest.getDeviceToken());
        return user.getUser_id();
    }

    private void checkDuplicatedEmail(String email) {
        if (userRepository.findByEmail(email).isPresent()) {
            throw new DuplicatedEmailException(String.format("[ %s ]" + " already exist.", email));
        }
    }

    private void checkMatchedPassword(String loginPassword, String userPassword) {
        if (!loginPassword.equals(userPassword)) {
            throw new UnMatchedPasswordException(String.format("Password is not matched"));
        }
    }
}
  • 서비스 클래스간 의존은 가능하지만, 계층 구조에 한해서 가능합니다. 서로가 의존하는 것은 지양합니다.
    • 조회 서비스를 활용하여 가입 및 로그인 시 중복을 체크할 수 있습니다.
      • checkDuplicatedEmail(), checkMatchedPassword()
      • 또한, 위와 같은 private 메서드를 통해 예외 처리를 커스텀하게 처리할 수도 있습니다. (RuntimeException 상속)
  • 서비스 단에서 중요한 점은 데이터 접근시 엔티티에 직접 접근하는 것을 최대한 지양하고 DTO의 형태로 접근해야합니다.
    • UserSignUpRequest
      //@Data도 무방 why? DTO이기 때문에 setter를 통해 데이터 변경도 가능하다.
      //getter가 없으면 웹과 데이터 통신 간 오류가 발생할 수 있다.
      @Getter
      @NoArgsConstructor
      public class UserSignUpRequest {
          private String email;
          private String password;
          private String name;
      }
    • UserLogInRequest
      @Getter
      @NoArgsConstructor
      public class UserLogInRequest {
          private String email;
          private String password;
          private String deviceToken;
      }
  • Builder를 통해서 새로운 회원의 정보를 생성하고 레포지토리를 활용해 저장소에 저장합니다.
    public class UserSignService {
    
    		//...
        @Transactional
        public Long signUp(UserSignUpRequest userSignUpRequest) {
            checkDuplicatedEmail(userSignUpRequest.getEmail());
            User user = User.builder()
                    .email(userSignUpRequest.getEmail())
                    .password(userSignUpRequest.getPassword())
                    .name(userSignUpRequest.getName())
                    .userRole(UserRole.NORMAL)
                    .build();
            User savedUser = userRepository.save(user);
            return savedUser.getUser_id();
        }
    		//...
    }
    public class User {
    		//...
    		@Builder
        public User(String email, String password, String name, UserRole userRole) {
            this.email = email;
            this.password = password;
            this.name = name;
            this.userRole = userRole;
        }
    		//...
    }

UserUpdateService

@Service
@RequiredArgsConstructor
public class UserUpdateService {

    private final UserFindService userFindService;

    @Transactional
    public Long update(Long userId, UserUpdateRequest userUpdateRequest) {
        User findUser = userFindService.findById(userId);
        User updatedUser = findUser.updateUserInfo(
                userUpdateRequest.getName(),
                userUpdateRequest.getPassword()
        );
        return updatedUser.getUser_id();
    }
}
  • UserUpdateRequest : DTO 활용
    @Getter
    @NoArgsConstructor
    public class UserUpdateRequest {
        private String name;
        private String password;
    }
  • 수정은 따로 저장하는 라인이 없습니다. ⇒ JPA의 변경감지를 이용한 데이터 수정이 이뤄집니다. (JPA 따로 공부 필수!!)
    public class User {
    		//...
    		public User updateUserInfo(String name, String password) {
            this.name = name;
            this.password = password;
            return this;
        }
    		//...
    }
    • setter를 지양하고 필요에 따라 메서드명을 올바르게 정의하여 수정하는 작업을 합니다.

UserDeleteService

@Service
@RequiredArgsConstructor
public class UserDeleteService {

    private final UserRepository userRepository;

    @Transactional
    public void deleteUserById(Long userId) {
        userRepository.deleteById(userId);
    }
}
  • JPA의 delete를 활용합니다.

Controller


  • ApiResult
    package me.jinmin.boardver2.util;
    
    import lombok.Getter;
    
    @Getter
    public class ApiResult<T> {
        private final T data;
        private final String message;
    
        public ApiResult(T data, String message) {
            this.data = data;
            this.message = message;
        }
    
        public static <T> ApiResult<T> succeed(T data) {
            return new ApiResult<>(data, null);
        }
    
        public static <T> ApiResult<T> failed(Throwable throwable) {
            return new ApiResult<>(null, throwable.getMessage());
        }
    
        public static <T> ApiResult<T> failed(String message) {
            return new ApiResult<>(null, message);
        }
    }
    • 동일한 API 결과를 도출하기 위해 커스텀 클래스를 구현합니다.
      • 제네릭 타입을 활용해 코드의 유연성을 높입니다.

UserFindApi

@Slf4j
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserFindApi {

    private final UserFindService userFindService;

    @GetMapping("/{userId}")
    public ApiResult<User> findById(@PathVariable Long userId) {
        try {
            return ApiResult.succeed(userFindService.findById(userId));
        } catch (Exception e) {
            log.error(e.getMessage());
            return ApiResult.failed(e.getMessage());
        }
    }

    @GetMapping()
    public ApiResult<List<User>> findUsers() {
        try {
            return ApiResult.succeed(userFindService.findAll());
        } catch (Exception e) {
            log.error(e.getMessage());
            return ApiResult.failed(e.getMessage());
        }
    }
}
  • @Slf4j : 로그를 활용할 수 있도록 하는 애노테이션입니다.
    • log.xxx()
  • @RestController : @ResponseBody + @Controller
    • 컨트롤러 클래스를 나타내며, @ResponseBody는 메서드 반환 값을 웹 응답 본문에 바인딩하도록 하는 애노테이션입니다.
  • @RequestMapping("xxx") : URL(xxx)을 통해 HTTP Method를 실행합니다.
    • include @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, or @PatchMapping.
    • xxx에는 파라미터(값)가 전달될 수도 있습니다.

UserSignApi

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserSignApi {

    private final UserSignService userSignService;

    @PostMapping("/signup")
    public ApiResult<Long> signUp(@RequestBody UserSignUpRequest userSignUpRequest) {
        try {
            Long userId = userSignService.signUp(userSignUpRequest);
            return ApiResult.succeed(userId);
        } catch (Exception e) {
            log.error(e.getMessage());
            return ApiResult.failed(e.getMessage());
        }
    }

    @PostMapping("/login")
    public ApiResult<Long> login(@RequestBody UserLogInRequest userLogInRequest) {
        try {
            Long userId = userSignService.logIn(userLogInRequest);
            return ApiResult.succeed(userId);
        } catch (Exception e) {
            log.error(e.getMessage());
            return ApiResult.failed(e.getMessage());
        }
    }
}
  • UserFindApiUserSignApi에서 살펴볼 수 있듯이 각 컨트롤러는 그에 맞는 서비스 클래스를 통해 구현할 수 있습니다.
    • 계층 구조 : 레포지토 - 서비스 - 컨트롤러
    • 서비스와 컨트롤러는 엔티티에 직접 접근하지 않고 DTO를 통해 통해 접근하는 것을 지향합니다.
  • UserUpdateApiUserDeleteApi 또한 각 역할에 맞는 서비스를 이용해 구현하기 때문에 생략하겠습니다.
profile
열심히 해보자9999

0개의 댓글