클로드와 공부해요 DAY 22-27

닥개·2026년 9월 24일

지피티야과외해줘

목록 보기
4/10

Spring Boot 22~27일차 정리본


22일차: JPA CRUD 완성

CRUD 전체 구조

HTTP 메서드URL역할
POST/user생성
GET/users전체 조회
GET/user/{id}단건 조회
PUT/user/{id}수정
DELETE/user/{id}삭제

User.java

package com.example.demo;

import jakarta.persistence.*;

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private int age;

    public User() {}

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    public int getAge() { return age; }
    public void setName(String name) { this.name = name; }
    public void setAge(int age) { this.age = age; }
}

UserRepository.java

package com.example.demo;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
    // save(), findAll(), findById(), deleteById() 자동 생성!
}

UserService.java

package com.example.demo;

import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 생성
    public User join(String name, int age) {
        User user = new User(name, age);
        return userRepository.save(user);
    }

    // 전체 조회
    public List<User> findAll() {
        return userRepository.findAll();
    }

    // 단건 조회
    public User findById(Long id) {
        return userRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("유저를 찾을 수 없습니다."));
    }

    // 수정
    public User update(Long id, String name, int age) {
        User user = findById(id);
        user.setName(name);
        user.setAge(age);
        return userRepository.save(user);
    }

    // 삭제
    public void delete(Long id) {
        userRepository.deleteById(id);
    }
}

UserController.java

package com.example.demo;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    // 생성
    @PostMapping("/user")
    public ResponseEntity<User> join(@RequestBody User user) {
        User saved = userService.join(user.getName(), user.getAge());
        return ResponseEntity.status(HttpStatus.CREATED).body(saved);
    }

    // 전체 조회
    @GetMapping("/users")
    public List<User> findAll() {
        return userService.findAll();
    }

    // 단건 조회
    @GetMapping("/user/{id}")
    public ResponseEntity<User> findById(@PathVariable Long id) {
        return ResponseEntity.ok(userService.findById(id));
    }

    // 수정
    @PutMapping("/user/{id}")
    public User update(@PathVariable Long id, @RequestBody User user) {
        return userService.update(id, user.getName(), user.getAge());
    }

    // 삭제
    @DeleteMapping("/user/{id}")
    public ResponseEntity<String> delete(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.ok(id + "번 유저가 삭제되었습니다!");
    }
}

PUT vs PATCH 차이

PUTPATCH
의미전체 수정일부 수정
동작객체 전체를 덮어씌움바꿀 필드만 수정
필드 누락 시null/0으로 덮어씌워짐기존 값 유지

실무에서는 PATCH를 더 많이 써요! Spring에서는 @PatchMapping 사용


23일차: HTTP 응답 코드 + ResponseEntity

자주 쓰는 HTTP 응답 코드

코드의미예시
200성공조회 성공
201생성 성공회원가입 성공
400잘못된 요청필수값 누락
401인증 실패로그인 실패
403권한 없음토큰 없거나 만료
404찾을 수 없음없는 유저 조회
500서버 에러서버 내부 오류

ResponseEntity 사용법

ResponseEntity.ok(user)                               // 200
ResponseEntity.status(HttpStatus.CREATED).body(user)  // 201
ResponseEntity.notFound().build()                     // 404
ResponseEntity.status(401).body("로그인 실패!")        // 401

24일차: 예외 처리 (@ExceptionHandler)

GlobalExceptionHandler.java

package com.example.demo;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.stream.Collectors;

@RestControllerAdvice  // 모든 컨트롤러의 예외를 여기서 처리!
public class GlobalExceptionHandler {

    // RuntimeException 처리 → 404
    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<String> handleRuntimeException(RuntimeException e) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
    }

    // Validation 에러 처리 → 400
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<String> handleValidException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(error -> error.getDefaultMessage())
                .collect(Collectors.joining(", "));
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(message);
    }
}

전후 비교

이전 (try-catch)이후 (@ExceptionHandler)
에러 처리 위치컨트롤러마다GlobalExceptionHandler 한 곳
응답 본문비어있음에러 메시지 포함
코드 중복많음없음

25일차: Spring Validation

build.gradle 추가

implementation 'org.springframework.boot:spring-boot-starter-validation'

User.java에 검증 조건 추가

import jakarta.validation.constraints.*;

@NotBlank(message = "이름은 필수입니다.")
private String name;

@Min(value = 0, message = "나이는 0 이상이어야 합니다.")
private int age;

UserController.java에 @Valid 추가

@PostMapping("/user")
public ResponseEntity<User> join(@Valid @RequestBody User user) {
    User saved = userService.join(user.getName(), user.getAge());
    return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}

자주 쓰는 검증 어노테이션

어노테이션의미
@NotBlank빈 문자열 불가
@NotNullnull 불가
@Min(value)최솟값 제한
@Max(value)최댓값 제한
@Email이메일 형식 확인

26일차: Spring Security

build.gradle 추가

implementation 'org.springframework.boot:spring-boot-starter-security'

application.properties 임시 계정 설정

spring.security.user.name=user
spring.security.user.password=1234

SecurityConfig.java

package com.example.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig {

    private final JwtFilter jwtFilter;

    public SecurityConfig(JwtFilter jwtFilter) {
        this.jwtFilter = jwtFilter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/login").permitAll()  // 로그인은 누구나
                .anyRequest().authenticated()           // 나머지는 인증 필요
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

27일차: JWT 토큰 발급 + 인증

전체 흐름

1. POST /login → 서버가 JWT 토큰 발급
2. 클라이언트가 토큰 저장
3. 이후 요청마다 헤더에 토큰 담아서 보냄
   Authorization: Bearer 토큰
4. JwtFilter가 토큰 검증
5. 유효하면 통과, 아니면 403!

build.gradle 추가

implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

JwtUtil.java

package com.example.demo;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;

@Component
public class JwtUtil {
    private final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    private final long EXPIRATION = 1000 * 60 * 60; // 1시간

    // 토큰 생성 → 신분증 발급
    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(key)
                .compact();
    }

    // 토큰에서 username 꺼내기
    public String extractUsername(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }

    // 토큰 유효성 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (JwtException e) {
            return false;
        }
    }
}

JwtFilter.java

package com.example.demo;

import jakarta.servlet.*;
import jakarta.servlet.http.*;
import org.springframework.security.authentication.*;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;

@Component
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    public JwtFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        String header = request.getHeader("Authorization");

        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);  // "Bearer " 제거

            if (jwtUtil.validateToken(token)) {
                String username = jwtUtil.extractUsername(token);
                UsernamePasswordAuthenticationToken auth =
                    new UsernamePasswordAuthenticationToken(username, null, List.of());
                auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }
        filterChain.doFilter(request, response);
    }
}

AuthController.java

package com.example.demo;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
public class AuthController {

    private final JwtUtil jwtUtil;

    public AuthController(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    static class LoginRequest {
        private String name;
        public String getName() { return name; }
    }

    @PostMapping("/login")
    public ResponseEntity<String> login(@RequestBody LoginRequest request) {
        if ("user".equals(request.getName())) {
            String token = jwtUtil.generateToken(request.getName());
            return ResponseEntity.ok(token);
        }
        return ResponseEntity.status(401).body("로그인 실패!");
    }
}

세 파일의 역할

파일역할비유
JwtUtil.java토큰 만들기/검증신분증 발급소
JwtFilter.java모든 요청에서 토큰 확인입구 경비원
SecurityConfig.javaJwtFilter를 Security에 연결경비원 규칙서

test.http 예시

### 로그인 → 토큰 발급
POST http://localhost:8080/login
Content-Type: application/json

{
  "name": "user"
}

### 토큰으로 API 호출
GET http://localhost:8080/users
Authorization: Bearer 여기에토큰붙여넣기

⚠️ 토큰 유효시간은 1시간! 만료되면 다시 로그인해서 새 토큰 발급받아야 해요.


자주 나오는 에러 (22~27일차)

에러원인해결
403 Forbidden토큰 없거나 만료됨다시 로그인해서 토큰 재발급
400 Bad RequestValidation 실패요청 데이터 확인
404 Not Found없는 리소스 조회id 확인
int → null 에러int 타입에 null 들어옴int → Integer로 변경

profile
발바닥부터 시작하는 코딩공부

0개의 댓글