extendsSingle Responsibility Principle(단일 책임 원칙): "클래스는 단 하나의 책임만 가져야 한다”Open/Closed Principle(개방/폐쇄 원칙): "클래스는 확장에는 열려있고, 수정에는 닫혀있어야 한다”Liskov Substitution Principle(리스코프 치환 원칙): "자식 클래스는 부모 클래스를 대체할 수 있어야 한다”Interface Segregation Principle(인터페이스 분리 원칙): "클라이언트는 자신이 사용하지 않는 메서드에 의존하면 안 된다”Dependency Inversion Principle(의존 역전 원칙): "고수준 모듈은 저수준 모듈에 의존하면 안 된다. 둘 다 추상화에 의존해야 한다”IoC/DIIoC(Inversion of Control) - 제어의 역전: 말 그대로 제어의 흐름이 바뀌는 것DI(Dependency Injection) - 의존성 주입: IoC 개념을 실제로 구현하느 대표적인 방법Bean(1)Bean: 스프링이 개발자 대신 생성한 객체Spring 컨테이너에 의해 생성, 관리, 소멸됨싱글톤 스코프로 관리싱글톤(Singleton): 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴
애플리케이션 전체에서 해당 클래스의 객체를 하나만 만들고, 그것을 공유해서 사용
IoC Container: 그 Bean을 관리하는 상자Bean의 생성 및 생명주기 관리DI)Bean 설정 정보 관리Bean 간의 의존 관계 설정@RestController = Controller + ResponseBodyBean 등록Bean 자동 등록Spring이 @Component 클래스를 찾아서 Bean으로 등록하는 방식@ComponentScan에 설정되어있는 패키지를 기준으로 하위의 모든 @Component 클래스를 탐색하여 Bean으로 등록@Component: 가장 기본적인 컴포넌트 어노테이션, 개발자가 직접 작성한 클래스를 빈으로 등록할 때 사용@Controller, @RestController, @Service, @Repository@ComponentScan: 이 컨테이너에게 어디에서 빈으로 사용할 클래스들을 찾아야 할지 알려주는 역할@SpringBootApplication 어노테이션에 이미 포함되어있기 때문에 사용하지않음Bean 수동 등록Spring의 @Configuration 클래스와 @Bean 메소드를 사용하여 명시적으로 Bean을 등록하는 방식@Configuration 클래스에 정의된 @Bean 메소드의 반환 객체가 Spring IoC 컨테이너에 Bean으로 등록됨@Configuration이 붙은 클래스는 설정 클래스로 인식@Configuration 클래스 내부의 @Bean 메소드들이 Bean 정의를 포함@Bean은 메소드 레벨에서 사용되며, 해당 메소드가 반환하는 객체를 Spring 컨테이너에 Bean으로 등록Bean의 이름은 기본적으로 메소드명이 되며, @Bean(name="customName")으로 커스텀 가능DI(Dependency Injection) 방식 비교@Autowired를 필드에 직접 선언하여 의존성을 주입받는 방식. 사용XSetter 메소드를 통해 의존성을 주입받는 방식. 사용X@Autowired를 생략. 권장Bean(2)**Bean 우선순위**@Primary: 여러 빈 중에서 우선적으로 선택될 기본(Default) Bean을 지정하는 어노테이션@Qualifier라는 이름으로 특정 빈을 직접 지정하여 주입하는 어노테이션, @Primary보다 우선순위가 높음@Scope("prototype")으로 지정된 빈은, 요청이 올 때마다 계속 새로운 객체를 생성하여 반환. 스프링 컨테이너는 생성만 책임지고, 그 이후의 관리는 하지 않음생성 → 의존성 주입 → 초기화 → 사용 → 소멸이라는 스프링 빈이 갖는 생명주기(Lifecycle)콜백(Callback) 메서드를 지정할수있음@PostConstruct이 붙은 메서드는 빈의 생성과 모든 의존성 주입이 완료된 직후에 딱 한 번 호출@PreDestroy는 스프링 컨테이너에서 빈이 제거되기 직전에 호출됨ValidationValidation
Validation 오류를 Response 예시에 남겨주어야 함Bean Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'
많이 쓰이는 Validation 어노테이션
@NotBlank: 공백이 아닌 문자가 1개 이상 (null, "", " " 모두 거부)@Email: @가 포함된 이메일 형식@Size: 문자열 길이나 컬렉션 크기 제한@Min/@Max: 숫자의 최소/최대값@Pattern: 정규표현식 패턴 (전화번호, 주민번호 형식 등)컨트롤러에서 검증 실행 예시
@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;
}
정규식 (Regular Expression)
public boolean isValidPhone(String phone) {
return phone.matches("^010-\\d{4}-\\d{4}$");
}
@Pattern에 적용
// 1️⃣ 한글 이름 (2~10자)
@Pattern(regexp = "^[가-힣]{2,10}$",
message = "이름은 한글 2~10자로 입력하세요")
private String koreanName;
// 2️⃣ 영문 이름 (첫글자 대문자)
@Pattern(regexp = "^[A-Z][a-z]{1,29}$",
message = "영문명은 첫글자 대문자로 시작하세요")
private String englishName; // 예: "James"
// 3️⃣ 우편번호 (5자리 숫자)
@Pattern(regexp = "^\\d{5}$",
message = "우편번호는 5자리 숫자입니다")
private String zipCode; // 예: "12345"
// 4️⃣ 비밀번호 (복잡한 규칙)
@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자 이상
// 5️⃣ 주민번호 앞자리 (생년월일)
@Pattern(regexp = "^\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])$",
message = "올바른 생년월일 형식이 아닙니다 (YYMMDD)")
private String birthDate; // 예: "990315"
// 6️⃣ 차량번호
@Pattern(regexp = "^\\d{2,3}[가-힣]\\d{4}$",
message = "차량번호 형식이 올바르지 않습니다")
private String carNumber; // 예: "12가3456"
스프링 예외 처리 기본 동작
스프링 예외 처리 전략
@ExceptionHandler - 컨트롤러별 예외 처리@RestControllerAdvice - 전역 예외 처리로깅
로그(Log): 프로그램이 예상대로 잘 동작하는지, 혹은 문제가 생겼을 때 어디서 왜 문제가 발생했는지 추적하기 위한 기록
로깅(Logging): 로그을 남기는 행위
모든 기록이 다 같은 중요도를 갖지는 않기 때문에 로그의 중요도를 레벨(Level)로 나누어 관리함
// 로그 레벨 낮음 -> 높음, 순서대로 중요도 높아짐
TRACE → DEBUG → INFO → WARN → ERROR
| 로그 레벨 | 설명 |
|---|---|
| TRACE | 가장 상세한 흐름 정보(거의 안씀) |
| DEBUG | 개발 단계의 디버깅을 위한 상세 정보 |
| INFO | 중요한 비즈니스 흐름, 운영 정보 |
| WARN | 당장 문제는 아니지만, 잠재적 위험 경고 |
| ERROR | 기능 수행이 불가능한 심각한 오류 |
application.properties에서 아래와 같이 로그 레벨을 설정 가능, 기본 로그 레벨은 INFO
// 소문자로도 설정 가능!
logging.level.root=WARN
로깅
스프링 프레임워크에서는 Lombok 라이브러리의 @Slf4j을 사용하면 아주 간편하게 로깅 가능
@Slf4j 를 사용하면 log.xxx의 형태로 편하게 로깅이 가능
아래는 예시
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@Slf4j // <-- 바로 이 어노테이션입니다!
@RestController
public class LogController {
@GetMapping("/log-test/{userId}")
public String logTest(@PathVariable String userId) {
String userName = "봉이 김선달";
// 각 로그 레벨별로 메시지를 출력합니다.
log.trace("TRACE 로그: 사용자 ID = {}, 사용자 이름 = {}", userId, userName);
log.debug("DEBUG 로그: 사용자 ID = {}, 사용자 이름 = {}", userId, userName);
log.info("INFO 로그: 사용자 ID = {} 님이 로그인했습니다.", userId);
log.warn("WARN 로그: 사용자 ID = {} 님의 비밀번호 만료가 임박했습니다.", userId);
try {
// 일부러 예외를 발생시킵니다.
int errorResult = 10 / 0;
} catch (Exception e) {
log.error("ERROR 로그: 사용자 ID = {} 님 처리 중 예외 발생!", userId, e);
}
return "로그 테스트 완료!";
}
}
커스텀 에러
IllegalStateException 같은 자바 기본 예외는 너무 범용적RuntimeException을 상속받아서 우리만의 예외 클래스를 만들어보겠습니다.RuntimeException을 상속해야함노션 참고Bean Validation 에러 핸들링
우리는 Bean Validation을 배웠지만, 그 에러를 클라이언트에게 보여줄 수 없음
Bean Validation은 내부적으로 MethodArgumentNotValidException 에러를 던짐
따라서 GlobalExceptionHandler 클래스에 해당 에러를 핸들링해준다면, 클라이언트에게 원하는 에러 메시지를 보여줄 수 있음
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
String errorMessage = ex.getBindingResult().getFieldErrors().stream()
.findFirst() // 첫 번째 에러를 Optional로 가져옴
.map(fieldError -> fieldError.getDefaultMessage()) // 있다면 메시지로 변환
.orElse("입력 값이 올바르지 않습니다."); // 없다면 기본 메시지 사용
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMessage);
}
Bean Validation은 에러를 2개 이상 가지고 있을 수 있다!
-> DTO의 필드는 2개 이상일 수 있으므로, 검증 에러도 2개 이상일 수 있다. 따라서 스스로(혹은 팀/회사)의 규칙에 따라 2개 이상을 응답 객체에 매핑하여 반환해도 되지만, 위 예시는 하나의 에러만 반환하는 예시이다.