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> {
}
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 차이
| PUT | PATCH |
|---|
| 의미 | 전체 수정 | 일부 수정 |
| 동작 | 객체 전체를 덮어씌움 | 바꿀 필드만 수정 |
| 필드 누락 시 | null/0으로 덮어씌워짐 | 기존 값 유지 |
실무에서는 PATCH를 더 많이 써요! Spring에서는 @PatchMapping 사용
23일차: HTTP 응답 코드 + ResponseEntity
자주 쓰는 HTTP 응답 코드
| 코드 | 의미 | 예시 |
|---|
| 200 | 성공 | 조회 성공 |
| 201 | 생성 성공 | 회원가입 성공 |
| 400 | 잘못된 요청 | 필수값 누락 |
| 401 | 인증 실패 | 로그인 실패 |
| 403 | 권한 없음 | 토큰 없거나 만료 |
| 404 | 찾을 수 없음 | 없는 유저 조회 |
| 500 | 서버 에러 | 서버 내부 오류 |
ResponseEntity 사용법
ResponseEntity.ok(user)
ResponseEntity.status(HttpStatus.CREATED).body(user)
ResponseEntity.notFound().build()
ResponseEntity.status(401).body("로그인 실패!")
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 {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<String> handleRuntimeException(RuntimeException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
}
@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 | 빈 문자열 불가 |
@NotNull | null 불가 |
@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;
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(key)
.compact();
}
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);
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.java | JwtFilter를 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 Request | Validation 실패 | 요청 데이터 확인 |
| 404 Not Found | 없는 리소스 조회 | id 확인 |
| int → null 에러 | int 타입에 null 들어옴 | int → Integer로 변경 |