개발을 시작하기 앞서, 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
: 파라미터가 없는 생성자를 생성합니다.UserRole
public enum UserRole {
NORMAL, ADMIN
}
UserRepository
public 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님의 블로그)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
상속)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);
}
}
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);
}
}
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
또한 각 역할에 맞는 서비스를 이용해 구현하기 때문에 생략하겠습니다.