To Do List
- ResponseEntity 이해
- JPA 어노테이션 (@Entity, @Id, @GeneratedValue 등)
- 연관관계 (@ManyToOne, @JoinColumn)
- Cookie/Session 인증/인가
- Bean Validation (@Valid 등)
Part 1 : ResponseEntity
✔️ 개념 설명
Spring에서 API를 만들 때, 클라이언트에게 응답을 돌려줄 때 쓰는 클래스
일반적으로 그냥 값만 리턴할 수도 있지만, ResponseEntity를 쓰면
- HTTP 상태 코드 (200, 400, 404 등)
- 응답 헤더
- 응답 바디 (데이터)
이 세 가지를 한 번에 제어할 수 있다.
✔️ 예시 비교
ResponseEntity 없이
@GetMapping("/user/{id}")
public UserResponseDto getUser(@PathVariable Long id) {
return userService.getUser(id);
}
→ 데이터는 주지만, HTTP 상태 코드를 직접 제어 못함
ResponseEntity 사용
@GetMapping("/user/{id}")
public ResponseEntity<UserResponseDto> getUser(@PathVariable Long id) {
UserResponseDto user = userService.getUser(id);
return ResponseEntity.ok(user); // 200 OK + 데이터
}
🍬 자주 쓰는 패턴
// 200 OK
return ResponseEntity.ok(data);
// 201 Created (생성 성공)
return ResponseEntity.status(HttpStatus.CREATED).body(data);
// 400 Bad Request
return ResponseEntity.badRequest().body("잘못된 요청입니다");
// 404 Not Found
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("유저를 찾을 수 없습니다");
🕶️ 실전 예시(전체 흐름)
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@PostMapping
public ResponseEntity<UserResponseDto> createUser(@RequestBody UserRequestDto requestDto) {
UserResponseDto responseDto = userService.createUser(requestDto);
return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); // 201
}
@GetMapping("/{id}")
public ResponseEntity<UserResponseDto> getUser(@PathVariable Long id) {
UserResponseDto responseDto = userService.getUser(id);
return ResponseEntity.ok(responseDto); // 200
}
}
⭐️ Key Point ⭐️
상황코드HTTP 상태
| 조회 성공 | ResponseEntity.ok(data) | 200 |
| 생성 성공 | ResponseEntity.status(CREATED).body(data) | 201 |
| 잘못된 요청 | ResponseEntity.badRequest().body(msg) | 400 |
| 찾을 수 없음 | ResponseEntity.status(NOT_FOUND).body(msg) | 404 |
Part 2 : JPA 어노테이션
✔️ 개념 설명
JPA는 Java 객체와 데이터베이스 테이블을 자동으로 연결해주는 기술
어노테이션을 클래스/필드에 붙여주면 JPA가 알아서 테이블을 만들고 관리
✔️ 핵심 어노테이션 살펴보기
🍬 @Entity
@Entity
public class User {
// 이 클래스가 DB 테이블과 연결된다는 선언
}
- 이 클래스가 DB 테이블과 매핑된다고 JPA에게 알려주는 것
- @Entity가 없으면 JPA가 그냥 일반 클래스로 취급
🍬 @Id
@Entity
public class User {
@Id // 이 필드가 기본 키(PK)야!
private Long id;
}
- 테이블의 Primary Key가 될 필드에 붙인다
🍬 @GeneratedValue
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // DB가 알아서 1, 2, 3... 자동 증가
}
- PK를 자동으로 생성하는 전략을 설정
- IDENTITY = DB가 알아서 번호 매겨줌 (MySQL의 AUTO_INCREMENT)
🕶️ 실전 예시(전체 Entity)
@Entity
@Table(name = "users") // 테이블 이름을 "users"로 지정 (생략하면 클래스명 그대로)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false) // NOT NULL 조건
private String username;
@Column(nullable = false, unique = true) // 중복 불가
private String email;
private String password;
}
- 이렇게 클래스 하나를 만들면 테이블이 자동으로 생성
| id | username | password | |
| 1 | hong | hong@test.com | 1234 |
| 2 | kim | kim@test.com | 5678 |
🍬 @Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// JPA가 기본 CRUD를 자동으로 제공
// save(), findById(), findAll(), delete() 등등
}
- DAO 역할을 하는 인터페이스에 붙인다
- JpaRepository<엔티티, PK타입> 를 상속받으면 기본 CRUD 메서드 사용 가능
DAO (Data Access Object)란?
데이터베이스에 접근하는 역할만 담당하는 객체 (데이터 조회 / 저장 / 수정 / 삭제)
🍬 @NoArgsConstructor / @AllArgsConstructor / @RequiredArgsConstructor (feat. Lombok)
JPA는 기본 생성자가 반드시 필요
@Entity
@NoArgsConstructor // 기본 생성자 자동 생성 (JPA 필수!)
@AllArgsConstructor // 전체 필드 생성자 자동 생성
@RequiredArgsConstructor // final 필드만 생성자 생성
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
}
🍬 @Getter / @Setter (feat. Lombok)
JPA Entity에서 필드에 접근하려면 getter/setter가 필요한데, Lombok으로 자동 생성
@Entity
@Getter // 모든 필드의 getter 자동 생성
@Setter // 모든 필드의 setter 자동 생성
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
}
단, Entity에 @Setter는 신중하게 써야 한다.
아무데서나 값을 바꿀 수 있어서 나중에 버그 찾기 힘들어지기 때문에 필요한 메서드만 따로 만드는 것을 추천!
🕶️ 최종 실전 Entity 예시
@Entity
@Getter
@NoArgsConstructor
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String username;
@Column(nullable = false, unique = true)
private String email;
private String password;
// 생성자는 필요한 필드만 받아서 직접 만들기
public User(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
}
}
Part 3 : 연관관계 (@ManyToOne, @OneToMany)
✔️ 개념 설명
DB 테이블끼리 관계가 있을 때 JPA에서 표현하는 방법이다
예시 : 게시글(Post) 과 유저(User) 관계
- 유저 1명은 게시글을 여러 개 쓸 수 있다
- 게시글 1개는 한 명의 유저가 작성
🍬 @ManyToOne - 많은 쪽에 붙인다
@Entity
@Getter
@NoArgsConstructor
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
@ManyToOne // Post → User
@JoinColumn(name = "user_id") // FK 컬럼 이름 지정
private User user;
}
posts 테이블에 user_id 컬럼이 자동으로 생긴다
| id | title | content | user_id |
| 1 | 안녕 | 내용1 | 1 |
| 2 | 반가워 | 내용2 | 1 |
| 3 | 잘가 | 내용3 | 2 |
@ManyToOne 읽는 법 : "나(Many)는 저쪽(One)에 속해있어!"
| Many (많은 쪽) | One (하나인 쪽) |
| 게시글 (여러개) | 유저 (1명) |
| 학생 (여러명) | 반 (1개) |
| 주문 (여러개) | 고객 (1명) |
| 댓글 (여러개) | 게시글 (1개) |
🍬 @OneToMany - 일반적으로 잘 안 쓴다
@Entity
@Getter
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
@OneToMany(mappedBy = "user") // Post의 user 필드랑 연결
private List<Post> posts = new ArrayList<>();
}
단방향을 기본으로 쓴다. @ManyToOne만 쓰는 게 기본이고,
@OneToMany는정말 필요할 때만 추가한다. 양방향으로 하면 관리가 복잡해짐!
🍬 @JoinColumn
@ManyToOne
@JoinColumn(name = "user_id") // DB의 FK 컬럼 이름
private User user;
- 어떤 컬럼으로 JOIN할지 지정
- 생략하면 JPA가 자동으로 이름 만들어주는데, 명시적으로 써주는 게 좋다!
🕶️ 전체 흐름 예시
// 유저 저장
User user = new User("스파르타", "moon@test.com", "1234");
userRepository.save(user);
// 게시글 저장 (유저 연결)
Post post = new Post("제목", "내용", user);
postRepository.save(post);
// 게시글에서 유저 정보 가져오기
Post findPost = postRepository.findById(1L).orElseThrow();
String username = findPost.getUser().getUsername(); // "스파르타"
⭐️ Key Point ⭐️
| 어노테이션 | 의미 | 사용 위치 |
| @ManyToOne | 많은 → 적은 | Post에서 User를 참조 |
| @OneToMany | 적은 → 많은 | 필요할 때만 사용 |
| @JoinColumn | FK 컬럼 지정 | @ManyToOne 옆에 |
단방향 원칙 기억하기! @ManyToOne 하나만 쓰는 게 기본!
@OneToMany는 진짜 필요할 때만 추가! (거의 없음)
Part 4 : Cookie/Session 인증/인가
✔️ 개념 설명
로그인 기능을 만들 때 사용하는 방법
왜 Cookie/Session이 필요할까?
🍬 HTTP의 특징 - "무상태(Stateless)"
HTTP는 기본적으로 요청이 끝나면 상태를 기억 못한다!
클라이언트: "나 스파르타야, 로그인할게!"
서버: "오케이 로그인 됐어!"
-- 1초 후 --
클라이언트: "내 게시글 목록 줘!"
서버: "너 누구야...?" // 기억 못함!!
이 문제를 해결하는 게 Cookie/Session 이다.
🍬 Cookie vs Session 차이
| Cookie | Session | |
| 저장 위치 | 클라이언트 (브라우저) | 서버 |
| 보안 | 상대적으로 취약 | 상대적으로 안전 |
| 용량 | 작음 (4KB) | 큼 |
🍬 동작 흐름
1. 로그인 요청
클라이언트 → 서버: "아이디/비밀번호 보낼게!"
2. 서버가 Session 생성
서버: "맞네! Session 만들어서 저장해놓을게"
서버 메모리: { "ABC123": { userId: 1, username: "스파르타" } }
3. Cookie에 SessionId 전달
서버 → 클라이언트: "이 SessionId(ABC123) 가지고 있어!"
브라우저: 쿠키에 ABC123 저장
4. 다음 요청부터 자동으로 Cookie 전송
클라이언트 → 서버: "게시글 줘!" + Cookie(ABC123)
서버: "ABC123으로 조회해보니 스파르타네! 게시글 줘야겠다"
🍬 실제 코드
🕶️ 로그인 (Session 저장)
@PostMapping("/login")
public ResponseEntity<String> login(
@RequestBody LoginRequestDto requestDto,
HttpServletRequest request) {
// 1. 유저 확인
User user = userService.login(requestDto);
// 2. Session 생성 & 저장
HttpSession session = request.getSession();
session.setAttribute("loginUser", user.getId());
return ResponseEntity.ok("로그인 성공!");
}
🕶️ 로그아웃 (Session 삭제)
@PostMapping("/logout")
public ResponseEntity<String> logout(HttpServletRequest request) {
// Session 무효화 (삭제)
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return ResponseEntity.ok("로그아웃 성공!");
}
🕶️ 인가 (로그인한 사람만 접근 가능)
@GetMapping("/posts")
public ResponseEntity<?> getPosts(HttpServletRequest request) {
// Session에서 로그인 정보 확인
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("loginUser") == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("로그인이 필요합니다!");
}
// 로그인 된 경우만 실행
Long userId = (Long) session.getAttribute("loginUser");
return ResponseEntity.ok(postService.getPosts(userId));
}
⭐️ Key Point ⭐️
로그인 → Session 생성 → Cookie에 SessionId 저장
요청마다 → Cookie 자동 전송 → 서버가 Session 확인
로그아웃 → Session 삭제
인증(Authentication) = "너 누구야?" (로그인 확인)
인가(Authorization) = "너 이거 해도 돼?" (권한 확인)
Part 5 : Bean Validation (@Valid 등)
✔️ 개념 설명
클라이언트가 보내는 데이터를 검증하는 방법
예를 들어 회원가입할 때 :
- 이메일 형식이 맞는지
- 비밀번호가 최소 8자 이상인지
- 이름이 비어있지 않은지
이런 걸 일일이 if문으로 체크하면 너무 번거롭기 때문에
어노테이션 하나로 검증할 수 있게 해주는 게 Bean Validation이다!
🍬 검증 없이 짜면
// 😱 검증 없이 짜면 이렇게 됨
@PostMapping("/signup")
public ResponseEntity<String> signup(@RequestBody SignupRequestDto requestDto) {
if (requestDto.getUsername() == null || requestDto.getUsername().isEmpty()) {
return ResponseEntity.badRequest().body("이름을 입력해주세요!");
}
if (requestDto.getEmail() == null || !requestDto.getEmail().contains("@")) {
return ResponseEntity.badRequest().body("이메일 형식이 맞지 않습니다!");
}
if (requestDto.getPassword() == null || requestDto.getPassword().length() < 8) {
return ResponseEntity.badRequest().body("비밀번호는 8자 이상이어야 합니다!");
}
// ... 끝도 없이 계속됨
}
🍬 @Valid 사용하면?
// @Valid 사용하면 이렇게 깔끔해짐!
@PostMapping("/signup")
public ResponseEntity<String> signup(@Valid @RequestBody SignupRequestDto requestDto) {
// 검증은 DTO에서 알아서 다 해줌!
userService.signup(requestDto);
return ResponseEntity.ok("회원가입 성공!");
}
🍬 자주 쓰는 검증 어노테이션
@Getter
public class SignupRequestDto {
@NotBlank(message = "이름은 필수입니다!")
// null, "", " " 모두 거부
private String username;
@Email(message = "이메일 형식이 올바르지 않습니다!")
// hong@test.com 같은 형식인지 체크
private String email;
@Size(min = 8, max = 20, message = "비밀번호는 8~20자여야 합니다!")
// 글자 수 체크
private String password;
@Min(value = 1, message = "나이는 1살 이상이어야 합니다!")
@Max(value = 150, message = "나이는 150살 이하여야 합니다!")
// 숫자 범위 체크
private int age;
@NotNull(message = "약관 동의는 필수입니다!")
// null만 거부 (빈 문자열은 허용)
private Boolean agreeTerms;
}
🕶️ @NotNull vs @NotEmpty vs @NotBlank 차이
String a = null; // null
String b = ""; // 빈 문자열
String c = " "; // 공백만 있는 문자열
String d = "홍길동"; // 정상
@NotNull → null만 거부 → b, c, d 통과 / a 거부
@NotEmpty → null, "" 거부 → c, d 통과 / a, b 거부
@NotBlank → null, "", " " 모두 거부 → d만 통과
문자열 검증할 땐 @NotBlank를 제일 많이 쓴다! 공백만 입력하는 경우까지 막아줌
🍬 검증 실패할 때 처리 방법
@Valid 검증이 실패하면 MethodArgumentNotValidException이 발생한다.
이걸 @ExceptionHandler로 잡아서 처리할 수 있다!
@RestControllerAdvice // 전역 예외 처리 클래스
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleValidationException(
MethodArgumentNotValidException e) {
// 첫 번째 에러 메시지만 꺼내기
String errorMessage = e.getBindingResult()
.getAllErrors()
.get(0)
.getDefaultMessage();
return ResponseEntity.badRequest().body(errorMessage);
// 예: "이름은 필수입니다!" 반환
}
}
✔️ 전체 흐름 정리
클라이언트: POST /signup { username: "", email: "잘못된형식", password: "123" }
↓
Controller: @Valid 검증 시작!
↓
검증 실패! MethodArgumentNotValidException 발생
↓
GlobalExceptionHandler가 잡아서 처리
↓
클라이언트: 400 Bad Request "이름은 필수입니다!"
⭐️ 자주 쓰는 어노테이션 정리
| @NotBlank | 문자열 필수 입력 (공백 불가) |
| @NotNull | null 불가 |
| 이메일 형식 체크 | |
| @Size(min, max) | 문자열 길이 체크 |
| @Min(value) | 최솟값 체크 |
| @Max(value) | 최댓값 체크 |
| @Pattern(regexp) | 정규식으로 형식 체크 |
🔥 다음 글 : [Spring 숙련] 일정 관리 앱 만들기 - 일정 CRUD 구현