개발을 시작하기 앞서, 1편에서는 하지 않았지만 도메인이 필요한 변수들은 요구사항을 설계할 때, 미리 설계했습니다. 이 점, 참고해주세요!
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 : 파라미터가 없는 생성자를 생성합니다.UserRolepublic enum UserRole {
NORMAL, ADMIN
}UserRepositorypublic interface UserRepository extends JpaRepository<User, Long> {
// 메서드를 통한 단일 데이터(User) 조회
Optional<User> findByEmail(String email);
}사용자의 회원가입(삽입), 조회, 수정, 삭제를 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 : @NonNull과 final 변수에 해당하는 생성자를 생성하여 자동적으로 빈컨테이너로부터 해당 클래스로 주입받을 수 있도록 합니다.@Transactional(readOnly = true)true로 설정하면, 해당 메서드는 데이터 조회만 가능하며 성능을 높일 수 있습니다. (참고 : seungh0님의 블로그)NotFoundUserExceptionpackage 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 상속)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;
}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);
}
}
ApiResultpackage 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);
}
}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를 실행합니다.@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());
}
}
}
UserFindApi와 UserSignApi에서 살펴볼 수 있듯이 각 컨트롤러는 그에 맞는 서비스 클래스를 통해 구현할 수 있습니다.UserUpdateApi와 UserDeleteApi 또한 각 역할에 맞는 서비스를 이용해 구현하기 때문에 생략하겠습니다.