TIL - 26.01.05

이준연·2026년 1월 5일

학습 키워드


  • Validation
  • 예외 처리
  • 인증 & 인가

Validation


Validation

특정 데이터(주로 클라이언트의 요청 데이터)의 값이 유효한지 확인하는 단계를 의미합니다.

Validation의 역할

  1. 검증을 통해 적절한 메시지를 유저에세 보여주어야 합니다.
  2. 검증 오류로 인해 정상적인 동작을 하지 못하는 경우는 없어야 합니다.
  3. 사용자가 입력한 데이터는 유지된 상태여야 합니다.

검증의 종류

  1. 프론트엔드 검증
    • 해당 검증은 유저가 조작할 수 있음므로 보안에 취약합니다.
    • 보안에 취약하지만 그럼에도 꼭 필요합니다.
      ex) 비밀번호에 특수문자가 포함되어야 한다면 즉각적인 alert 가능 → 유저 사용성 증가
  2. 서버 검증
    • 프론트 검증없이 서버에서만 검증한다면 유저 사용성이 떨어집니다.
    • API 스펙을 정의해서 Validation 오루를 Response 예시에 남겨주어야 합니다.
      • API 명세서를 잘 만들어야 그에 맞는 대응을 할 수 있습니다.
    • 서버 검증은 선택이 아닌 필수입니다.
  3. 데이터베이스 검증
    • Not Null, Default와 같은 제약조건을 설정합니다.
    • 최종 방어선의 역할을 수행합니다.

💡기본적으로 프론트엔드, 서버, 데이터베이스 모두 검증을 꼼꼼하게 하는 것이 바람직합니다. Validation으로 수많은 Error와 문제들을 방지할 수 있습니다.

Bean Validation

  • 웹 애플리케이션에서 가장 중요한 것 중 하나는 잘못된 데이터가 시스템에 들어오지 못하게 막는 것입니다.
  • Bean Validation은 검증 로직을 어노테이션으로 표현하는 기술로, 어노테이션 선언만으로 편하고 직관적으로 데이터를 검증할 수 있습니다.
public class SaveMemberRequestDto {
 
 @NotBlank // 1. "값이 꼭 있어야 해요!" (null, "", " " 모두 거부)
 private String name;
 
 @Email // 2. "이메일 형식이어야 해요!" (xxx@xxx.xxx)
 private String email;
 
 @Size(min = 8, max = 20) // 3. "8~20자 사이여야 해요!"
 private String password;
 
 @Min(19) // 4. "최소 19 이상이어야 해요!"
 private Integer age;
 
 @Pattern(regexp = "^010-\\d{4}-\\d{4}$") // 5. "이 패턴과 일치해야 해
요!"
 private String phone;
}

📌 많이 쓰는 Validation 어노테이션

  • @NotBlank: 공백이 아닌 문자가 1개 이상 (null,""," " 모두 거부)
  • @Email:@가 포함된 이메일 형식
  • @Size: 문자열 길이나 컬렉션 크기 제한
  • @Min/@Max: 숫자의 최소/최대값
  • @Pattern: 정규표현식 패턴 (전화번호, 주민번호 형식 등)

📌 에러 메시지 커스터마이징

  • message 를 사용하여 쉽게 커스텀 에러 메시지를 설정할 수 있습니다.
// 직접 작성
@NotBlank(message = "이름을 입력해주세요!")
private String name;
// 다른 옵션값 포함 가능
@Size(min = 8, message = "비밀번호는 {min}자 이상이어야 합니다")
private String password;

컨트롤러에서 검증 실행하기

  • @Valid만 붙이면 자동으로 검증합니다.
@RestController
public class MemberController {
 
	// @Valid만 붙이면 자동으로 검증 실행!
 	@PostMapping("/signup")
 	public String signup(@Valid @RequestBody SaveMemberRequestDto request) {
 		// ...
 		return "가입이 완료되었습니다.";
 	}
}
@Getter
public class SaveMemberRequestDto {
 	@NotBlank // 1. "값이 꼭 있어야 해요!" (null, "", " " 모두 거부)
 	private String name;
 	@Email(message = "올바른 이메일 형식이 아닙니다.") // 2. "이메일 형식이어야 해요!" (xxx@xxx.xxx)
 	private String email;
 	@Size(min = 8, max = 20) // 3. "8~20자 사이여야 해요!"
 	private String password;
 	@Min(19) // 4. "최소 19 이상이어야 해요!"
 	private Integer age;
 	@Pattern(regexp = "^010-\\d{4}-\\d{4}$") // 5. "이 패턴과 일치해야 해요!"
 	private String phone;
}

💡 이 경우 검증에 실패하면 자동으로 400 Bad Request 에러를 반환합니다.

정규식

  • 문자열의 패턴을 표현하는 특별한 문자 조합입니다.
  • Regex라고도 합니다.

@Pattern에 적용

  • 스프링은 기본적으로 @Size, @Email 등의 많이 쓰는 형태의 검증 어노테이션을 제공합니다.
  • 하지만 정규식을 활용한다면 더욱다양한 패턴들을 검증할 수 있습니다.
// 한글 이름 (2~10자)
@Pattern(regexp = "^[가-힣]{2,10}$", 
 message = "이름은 한글 2~10자로 입력하세요")
private String koreanName;
// 영문 이름 (첫글자 대문자)
@Pattern(regexp = "^[A-Z][a-z]{1,29}$",
 message = "영문명은 첫글자 대문자로 시작하세요")
private String englishName; // 예: "James"
// 우편번호 (5자리 숫자)
@Pattern(regexp = "^\\d{5}$",
 message = "우편번호는 5자리 숫자입니다")
private String zipCode; // 예: "12345"
// 󾠱 비밀번호 (복잡한 규칙)
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*?&]{8,}$",
 message = "비밀번호는 영문, 숫자를 포함한 8자 이상이어야 합니다")
private String password;
// (?=.*[A-Za-z]) : 영문자 최소 1개 포함
// (?=.*\\d) : 숫자 최소 1개 포함
// [A-Za-z\\d@$!%*?&]{8,} : 허용된 문자로 8자 이상
// 󾠲 주민번호 앞자리 (생년월일)
@Pattern(regexp = "^\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])$",
 message = "올바른 생년월일 형식이 아닙니다 (YYMMDD)")
private String birthDate; // 예: "990315"
// 󾠳 차량번호
@Pattern(regexp = "^\\d{2,3}[가-힣]\\d{4}$",
 message = "차량번호 형식이 올바르지 않습니다")
private String carNumber; // 예: "12가3456"

💡 정규식을 공부할 필요는 전혀 없습니다. 필요하다면 검색하여 찾아봅시다!

예외 처리


커스텀 에러

IllegalStateException 같은 자바의 기본 에러는 너무 범용적이기 때문에 에러의 발생 원인을 한눈에 파악하기 어렵습니다. 그렇기 때문에 상황에 맞는 이름의 예외를 직접 만들어서 사용합니다.

import lombok.Getter;
@Getter
public class ServiceException extends RuntimeException {
 	private final HttpStatus status;
 	public ServiceException(HttpStatus status, String message) {
 	super(message);
 	this.status = status;
 	}
}
// extends ServerException 중요!
public class MovieNotFoundException extends ServiceException {
 	public MovieNotFoundException(String message) {
 	super(HttpStatus.NOT_FOUND, message); // HttpStatus.NOT_FOUND지정
 	}
}
// Service 코드
@Transactional(readOnly = true)
public GetMovieResponse findOne(Long movieId) {
 	Movie movie = movieRepository.findById(movieId).orElseThrow(
 		() -> new MovieNotFoundException("없는 영화입니다.")
 	);
 	return new GetMovieResponse(
		movie.getId(),
 		movie.getTitle(),
 		movie.getEmail(),
		movie.getPassword(),
 		movie.getPhoneNumber()
 	);
}
@RestControllerAdvice
public class GlobalExceptionHandler {
 	// MovieNotFoundException 커스텀 에러 핸들링
 	@ExceptionHandler(ServiceException.class)
 	public ResponseEntity<String> handleServiceException(ServiceException ex) {
 		return ResponseEntity.status(ex.getStatus()).body(ex.getMessage());
 	}
}

인증 & 인가


인증과 인가

인증(Authentication)

인증은 사용자가 누구인지 확인하는 절차입니다. 시스템에 등록된 사용자인지를 증명하는 과정이라고 할 수 있습니다.

  • 웹사이트에 아이디와 비밀번호를 입력하여 로그인하는 것
  • 스마트폰 잠금을 지문이나 얼굴 인식으로 해제하는 것
  • 건물 출입구에서 신분증을 제시하여 신원을 확인받는 것

인가(Authorization)

인가는 인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지 확인하는 절차입니다.

  • 일반 사용자는 게시글을 일고 쓸 수 있지만, 관리자는 다른 사용자의 게시글을 삭제할 수 있는 것
  • 유료 구독자만 프리미엄 콘텐츠를 볼 수 있는 것
  • 회사 건물에 출입은 했지만, 특정 부서 사무실이나 서버실에는 들어갈 수 없는 것

세션과 JWT

세션(Session)

전통적으로 많이 사용되는 방식으로, 서버가 사용자의 로그인 상태를 기억하는 방식입니다.

  • 동작 흐름

    1. 사용자가 아이디/비밀번호로 로그인을 시도합니다.

    2. 서버는 로그인 정보가 유효하면, 사용자를 위한 고유한 세션 ID를 생성하고 이 정보를 서버 메모리나 데이터베이스에 저장합니다.

    3. 서버는 생성된 세션 ID를 클라이언트(브라우저)에게 보내고, 브라우저는보통 이 ID를 쿠키에 저장합니다.

    4. 이후 클라이언트는 서버에 요청을 보낼 때마다 쿠키에 담긴 세션 ID를 함께 보냅니다. 서버는 이 세션 ID를 받아 저장된 세션 정보와 비교하여 사용자를 식별하고 요청을 처리합니다.

  • 장점

    • 서버에서 모든 세션 정보를 관리하므로 보안에 유리하고, 특정 사용자를 강제로 로그아웃시키는 등 제어가 쉽습니다.
    • 쿠키에 세션 ID만 저장하므로 클라이언트에 민감한 정보가 남지 않습니다.
  • 단점

    • 사용자가 많아질수록 서버의 메모리나 DB 부하가 커집니다.
    • 확장성 문제

JWT(JsonWeb Token)

최근 웹/앱 환경에서 널리 사용되는 방식으로, 서버가 로그인 상태를 저장하지 않는(stateless) 인증 방식입니다. 서버가 상태를 저장하지 않으므로 JWT 자체에 모든 인증 정보가 담겨있습니다.

  • 동작 흐름
    1. 사용자가 아이디/비밀번호로 로그인을 시도합니다.
    2. 서버는 로그인 정보가 유효하면, 사용자의 정보와 권한, 만료 시간 등을 담은 암호화된 토큰(Access Token)을 생성하여 클라이언트에게 보냅니다. 서버는 이 토큰을 저장하지 않습니다.
    3. 클라이언트는 전달받은 토큰을 로컬 스토리지나 쿠키에 저장합니다.
    4. 이후 클라이언트는 서버에 요청을 보낼 때마다 HTTP 헤더에 토큰을 실어 보냅니다. 서버는 이 토큰의 서명을 검증하여 유효성을 확인하고 요청을 처리합니다.
  • 장점
    • 서버가 토큰을 저장하지 않으므로 서버의 부하가 줄고, 여러 서버로 확장하기 용이합니다. 서버가 토큰을 저장하지 않아도 유저를 식별할 수 있는 이유는, JWT 자체에 모든 인증 정보가 담겨있기 때문입니다.
    • 웹뿐만 아니라 모바일 앱 등 다양한 클라이언트 환경에서 사용하기 편리합니다.
  • 단점
    • 토큰이 탈취되면 만료될 때까지 악용될 수 있습니다.
    • 세션 ID보다 토큰의 크기가 더 큽니다.
profile
반갑습니다!

0개의 댓글