스프링-JPA 중요 개념 정리

wisdom·2022년 8월 16일
0

백엔드 개발자라면?

목록 보기
15/42

객체 생성 제약

protected Account() {}
  • 객체의 직접 생성을 외부에서 못하게 설정
  • @builder 어노테이션이 설정돼 있는 생성자 메서드를 통해 객체를 생성한다.

빌더 패턴을 이용한 객체 생성 강요의 장점

  • 객체를 생성할 때 인자 값의 순서가 상관없음
  • 여러 생성자를 두지 않고 하나의 생성자를 통해 객체 생성이 가능함

DTO 클래스의 필요 이유

  • 데이터 안정성
    - 엔티티 클래스로 RequestBody 를 받게 된다면? 모든 속성값들을 컨트롤러를 통해 넘겨 받게 되고 의도하지 않은 데이터 변경이 발생할 수 있음
    - 엔티티 클래스로 Response 를 처리하면? 엔티티의 모든 정보가 노출됨
    - JsonIgnore 속성으로 막을 순 있지만 임시방편이고 바람직하지 않음
  • 명확해지는 요구사항
    - 만약 유저 정보를 변경할 때, 변경할 수 있는 값이 password, address 라면 DTO 클래스만 확인해서 어떤 값들이 변경되는지 명확해진다.
  • swagger API Document를 사용한다면 DTO 사용으로 request/response 값이 자동으로 명세된다.

Setter 사용 안하기

  • JPA 에서는 영속성이 있는 객체에서 Setter 메서드를 통해서 데이터베이스 DML이 가능하게 된다.
  • 무분별하게 setter를 사용한다면?
    - 특정 필드의 변경의도가 없음에도, 영속성이 있는 상태에서 setter 를 사용해서 얼마든지 변경이 가능하게 된다.
    - 안전하지 않은 구조
    - 데이터 변경에 발생했을 때 추적할 포인트가 많아져 유지보수가 어려워진다.

    setter 대신 DTO 클래스를 기준으로 데이터를 변경하는 것이 더 직관적이고 유지보수하기 쉽다.

도메인 클래스에 있는 update 메서드

@Getter  
@Entity  
public class Account {  

	private String address1;  
	private String address2;

	@Column(name = "zip", nullable = false)  
	private String zip;

	...
  
    @Builder  
    public Account(String email, String firstName, String lastName, String password, String address1, String address2, String zip) {  
        this.email = email;  
        this.firstName = firstName;  
        this.lastName = lastName;  
        this.password = password;  
        this.address1 = address1;  
        this.address2 = address2;  
        this.zip = zip;  
    }  
  
    public void updateMyAccount(AccountDto.MyAccountReq dto) {  
        this.address1 = dto.getAddress1();  
        this.address2 = dto.getAddress2();  
        this.zip = dto.getZip();  
    }  
}
  • 도메인 클래스 안에 있는 update 메서드가 흥미롭다. 객체 자신을 변경하는 것은 언제나 자기 자신이어야 한다는 OOP 관점에서 도메인 클래스에 있는 게 적절하다고 한다.

validate와 예외 처리

  • API을 개발하다 보면 프런트에서 넘어온 값에 대한 유효성 검사를 수없이 진행하게 된다. 반복적인 작업을 보다 효율적으로 처리하고, 정확한 예외 메시지를 프런트엔드에게 전달해주는 것이 목표다.

@Valid 를 통한 유효성 검사

  • DTO 유효성 검사 어노테이션 추가
    - DTO에 대한 반복적인 유효성 검사 어노테이션이 필요하다는 단점이 있다.
    - 유효성 검사 로직이 변경되면? 모든 곳에 변경이 따름
    - ex) @Email, @NotEmpty
  • 컨트롤러에 @Valid 어노테이션 추가하면 유효성 검사를 진행하고 실패하면 MethodArgumentNotValidException 가 발생한다.
    - MethodArgumentNotValidException 발생 시 공통적으로 사용자에게 적절한 Response 값을 리턴해주는 게 효율적 -> @ControllerAdmivce

@ControllerAdvice 를 이용한 Exception 핸들링

@ControllerAdvice
public class ErrorExceptionController {
	@ExceptionHandler(MethodArgumentNotValidException.class)
	@ResponseStatus(HttpStatus.BAD_REQUEST)
	protected ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
	    retrun errorResponse...
	}
}
  • @ControllerAdvice
    - 특정 Exception 을 핸들링하여 적절한 값을 Response 값으로 리턴해준다.
    - 위에 코드처럼 MethodArgumentNotValidException 핸들링을 따로 하지 않으면 스프링 자체의 에러 Response 값을 리턴해준다.

스프링 자체 에러 Response 값

  • 시스템 정보를 포함한 너무 많은 값이 포함되어 있음
  • 그리고 Response 값으로 프론트엔드에서 처리하기 때문에 공통의 Response 포맷을 유지하는 게 합리적이다.
{
  "timestamp": 1525182817519,
  "status": 400,
  "error": "Bad Request",
  "exception": "org.springframework.web.bind.MethodArgumentNotValidException",
  "errors": [
    {
      "codes": [
        "Email.signUpReq.email",
        "Email.email",
        "Email.java.lang.String",
        "Email"
      ],
      "arguments": [
        {
          "codes": [
            "signUpReq.email",
            "email"
          ],
          "arguments": null,
          "defaultMessage": "email",
          "code": "email"
        },
        [],
        {
          "arguments": null,
          "defaultMessage": ".*",
          "codes": [
            ".*"
          ]
        }
      ],
      "defaultMessage": "이메일 주소가 유효하지 않습니다.",
      "objectName": "signUpReq",
      "field": "email",
      "rejectedValue": "string",
      "bindingFailure": false,
      "code": "Email"
    }
  ],
  "message": "Validation failed for object='signUpReq'. Error count: 3",
  "path": "/accounts"
}

MethodArgumentNotValidException의 Response 처리

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
protected ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
	log.error(e.getMessage());
	final BindingResult bindingResult = e.getBindingResult();
	final List<FieldError> errors = bindingResult.getFieldErrors();

		return buildFieldErrors(
			ErrorCode.INPUT_VALUE_INVALID,
			errors.parallelStream()
				.map(error -> ErrorResponse.FieldError.builder()
					.reason(error.getDefaultMessage())
					.field(error.getField())
					.value((String) error.getRejectedValue())
					.build())
				.collect(Collectors.toList())
	);
}

ErrorResponse

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ErrorResponse {
    private String message;
    private String code;
    private int status;
    private List<FieldError> errors;
		...

    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public static class FieldError {
        private String field;
        private String value;
        private String reason;
				...
    }
}

ErrorResponse : 공통적인 예외 Response

{
  "message": "입력값이 올바르지 않습니다.",
  "code": "???",
  "status": 400,
  "errors": [
    {
      "field": "email",
      "value": "string",
      "reason": "이메일 주소가 유효하지 않습니다."
    },
    {
      "field": "lastName",
      "value": null,
      "reason": "반드시 값이 존재하고 길이 혹은 크기가 0보다 커야 합니다."
    },
    {
      "field": "fistName",
      "value": null,
      "reason": "반드시 값이 존재하고 길이 혹은 크기가 0보다 커야 합니다."
    }
  ]
}
  • 예외가 발생한 경우 throw 를 통해 Exception 을 잘 처리해주면 비즈니스 로직과 예외 처리를 하는 로직이 분리되어 코드 가독성 및 유지 보수에 좋다.

Error Code

@Getter
public enum ErrorCode {

    ACCOUNT_NOT_FOUND("AC_001", "해당 회원을 찾을 수 없습니다.", 404),
    EMAIL_DUPLICATION("AC_002", "이메일이 중복되었습니다.", 400),
    INPUT_VALUE_INVALID("CM_001", "입력값이 올바르지 않습니다.", 400);

    private final String code;
    private final String message;
    private final int status;

    ErrorCode(String code, String message, int status) {
        this.code = code;
        this.message = message;
        this.status = status;
    }
}

연관 관계의 주인 설정

  • 주인 설정이라고 하면 뭔가 더 중요한 것이 주인이 되어야 할 거 같다는 생각이 들지만 연관 관계의 주인이라는 것은 외래 키의 위치와 관련해서 정해야 하지 해당 도메인의 중요성과는 상관관계가 없다.
profile
문제를 정의하고, 문제를 해결하는

0개의 댓글