Day29

강태훈·2026년 2월 6일

nbcamp TIL

목록 보기
29/58

숙련 Spring


1. 객체지향 프로그래밍

  1. 상속: extends
  2. 응집도:
    • 하나의 클래스(또는 모듈)이 얼마나 하나의 목적을 위해 긴밀하게 뭉쳐있는지 나타내는 척도
    • 클래스 내의 메소드와 데이터들이 얼마나 서로 관련 있는 작업을 수행하는지를 의미
  3. 결합도:
    • 하나의 클래스(또는 모듈)가 다른 클래스와 얼마나 많이 엮여있는지, 얼마나 의존적인지 나타내는 척도
  4. SOLID 원칙
    • Single Responsibility Principle(단일 책임 원칙): "클래스는 단 하나의 책임만 가져야 한다”
    • Open/Closed Principle(개방/폐쇄 원칙): "클래스는 확장에는 열려있고, 수정에는 닫혀있어야 한다”
    • Liskov Substitution Principle(리스코프 치환 원칙): "자식 클래스는 부모 클래스를 대체할 수 있어야 한다”
    • Interface Segregation Principle(인터페이스 분리 원칙): "클라이언트는 자신이 사용하지 않는 메서드에 의존하면 안 된다”
    • Dependency Inversion Principle(의존 역전 원칙): "고수준 모듈은 저수준 모듈에 의존하면 안 된다. 둘 다 추상화에 의존해야 한다”

2. IoC/DI

  1. 의존성: 한 클래스가 다른 클래스(혹은 객체)를 사용하는 관계
  2. IoC(Inversion of Control) - 제어의 역전: 말 그대로 제어의 흐름이 바뀌는 것
    • 스프링이 개발자를 대신해서 객체를 생성하여 관리하는 것
  3. DI(Dependency Injection) - 의존성 주입: IoC 개념을 실제로 구현하느 대표적인 방법

3. Bean(1)

  1. Bean: 스프링이 개발자 대신 생성한 객체
    • Spring 컨테이너에 의해 생성, 관리, 소멸됨
    • 애플리케이션 전역에서 재사용 가능
    • 기본적으로 싱글톤 스코프로 관리

      싱글톤(Singleton): 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴
      애플리케이션 전체에서 해당 클래스의 객체를 하나만 만들고, 그것을 공유해서 사용

  2. IoC Container: 그 Bean을 관리하는 상자
    • Bean의 생성 및 생명주기 관리
    • 의존성 주입 (DI)
    • Bean 설정 정보 관리
    • Bean 간의 의존 관계 설정
  3. 어노테이션 합성: 여러 개의 어노테이션을 조합하여 하나의 새로운 어노테이션을 만드는 것
    • 스프링은 어노테이션을 분석할 때, 해당 어노테이션 위에 붙어있는 다른 어노테이션(메타 어노테이션)까지 재귀적으로 탐색하여 그 기능을 모두 적용해줌
    • 예: @RestController = Controller + ResponseBody
  4. 스프링 Bean 등록
    1. Bean 자동 등록
    • Spring@Component 클래스를 찾아서 Bean으로 등록하는 방식
    • @ComponentScan에 설정되어있는 패키지를 기준으로 하위의 모든 @Component 클래스를 탐색하여 Bean으로 등록
      • @Component: 가장 기본적인 컴포넌트 어노테이션, 개발자가 직접 작성한 클래스를 빈으로 등록할 때 사용
      • @Controller, @RestController, @Service, @Repository
    • @ComponentScan: 이 컨테이너에게 어디에서 빈으로 사용할 클래스들을 찾아야 할지 알려주는 역할
      • 지정된 경로와 그 하위 패키지를 모두 탐색
      • @SpringBootApplication 어노테이션에 이미 포함되어있기 때문에 사용하지않음
    1. Bean 수동 등록
    • Spring@Configuration 클래스와 @Bean 메소드를 사용하여 명시적으로 Bean을 등록하는 방식
    • @Configuration 클래스에 정의된 @Bean 메소드의 반환 객체가 Spring IoC 컨테이너Bean으로 등록됨
    • @Configuration이 붙은 클래스는 설정 클래스로 인식
    • @Configuration 클래스 내부의 @Bean 메소드들이 Bean 정의를 포함
    • @Bean은 메소드 레벨에서 사용되며, 해당 메소드가 반환하는 객체를 Spring 컨테이너Bean으로 등록
    • Bean의 이름은 기본적으로 메소드명이 되며, @Bean(name="customName")으로 커스텀 가능
  5. DI(Dependency Injection) 방식 비교
    • Spring에서 의존성 주입은 '생성자 주입', '세터 주입', '필드 주입' 방식으로 구현가능
    1. 필드 주입 (Field Injection): @Autowired를 필드에 직접 선언하여 의존성을 주입받는 방식. 사용X
    2. 세터 주입 (Setter Injection): Setter 메소드를 통해 의존성을 주입받는 방식. 사용X
    3. 생성자 주입 (Constructor Injection): 생성자를 통해 의존성을 주입받는 방식. 일반적으로는 @Autowired를 생략. 권장

4. Bean(2)

  1. **Bean 우선순위**
    1. @Primary: 여러 빈 중에서 우선적으로 선택될 기본(Default) Bean을 지정하는 어노테이션
    2. @Qualifier라는 이름으로 특정 빈을 직접 지정하여 주입하는 어노테이션, @Primary보다 우선순위가 높음
  2. Bean 스코프
    • 스프링 빈이 얼마나 오래, 그리고 어떻게 존재할지를 정의하는 개념
    • 기본(Default) 값은 싱글톤
    1. 싱글톤 스코프: 스프링 컨테이너가 시작될 때 단 한 번만 생성되고, 애플리케이션이 끝날 때까지 계속 재사용되는 방식. 대부분의 빈은 싱글톤으로 관리되며, 메모리 효율성이 매우 좋음
    2. 프로토타입 스코프: @Scope("prototype")으로 지정된 빈은, 요청이 올 때마다 계속 새로운 객체를 생성하여 반환. 스프링 컨테이너는 생성만 책임지고, 그 이후의 관리는 하지 않음
  3. 라이프 사이클 콜백
    • 생성 → 의존성 주입 → 초기화 → 사용 → 소멸이라는 스프링 빈이 갖는 생명주기(Lifecycle)
    • 이때, 초기화와 소멸 단계에서 특정 작업을 수행하도록 콜백(Callback) 메서드를 지정할수있음
    • 초기화 단계
      • @PostConstruct이 붙은 메서드는 빈의 생성과 모든 의존성 주입이 완료된 직후에 딱 한 번 호출
      • 주로 주입받은 의존성을 사용하여 외부 리소스를 가져오거나, 초기 설정 값을 세팅하는 등 무거운 초기화 작업에 사용
    • 소멸 단계
      • @PreDestroy는 스프링 컨테이너에서 빈이 제거되기 직전에 호출됨
      • 주로 사용하던 외부 리소스의 연결을 안전하게 종료하거나, 임시 파일 삭제 등 뒷정리(Clean-up) 작업에 사용

5. Validation

  1. Validation

    • 특정 데이터(주로 클라이언트의 요청 데이터)의 값이 유효한지 확인하는 단계
    • 역할
      1. 검증을 통해 적절한 메세지를 유저에게 보여주어야 합니다.
      2. 검증 오류로 인해 정상적인 동작을 하지 못하는 경우는 없어야 합니다.
      3. 사용자가 입력한 데이터는 유지된 상태여야 합니다.
    • 검증
      1. 프론트엔드 검증
        • 해당 검증은 유저가 조작할 수 있음으로 보안에 취약
        • 보안에 취약하지만 그럼에도 꼭 필요
      2. 서버 검증
        • 프론트 검증 없이 서버에서만 검증한다면 유저 사용성이 떨어짐
        • API 스펙을 정의해서 Validation 오류를 Response 예시에 남겨주어야 함
          • API 명세서를 잘 만들어야 그에 맞는 대응을 할 수 있음
        • 서버 검증은 선택이 아닌 필수
      3. 데이터베이스 검증
        • Not Null, Default와 같은 제약조건을 설정
        • 최종 방어선의 역할을 수행
  2. 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;
    }
  3. 정규식 (Regular Expression)

    • 정규식: 문자열의 패턴을 표현하는 특별한 문자 조합
    • 예시:
    public boolean isValidPhone(String phone) {
    	return phone.matches("^010-\\d{4}-\\d{4}$");
    }
    • @Pattern에 적용

      • 스프링에서는 기본적으로 @Size, @Email 등의 많이 쓰는 형태의 검증 어노테이션을 제공하지만, 정규식을 활용한다면 더욱 다양한 패턴들을 검증할 수 있다.
      • 예시
      // 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"

6. 예외처리(1)

  1. 스프링 예외 처리 기본 동작

  2. 스프링 예외 처리 전략

    • 스프링 예외 처리 전략 3가지
      • 동시에 존재할 경우 1, 2, 3 순서대로 우선순위를 가짐
      1. @ExceptionHandler - 컨트롤러별 예외 처리
        이 컨트롤러에서 발생하는 예외만 처리
      2. @RestControllerAdvice - 전역 예외 처리
        모든 컨트롤러의 예외를 한 곳에서 처리
        가장 많이 사용되는 방식, 사실상의 표준
      3. Spring 기본 예외 처리
  3. 로깅

    • 로그(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 "로그 테스트 완료!";
            }
        }

7. 예외 처리(2)

  1. 커스텀 에러

    • IllegalStateException 같은 자바 기본 예외는 너무 범용적
    • 그렇기때문에 상황에 딱 맞는 이름의 예외를 직접 만들어서 사용
    • 커스텀 에러 만들기
      • RuntimeException을 상속받아서 우리만의 예외 클래스를 만들어보겠습니다.
      • 반드시 RuntimeException을 상속해야함
        노션 참고
  2. 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개 이상을 응답 객체에 매핑하여 반환해도 되지만, 위 예시는 하나의 에러만 반환하는 예시이다.

0개의 댓글