카테고리 없음

[TIL] Spring 숙련 - JPA & Cookie/Session 정리

mooncommit 2026. 4. 16. 16:28

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     email     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 불가
@Email 이메일 형식 체크
@Size(min, max) 문자열 길이 체크
@Min(value) 최솟값 체크
@Max(value) 최댓값 체크
@Pattern(regexp) 정규식으로 형식 체크

🔥 다음 글 : [Spring 숙련] 일정 관리 앱 만들기 - 일정 CRUD 구현