본문 바로가기

백엔드 면접준비/Java

4. Java Annotation

1. Java Annotation이란?

 

자바에서 어노테이션(Annotation)은 코드에 메타데이터를 추가하는 방법입니다. 어노테이션은 코드 자체를 변경하지 않지만, 코드에 대한 추가적인 정보를 제공하거나 특정 처리를 할 수 있게 도와줍니다. 주로 컴파일러나 런타임에서 이 정보를 활용하여 특정 작업을 자동으로 처리하거나, 개발자가 의도한 동작을 명확히 할 수 있도록 합니다.

 

어노테이션의 예를 들면:

  1. @Override
    • 메서드가 부모 클래스의 메서드를 오버라이드하고 있다는 것을 컴파일러에 알려주는 역할을 합니다. 
    • 오타나 실수로 메서드 시그니처가 일치하지 않는 경우 컴파일러가 경고를 줍니다.
  2. @Entity
    • JPA에서 데이터베이스 테이블과 매핑되는 클래스를 표시할 때 사용합니다.
    • 해당 클래스가 엔티티로서 데이터베이스의 테이블과 매핑된다는 의미를 컴파일러와 런타임 시스템에 전달할 수 있습니다.
  3. @Deprecated
    • 더 이상 사용되지 않는 메서드나 클래스를 표시할 때 사용합니다. 이를 통해 다른 개발자들에게 해당 코드가 더 이상 사용되지 않거나 향후 삭제될 가능성이 있음을 경고합니다.

어노테이션은 크게 빌드 도구나 프레임워크에서 처리되는 경우가 많으며, 예를 들어 Spring Framework에서는 어노테이션을 통해 의존성 주입이나 트랜잭션 관리 등을 자동화할 수 있습니다.

어노테이션 자체는 실제 코드 실행에는 영향을 주지 않지만, 프로그램의 메타데이터로서 중요한 역할을 합니다.

 


2. 커스텀 어노테이션

- 목적 : 생성/수정 api별로 필드값의 null check를 하는 커스텀 어노테이션을 만들고 싶습니다.

 

1) CheckedValidator.class

  • 필드값의 Null 체크 및 응답 문구를 세팅하는 클레스
import com.oss.springBootTest.dto.annotation.Checked;
import io.micrometer.common.util.StringUtils;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.util.ObjectUtils;

public class CheckedValidator implements ConstraintValidator<Checked, Object> {

    private String displayName; // 한글 필드명

    @Override
    public void initialize(Checked constraintAnnotation) {
        this.displayName = constraintAnnotation.name(); // 한글 필드명 저장
    }

    @Override
    public boolean isValid(Object object, ConstraintValidatorContext context) {
        if(!ObjectUtils.isEmpty(object)){
            return true;
        }

        // 필드명을 자동으로 가져오기 위해 컨텍스트에서 정보 조회
        // 필드명 저장
        String fieldName;

        try {
            fieldName = context.getDefaultConstraintMessageTemplate();
        } catch (Exception e) {
            fieldName = "값"; // 필드명을 가져오지 못할 경우 기본값 설정
        }

        String errorMsg = "";
        if(StringUtils.isNotBlank(displayName) && StringUtils.isNotBlank(fieldName)){
            errorMsg = String.format("%s(%s)", displayName, fieldName);
        }

        // 기본 에러 메시지를 동적으로 변경
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(
                String.format("%s필드는 null이 될 수 없습니다.", errorMsg))
                .addConstraintViolation();

        return false; // 검증 실패
    }
}

 

 

2) Checked.class(Interface)

  • 커스텀 어노테이션 
import com.oss.springBootTest.aop.CheckedValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Constraint(validatedBy = CheckedValidator.class) // 유효성 검사 로직과 연결
@Target({ ElementType.FIELD, ElementType.PARAMETER }) // 필드, 파라미터에 적용 가능
@Retention(RetentionPolicy.RUNTIME) // 런타임까지 유지
public @interface Checked {
    String message() default "";// 노출메세지
    String name() default ""; // 필드명(한글명)
    Class<?>[] groups() default {}; // 그룹 지정 (기본값)
    Class<? extends Payload>[] payload() default {}; // 추가 정보 제공 (기본값)
}

 

 

3) ShppDircBoxDto.class

  • api 테스트를 위한 Dto
import com.oss.springBootTest.dto.annotation.Checked;
import com.oss.springBootTest.dto.annotation.ValidationGroups;
import lombok.*;

@ToString
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShppDircBoxDto {
    // Getter 메서드들
    @Checked(message = "shppNo", name = "배송번호", groups = {ValidationGroups.CreateGroup.class, ValidationGroups.UpdateGroup.class})  // shppNo 필드에 어노테이션 적용
    private String shppNo;

    @Checked(groups = ValidationGroups.CreateGroup.class)  // shppBoxSeq 필드에 어노테이션 적용
    private String shppBoxSeq;

    @Checked(message = "wblNo", name = "운송장번호", groups = ValidationGroups.UpdateGroup.class)
    private String wblNo;

}

 

 

4) GlobalExceptionHandler.class

  • api 테스트 시 에러 문구를 응답해주기 위한 ControllerAdvice
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, Object> errors = new HashMap<>();
        Map<String, String> fieldErrors = new HashMap<>();

        for (FieldError error : ex.getBindingResult().getFieldErrors()) {
            fieldErrors.put(error.getField(), error.getDefaultMessage());
        }
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

        errors.put("timestamp", LocalDateTime.now().format(formatter));
        errors.put("status", HttpStatus.BAD_REQUEST.value());
        errors.put("error", "Bad Request");
        errors.put("fieldErrors", fieldErrors);

        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
}

 

 

5) ShppDircBoxController.class

  • 생성, 수정 api 테스트 Controller
import com.oss.springBootTest.dto.ShppDircBoxDto;
import com.oss.springBootTest.dto.annotation.ValidationGroups;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
public class ShppDircBoxController {

    @PostMapping("/check")
    public ResponseEntity<String> checkFields(@RequestBody @Validated(ValidationGroups.CreateGroup.class) ShppDircBoxDto shppDircBox) {

        System.out.println("shppDircBox = " + shppDircBox);
        return ResponseEntity.ok("Validation Passed!");
    }

    @PostMapping("/update")
    public ResponseEntity<String> updateFields(@RequestBody @Validated(ValidationGroups.UpdateGroup.class) ShppDircBoxDto shppDircBox) {

        System.out.println("shppDircBox = " + shppDircBox);
        return ResponseEntity.ok("Validation Passed!");
    }
}

 

- 테스트 결과

 

1) CreateGroup

정상 API

 

필드값 누락