@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
@MappedSuperclass
"이 클래스는 테이블로 만들지 말고, 이 클래스를 상속받는 엔티티들한테 필드만 물려줘" 라는 뜻
BaseEntity를 @Entity로 만들면 base_entity 테이블이 생겨버림. 그건 원하는 게 아니니까 @MappedSuperclass로 "나는 테이블 없이 필드만 공유하는 부모야" 라고 선언
abstract
Java에서 추상 클래스를 만들 때 쓰는 키워드로, 직접 객체를 생성할 수 없게 만들고 상속받아서만 사용 가능하게 하는 것
BaseEntity는 단독으로 쓸 이유가 없고 항상 상속받아서만 쓰는 클래스. 그래서 abstract를 붙여서 "나는 혼자 쓰는 클래스가 아니야!" 라고 명시함.
@Column(updatable = false)
DB 컬럼 설정으로 updatable = false는 "이 컬럼은 처음 INSERT할 때만 값 넣고, 이후에 UPDATE할 때는 건드리지 마" 라는 뜻
createdAt은 생성될 때 한번만 찍히고 이후에 절대 바뀌면 안 됨. 그래서 이걸 붙여서 실수로도 수정이 안 되게 막는 것
@Getter
@Entity
@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private String password;
public User(String name, String email, String password) {
this.name = name;
this.email = email;
this.password = password;
}
public void update(String name, String email) {
this.name = name;
this.email = email;
}
}
@Getter
@Entity
@Table(name = "schedules")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Schedule extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@OneToMany(mappedBy = "schedule")
private List<Comment> commentList = new ArrayList<>();
public Schedule(String title, String content, User user) {
this.title = title;
this.content = content;
this.user = user;
}
public void update(String title, String content) {
this.title = title;
this.content = content;
}
}
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@ManyToOne → 일정 여러 개 : 유저 한 명 관계
fetch = FetchType.LAZY → 일정 조회할 때 User를 즉시 안 가져오고 실제로 user 필드를 사용할 때만 가져옴 (성능상 좋음)
optional = false → user가 null이면 안 된다는 뜻, 일정은 반드시 유저가 있어야 함.
@JoinColumn(name = "user_id", nullable = false)
DB에서 user_id 컬럼으로 연결한다는 뜻
nullable = false → DB 레벨에서도 null 허용 안 함
@OneToMany(mappedBy = "schedule")
@OneToMany → 일정 한 개 : 댓글 여러 개 관계
mappedBy = "schedule" → "연관관계의 주인은 내가 아니라 Comment 엔티티의 schedule 필드야" 라는 뜻
User user 추가
일정 생성 시 반드시 어떤 유저의 일정인지 알아야하니까 user 객체를 받아서 넣음.
@Getter
@Entity
@Table(name = "comments")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String content;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "schedule_id", nullable = false)
private Schedule schedule;
public Comment(String content, User user, Schedule schedule) {
this.content = content;
this.user = user;
this.schedule = schedule;
}
}
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@ManyToOne → 댓글 여러 개 : 유저 한 명 관계
@ManyToOne → 댓글 여러 개 : 일정 한 개 관계
User user 추가
댓글 생성 시 반드시 어떤 유저의 댓글인지 알아야하니까 user 객체를 받아서 넣음.
Schedule schedule 추가
댓글 생성 시 반드시 어떤 일정의 댓글인지 알아야하니까 user 객체를 받아서 넣음.
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
JpaRepository 를 상속받으면 findById, findAll, save, delete 같은 기본 메서드들은 자동으로 제공되지만, 이메일로 유저 찾기는 기능에 없기 때문에 직접 만들어야하는데, JPA가 메서드 이름을 분석해서 자동으로 쿼리를 만들어줌.
Optional findByEmail(String email);
Optional → Java에서 값이 있을 수도 있고 없을 수도 있을 때 쓰는 wrapper 클래스로, 이걸 그냥 User로 반환하면 없을 때 null이 반환되는데, null을 그냥 쓰면 NullPointerException이 터질 수있음. 그래서 Optional<User>로 감싸서 반환하면 값이 있으면 User 꺼내고, 없으면 예외를 던짐.
findBy → 뒤에 필드명을 붙이면 JPA가 알아서 해당 컬럼(email)으로 조회하는 쿼리를 만들어서, String email 값으로 변환해줌.
public interface ScheduleRepository extends JpaRepository<Schedule, Long> {
}
public interface CommentRepository extends JpaRepository<Comment, Long> {
}
필드
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final PasswordEncoder passwordEncoder;
회원가입을 할 때 비밀번호를 암호화해야하고, 로그인할 때 입력한 비밀번호랑 DB에 저장된 암호화된 비밀번호를 비교해야하기 때문에 passwordEncoder를 필드로 가지고 있음.
@Component
public class PasswordEncoder {
public String encode(String rawPassword) {
return BCrypt.withDefaults().hashToString(BCrypt.MIN_COST, rawPassword.toCharArray());
}
public boolean matches(String rawPassword, String encodedPassword) {
BCrypt.Result result = BCrypt.verifyer().verify(rawPassword.toCharArray(), encodedPassword);
return result.verified;
}
}
@Component
Spring한테 “이 클래스를 Bean으로 등록해줘”라는 뜻
그래야 UserService 에서 @RequiredArgsconstructor 로 주입받아서 쓸 수 있음.
encode() 메서드
회원가입할 때 사용자가 입력한 비밀번호를 암호화해서 저장하는 메서드
rawPassword → 클라이언트가 보낸 평문 비밀번호
BCrypt → 비밀번호를 안전하게 저장하기 위한 해시 알고리즘으로, 비밀번호를 원래 값으로 되돌릴 수 없게 변환
BCrypt.withDefaults() → BCrypt 객체를 기본 옵션으로 가져옴.
.hashToString → 그 객체를 이용해서 비밀번호를 해시 문자열로 만듦.
BCrypt.MIN_COST → “얼마나 복잡하게 계산할거냐”, “얼마나 느리게 해시할 거냐”를 정하는 값으로, 현재 값은 BCrypt에서 가장 낮은 비용 값임. (가장 가볍고, 가장 빠름. 보안 강도는 상대적으로 낮은 편)
rawPassword.toCharArray() → 사용자가 입력한 비밀번호 문자열을 char 배열로 바꾸는 것
.hashToString(BCrypt.MIN_COST, rawPassword.toCharArray()) → 비밀번호를 BCrypt 방식으로 해시해서 문자열로 만듦.
matches() 메서드
로그인 할 때 입력한 비밀번호가 저장된 암호화 비밀번호와 맞는지 검사하는 메서드
BCrypt.verifyer() → 비밀번호 검사용 BCrypt 객체를 가져오는 것 (도구 꺼내오기)
.verify → 비밀번호가 맞는지 검사
.verify(rawPassword.toCharArray(), encodedPassword) → 사용자가 입력한 원본 비밀번호랑 암호화해서 저장되있는 비밀번호랑 비교
BCrypt.Result result → 검사를 완료한 후 그 결과를 객체형태로 result에 담음.
return result.verified; → 검사 성공 여부로, 돌려줘야하는 값은 참/거짓 이기 때문에 결과를 참/거짓으로 변환해서 반환
회원가입 (유저 생성) 로직
@Transactional
public UserCreateResponseDto save(UserCreateRequestDto requestDto) {
String encodedPassword = passwordEncoder.encode(requestDto.getPassword());
User user = new User(
requestDto.getName(),
requestDto.getEmail(),
encodedPassword
);
User savedUser = userRepository.save(user);
return new UserCreateResponseDto(
savedUser.getId(),
savedUser.getName(),
savedUser.getEmail(),
savedUser.getCreatedAt()
);
}
로그인 로직
@Transactional(readOnly = true)
public UserSessionDto login(UserLoginRequestDto requestDto) {
User user = userRepository.findByEmail(requestDto.getEmail()).orElseThrow(
InvalidCredentialsException::new
);
if (!passwordEncoder.matches(requestDto.getPassword(), user.getPassword())) {
throw new InvalidCredentialsException();
}
return new UserSessionDto(user.getId(), user.getEmail());
}
유저 다건 조회 로직
@Transactional(readOnly = true)
public List<UserGetAllResponseDto> getAll() {
List<User> userList = userRepository.findAll();
return userList.stream()
.map(user -> new UserGetAllResponseDto(
user.getId(),
user.getName()
))
.collect(Collectors.toList());
}
유저 단건 조회 로직
@Transactional(readOnly = true)
public UserGetOneResponseDto getOne(Long userId) {
User user = userRepository.findById(userId).orElseThrow(
UserNotFoundException::new
);
return new UserGetOneResponseDto(
user.getId(),
user.getName(),
user.getEmail(),
user.getCreatedAt(),
user.getUpdatedAt()
);
}
유저 수정 로직
@Transactional
public UserUpdateResponseDto update(Long userId, UserUpdateRequestDto requestDto) {
User user = userRepository.findById(userId).orElseThrow(
UserNotFoundException::new
);
user.update(requestDto.getName(), requestDto.getEmail());
return new UserUpdateResponseDto(
user.getId(),
user.getName(),
user.getEmail(),
user.getUpdatedAt()
);
}
유저 삭제 로직
@Transactional
public void delete(Long userId) {
boolean existence = userRepository.existsById(userId);
if (!existence) {
throw new UserNotFoundException();
}
userRepository.deleteById(userId);
}
필드
@Service
@RequiredArgsConstructor
public class ScheduleService {
private final ScheduleRepository scheduleRepository;
private final UserRepository userRepository;
private final UserRepository userRepository;
유저가 존재해야 일정이 존재할 수 있으므로 SchduleService는 UserRepository를 필드로 가짐.
일정 생성 로직
@Transactional
public ScheduleCreateResponseDto save(ScheduleCreateRequestDto requestDto) {
User user = userRepository.findById(requestDto.getUserId()).orElseThrow(
UserNotFoundException::new
);
Schedule schedule = new Schedule(
requestDto.getTitle(),
requestDto.getContent(),
user
);
Schedule savedSchedule = scheduleRepository.save(schedule);
return new ScheduleCreateResponseDto(
savedSchedule.getId(),
savedSchedule.getTitle(),
savedSchedule.getContent(),
savedSchedule.getUser().getId(),
savedSchedule.getCreatedAt()
);
}
일정 다건 조회 로직
@Transactional(readOnly = true)
public List<ScheduleGetAllResponseDto> getAll() {
List<Schedule> scheduleList = scheduleRepository.findAll();
return scheduleList.stream()
.map(schedule -> new ScheduleGetAllResponseDto(
schedule.getId(),
schedule.getTitle(),
schedule.getUser().getId(),
schedule.getCreatedAt()
))
.collect(Collectors.toList());
}
일정 단건 조회 로직
@Transactional(readOnly = true)
public ScheduleGetOneResponseDto getOne(Long scheduleId) {
Schedule schedule = scheduleRepository.findById(scheduleId).orElseThrow(
ScheduleNotFoundException::new
);
return new ScheduleGetOneResponseDto(
schedule.getId(),
schedule.getTitle(),
schedule.getContent(),
schedule.getUser().getId(),
schedule.getCreatedAt(),
schedule.getUpdatedAt()
);
}
일정 수정 로직
@Transactional
public ScheduleUpdateResponseDto update(Long scheduleId, ScheduleUpdateRequestDto requestDto) {
Schedule schedule = scheduleRepository.findById(scheduleId).orElseThrow(
ScheduleNotFoundException::new
);
schedule.update(requestDto.getTitle(), requestDto.getContent());
return new ScheduleUpdateResponseDto(
schedule.getId(),
schedule.getTitle(),
schedule.getContent(),
schedule.getUser().getId(),
schedule.getUpdatedAt()
);
}
일정 삭제 로직
@Transactional
public void delete(Long scheduleId) {
boolean existence = scheduleRepository.existsById(scheduleId);
if (!existence) {
throw new ScheduleNotFoundException();
}
scheduleRepository.deleteById(scheduleId);
}
일정 페이지 조회 로직
@Transactional(readOnly = true)
public Page<SchedulePageResponseDto> getPage(int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "updatedAt"));
return scheduleRepository.findAll(pageable)
.map(schedule -> new SchedulePageResponseDto(
schedule.getTitle(),
schedule.getContent(),
schedule.getCommentList().size(),
schedule.getCreatedAt(),
schedule.getUpdatedAt(),
schedule.getUser().getName()
));
}
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "updatedAt"));
PageRequest.of → 페이징 조건 객체를 만드는 코드
page → 몇 번째 페이지
size → 한 페이지에 몇 개
Sort.by → 정렬 조건
Sort.Direction.DESC → 내림차순 정렬 (ASC → 오름차순)
"updatedAt" → 정렬 기준 필드명 (즉, 수정일자 기준으로 정렬)
필드
@Service
@RequiredArgsConstructor
public class CommentService {
private final CommentRepository commentRepository;
private final UserRepository userRepository;
private final ScheduleRepository scheduleRepository;
댓글은 유저가 존재해야만 작성될 수 있고, 일정이 존재해야만 작성 할 수 있기때문에 필드로 UserRepository와 ScheduleRepository를 가짐.
댓글 생성 로직
@Transactional
public CommentCreateResponseDto save(CommentCreateRequestDto requestDto) {
User user = userRepository.findById(requestDto.getUserId()).orElseThrow(
UserNotFoundException::new
);
Schedule schedule = scheduleRepository.findById(requestDto.getScheduleId()).orElseThrow(
ScheduleNotFoundException::new
);
Comment comment = new Comment(
requestDto.getContent(),
user,
schedule
);
Comment savedComment = commentRepository.save(comment);
return new CommentCreateResponseDto(
savedComment.getId(),
savedComment.getSchedule().getId(),
savedComment.getUser().getId(),
savedComment.getContent(),
savedComment.getCreatedAt()
);
}
댓글 다건 조회 로직
@Transactional(readOnly = true)
public List<CommentGetAllResponseDto> getAll() {
List<Comment> commentList = commentRepository.findAll();
return commentList.stream()
.map(comment -> new CommentGetAllResponseDto(
comment.getId(),
comment.getSchedule().getId(),
comment.getUser().getId(),
comment.getContent(),
comment.getCreatedAt(),
comment.getUpdatedAt()
))
.collect(Collectors.toList());
}
필드
@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserController {
private final UserService userService;
회원가입 (유저 생성) API
@PostMapping
public ResponseEntity<UserCreateResponseDto> userCreate(@Valid @RequestBody UserCreateRequestDto requestDto) {
return ResponseEntity.status(HttpStatus.CREATED).body(userService.save(requestDto));
}
@Getter
public class UserCreateRequestDto {
@NotBlank(message = "이름은 필수입니다.")
@Size(max = 5, message = "이름은 5글자 이내여야 합니다.")
private String name;
@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
private String email;
@NotBlank(message = "비밀번호는 필수입니다.")
@Size(min = 8, message = "비밀번호는 최소 8글자 이상이여야 합니다.")
private String password;
}
@Getter
@RequiredArgsConstructor
public class UserCreateResponseDto {
private final Long id;
private final String name;
private final String email;
private final LocalDateTime createdAt;
}
로그인 API
@PostMapping("/login")
public ResponseEntity<String> login(@Valid @RequestBody UserLoginRequestDto requestDto, HttpSession session) {
UserSessionDto userSessionDto = userService.login(requestDto);
session.setAttribute("loginUser", userSessionDto);
return ResponseEntity.ok("로그인 성공!");
}
session.setAttribute("loginUser", userSessionDto);
.setAttribute("loginUser", userSessionDto) → 세션 안에 값을 저장하는 메서드로, (키 : ”이름”, 값)
@Getter
public class UserLoginRequestDto {
@NotBlank(message = "이메일을 입력해주세요.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
private String email;
@NotBlank(message = "비밀번호를 입력해주세요.")
private String password;
}
@Getter
@RequiredArgsConstructor
public class UserSessionDto {
private final Long id;
private final String email;
}
로그아웃 API
@PostMapping("/logout")
public ResponseEntity<String> logout(HttpSession session) {
session.invalidate();
return ResponseEntity.ok("로그아웃 성공!");
}
session.invalidate();
session.invalidate() → 현재 세션을 삭제하는 메서드
유저 다건 조회 API
@GetMapping
public ResponseEntity<List<UserGetAllResponseDto>> userGetAll() {
return ResponseEntity.ok(userService.getAll());
}
@Getter
@RequiredArgsConstructor
public class UserGetAllResponseDto {
private final Long id;
private final String name;
}
유저 단건 조회 API
@GetMapping("/{userId}")
public ResponseEntity<UserGetOneResponseDto> userGetOne(@PathVariable Long userId) {
return ResponseEntity.ok(userService.getOne(userId));
}
@Getter
@RequiredArgsConstructor
public class UserGetOneResponseDto {
private final Long id;
private final String name;
private final String email;
private final LocalDateTime createdAt;
private final LocalDateTime updatedAt;
}
유저 수정 API
@PutMapping("/{userId}")
public ResponseEntity<UserUpdateResponseDto> userUpdate(@PathVariable Long userId, @Valid @RequestBody UserUpdateRequestDto requestDto, HttpSession session) {
UserSessionDto loginUser = (UserSessionDto) session.getAttribute("loginUser");
if (session.getAttribute("loginUser") == null) {
throw new UnauthorizedException();
}
if (!loginUser.getId().equals(userId)) {
throw new ForbiddenException();
}
return ResponseEntity.ok(userService.update(userId, requestDto));
}
@Getter
public class UserUpdateRequestDto {
@NotBlank(message = "이름은 필수입니다.")
@Size(max = 5, message = "이름은 5글자 이내여야 합니다.")
private String name;
@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
private String email;
}
@Getter
@RequiredArgsConstructor
public class UserUpdateResponseDto {
private final Long id;
private final String name;
private final String email;
private final LocalDateTime updatedAt;
}
유저 삭제 API
@DeleteMapping("/{userId}")
public ResponseEntity<Void> userDelete(@PathVariable Long userId, HttpSession session) {
UserSessionDto loginUser = (UserSessionDto) session.getAttribute("loginUser");
if (loginUser == null) {
throw new ForbiddenException();
}
if (session.getAttribute("loginUser") == null) {
throw new UnauthorizedException();
}
userService.delete(userId);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
필드
@RestController
@RequiredArgsConstructor
@RequestMapping("/schedules")
public class ScheduleController {
private final ScheduleService scheduleService;
일정 생성 API
@PostMapping
public ResponseEntity<ScheduleCreateResponseDto> scheduleCreate(@Valid @RequestBody ScheduleCreateRequestDto requestDto, HttpSession session) {
if (session.getAttribute("loginUser") == null) {
throw new UnauthorizedException();
}
return ResponseEntity.status(HttpStatus.CREATED).body(scheduleService.save(requestDto));
}
@Getter
public class ScheduleCreateRequestDto {
@NotBlank(message = "제목은 필수입니다.")
@Size(max = 10, message = "제목은 10글자 이내여야 합니다.")
private String title;
private String content;
@NotNull(message = "유저 아이디는 필수입니다.")
private Long userId;
}
@Getter
@RequiredArgsConstructor
public class ScheduleCreateResponseDto {
private final Long id;
private final String title;
private final String content;
private final Long userId;
private final LocalDateTime createdAt;
}
일정 다건 조회 API
@GetMapping
public ResponseEntity<List<ScheduleGetAllResponseDto>> scheduleGetAll() {
return ResponseEntity.ok(scheduleService.getAll());
}
@Getter
@RequiredArgsConstructor
public class ScheduleGetAllResponseDto {
private final Long id;
private final String title;
private final Long userId;
private final LocalDateTime createdAt;
}
일정 단건 조회 API
@GetMapping("/{scheduleId}")
public ResponseEntity<ScheduleGetOneResponseDto> scheduleGetOne(@PathVariable Long scheduleId) {
return ResponseEntity.ok(scheduleService.getOne(scheduleId));
}
@Getter
@RequiredArgsConstructor
public class ScheduleGetOneResponseDto {
private final Long id;
private final String title;
private final String content;
private final Long userId;
private final LocalDateTime createdAt;
private final LocalDateTime updatedAt;
}
일정 수정 API
@PutMapping("/{scheduleId}")
public ResponseEntity<ScheduleUpdateResponseDto> scheduleUpdate(@PathVariable Long scheduleId, @Valid @RequestBody ScheduleUpdateRequestDto requestDto, HttpSession session) {
if (session.getAttribute("loginUser") == null) {
throw new UnauthorizedException();
}
return ResponseEntity.ok(scheduleService.update(scheduleId, requestDto));
}
@Getter
public class ScheduleUpdateRequestDto {
@NotBlank(message = "제목은 필수입니다.")
@Size(max = 10, message = "제목은 10글자 이내여야 합니다.")
private String title;
private String content;
}
@Getter
@RequiredArgsConstructor
public class ScheduleUpdateResponseDto {
private final Long id;
private final String title;
private final String content;
private final Long userId;
private final LocalDateTime updatedAt;
}
일정 삭제 API
@DeleteMapping("/{scheduleId}")
public ResponseEntity<Void> scheduleDelete(@PathVariable Long scheduleId, HttpSession session) {
if (session.getAttribute("loginUser") == null) {
throw new UnauthorizedException();
}
scheduleService.delete(scheduleId);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
일정 페이지 조회 API
@GetMapping("/page")
public ResponseEntity<Page<SchedulePageResponseDto>> getPage(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return ResponseEntity.ok(scheduleService.getPage(page, size));
}
필드
@RestController
@RequiredArgsConstructor
@RequestMapping("/comments")
public class CommentController {
private final CommentService commentService;
댓글 생성 API
@PostMapping
public ResponseEntity<CommentCreateResponseDto> commentCreate(@Valid @RequestBody CommentCreateRequestDto requestDto, HttpSession session) {
if (session.getAttribute("loginUser") == null) {
throw new UnauthorizedException();
}
return ResponseEntity.status(HttpStatus.CREATED).body(commentService.save(requestDto));
}
@Getter
public class CommentCreateRequestDto {
@NotNull(message = "일정 아이디는 필수입니다.")
private Long scheduleId;
@NotNull(message = "유저 아이디는 필수입니다.")
private Long userId;
@NotBlank(message = "댓글 내용은 필수입니다.")
private String content;
}
@Getter
@RequiredArgsConstructor
public class CommentCreateResponseDto {
private final Long id;
private final Long scheduleId;
private final Long userId;
private final String content;
private final LocalDateTime createdAt;
}
댓글 다건 조회 API
@GetMapping
public ResponseEntity<List<CommentGetAllResponseDto>> commentGetAll() {
return ResponseEntity.ok(commentService.getAll());
}
@Getter
@RequiredArgsConstructor
public class CommentGetAllResponseDto {
private final Long id;
private final Long scheduleId;
private final Long userId;
private final String content;
private final LocalDateTime createdAt;
private final LocalDateTime updatedAt;
}
@Getter
public class ServiceException extends RuntimeException {
private final HttpStatus status;
public ServiceException(HttpStatus status, String message) {
super(message);
this.status = status;
}
}
서비스에서 발생하는 예외에 “상태코드”와 “메시지”를 함께 담기 위한 공통 예외 클래스
extends RuntimeException
RuntimeException → 언체크 예외라서 throws를 안 써도 됨. 그래서 스프링에서는 보통 서비스 예외를 만들 때 RuntimeException을 많이 상속함.
private final HttpStatus status;
HttpStatus → 스프링에서 제공하는 HTTP 상태코드 (enum)
status → 예외가 발생했을 때 어떤 HTTP 상태코드로 응답할지 저장하는 필드
생성자 부분
super(message) → 부모 클래스인 RuntimeException한테 메시지를 넘겨줌.
this.status = status → 매개변수로 받은 상태코드를 현재 객체 필드에 저장함.
public class UserNotFoundException extends ServiceException {
public UserNotFoundException() {
super(HttpStatus.NOT_FOUND, "해당 유저가 존재하지 않습니다.");
}
}
public class ScheduleNotFoundException extends ServiceException {
public ScheduleNotFoundException() {
super(HttpStatus.NOT_FOUND, "해당 일정이 존재하지 않습니다.");
}
}
public class InvalidCredentialsException extends ServiceException {
public InvalidCredentialsException() {
super(HttpStatus.UNAUTHORIZED, "이메일 또는 비밀번호가 일치하지 않습니다.");
}
}
public class UnauthorizedException extends ServiceException {
public UnauthorizedException() {
super(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
}
}
public class ForbiddenException extends ServiceException {
public ForbiddenException() {
super(HttpStatus.FORBIDDEN, "본인만 수정/삭제할 수 있습니다.");
}
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ServiceException.class)
public ResponseEntity<String> handleServiceException(ServiceException ex) {
return ResponseEntity.status(ex.getStatus()).body(ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
String errorMessage = ex.getBindingResult().getFieldErrors().stream()
.findFirst()
.map(fieldError -> fieldError.getDefaultMessage())
.orElse("입력 값이 올바르지 않습니다.");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMessage);
}
}
전역 예외 처리 클래스로, 프로젝트 전체에서 발생한 예외를 한 곳에서 처리
handleServiceException 메서드
@RestControllerAdvice → 애플리케이션 전체에서 발생하는 예외를 한 곳에서 처리해주는 전역 설정 어노테이션으로, 모든 @RestController에 적용됨. (어디서 예외가 터지든 여기로 옴.)
@ExceptionHandler → 특정 예외가 발생했을 때 실행할 메서드를 지정하는 어노테이션
@ExceptionHandler(ServiceException.class) → ServiceException이 발생하면 이 메서드가 대신 처리함.
ResponseEntity<String> → HTTP 응답 객체를 반환하고, 응답 본문(body)이 문자열이라는 뜻
handleMethodArgumentNotValidException 메서드
@ExceptionHandler(MethodArgumentNotValidException.class) → MethodArgumentNotValidException이 발생하면 이 메서드가 대신 처리함.
ex.getBindingResult() → 검증 결과 전체를 가져옴.
.getFieldErrors() → 필드 단위 에러들만 꺼내는 것 (목록처럼 들고 있음.)
.findFirst() → 첫 번째 에러 하나만 가져옴.
.map(fieldError -> fieldError.getDefaultMessage()) → findFirst()로 꺼낸 첫 번째 필드 에러 객체에 들어있는 기본 메시지를 꺼냄.