728x90

개요

이번에 스크랩 프로젝트 개발을 진행하다가 DTO를 검증해야 하는 경우가 있었습니다. 이를 if문을 이용해서 validation을 진행하였습니다. 이를 별도의 검증 클래스로 만들어 사용할 수 있지만 간단한 검증의 경우에는 JSR 표준을 이용해 간결하게 처리할 수 있습니다.

// 요청한 값에 대한 validation 처리 필요
if(postJoinReq.getUsername() == null || postJoinReq.getUsername().isEmpty()){
    return new BaseResponse(JOIN_USERNAME_EMPTY);
}
if(postJoinReq.getPassword() == null || postJoinReq.getPassword().isEmpty()){
    return new BaseResponse(JOIN_PASSWORD_EMPTY);
}
if(postJoinReq.getName() == null || postJoinReq.getName().isEmpty()){
    return new BaseResponse(JOIN_NAME_EMPTY);
}
// 형식 확인
if(!RegexService.checkUsername(postJoinReq.getUsername())){
    return new BaseResponse(JOIN_USERNAME_INVALID);
}
if(!RegexService.checkPw(postJoinReq.getPassword())){
    return new BaseResponse(JOIN_PASSWORD_INVALID);
}
if(!RegexService.checkName(postJoinReq.getName())){
    return new BaseResponse(JOIN_NAME_INVALID);
}

implementation 추가

Spring에서는 일종의 어댑터인 LocalValidatorFactoryBean가 제약 조건 검증을 처리한다. 이를 이용하려면 LocalValidatorFactoryBean을 빈으로 등록해야 하는데, SpringBoot에서는 아래의 의존성만 추가해주면 해당 기능들이 자동 설정된다.

// validation 검사
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation', version: '2.7.1'

@Valid과 @Validated  차이

Spring에서는 메서드 레벨 유효성 검증을 위해 JSR-303의 @Valid 어노테이션을 사용합니다. 또한 유효성 검사를 위해 멤버 속성을 표시하는 데에도 이를 사용합니다. 그러나 @Valid 어노테이션은 그룹 유효성 검사를 지원하지 않습니다.

그룹은 유효성 검사 중에 적용되는 제약 조건을 제한하는 데 도움이 됩니다. 한 가지 특정 사용 사례는 UI 마법사입니다. 여기에서 첫 번째 단계에서 필드의 특정 하위 그룹이 있을 수 있습니다. 다음 단계에서 동일한 Bean에 속하는 다른 그룹이 있을 수 있습니다. 따라서 각 단계에서 이러한 제한된 필드에 제약 조건을 적용해야 하지만 @Valid는 이를 지원하지 않습니다.

이 경우 그룹 수준 의 경우 이 JSR-303의 @Valid 의 변형인 Spring의 @Validated 를 사용해야 합니다. 이것은 메서드 수준에서 사용됩니다. 그리고 멤버 속성을 표시하기 위해 @Valid 어노테이션을 계속 사용합니다.

또한 @Valid 어노테이션은 Controller 단에서만 유효성 검사가 가능하다. 일반적으로 파라미터에 대한 유효성 검증은 Controller 단에서 최대한 처리하는 것이 좋지만, 경우에 따라 다른 곳에서도 파라미터에 대한 검증이 필요합니다. 이런 경우에 AOP 기반으로 메서드의 요청을 가로채서 유효성 검증을 진행할 수 있는 것이 바로 @Validated 어노테이션이다.

@Valid 작성

@PostMapping("/join")
public BaseResponse join(@Validated(ValidationSequence.class) @RequestBody PostJoinReq postJoinReq){
		userService.join(postJoinReq);
		return new BaseResponse("회원가입에 성공했습니다");
}

@RequestBody 어노테이션 옆에 @Valid를 작성하면, RequestBody로 들어오는 객체에 대한 검증을 수행한다. 이 검증의 세부적인 사항은 객체 안에 정의를 해두어야 한다.

Request class 생성

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class PostJoinReq {

    @NotEmpty(message = "이메일을 입력해주세요", groups = NotEmptyGroup.class)
    @Email(message = "이메일 형식이 일치하지 않습니다", groups = EmailGroup.class)
    private String email; // 아이디

    @NotEmpty(message = "비밀번호를 입력해주세요", groups = NotEmptyGroup.class)
    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\\\d)[A-Za-z\\\\d!?@#$%&*]{5,15}$", message = "비밀번호 형식이 일치하지 않습니다", groups = PatternGroup.class)
    private String password; // 비밀번호

    @NotEmpty(message = "이름을 입력해주세요", groups = NotEmptyGroup.class)
    @Pattern(regexp = "^(?=. *[a-zA-Z가-힣])[A-Za-z가-힣]{1,30}$", message = "이름 형식이 일치하지 않습니다", groups = PatternGroup.class)
    private String name; // 이름
}

위와 같이 PostJoinReq 객체를 정의한 후 각 필드에 맞는 어노테이션을 사용하면 된다.

ValidationGroup class 생성

public class ValidationGroup {
    // 인터페이스 선언. 이 인터페이스를 @NotEmpty같은 어노테이션에 적용시켜주면 됨.
    public interface NotEmptyGroup{}
    public interface NotNullGroup{}
    public interface PatternGroup{}
    public interface EmailGroup{}
}

ValidationSequence interface 생성

// 왼 -> 오 순서로 적용됨
@GroupSequence({Default.class, NotNullGroup.class, NotEmptyGroup.class, PatternGroup.class, EmailGroup.class})
public interface ValidationSequence {
}

@GroupSequence 어노테이션에 적용되어있는 왼쪽에서 오른쪽 순서로 적용된다.

 

validation 어노테이션 종류

@AssertTrue Boolean, boolean 값이 항상 True 여야 한다.  
@DecimalMax 실수 제외 숫자 클래스. 지정된 최대 값보다 작거나 같아야 하는 숫자이다. String : value (max 값을 지정한다.)
@DecimalMin 실수 제외 숫자 클래스. 지정된 최소 값보다 크거나 같아야하는 숫자이다. String : value (min 값을 지정한다.)
@Digits BigDecimalBigIntegerCharSequencebyte, short, int, long, 이에 대응하는 Wrapper 클래스 허용된 범위 내의 숫자이다. int : integer (이 숫자에 허용되는 최대 정수 자릿수)int : fraction (이 숫자에 허용되는 최대 소수 자릿수)
@Email null도 valid로 간주된다. 올바른 형식의 이메일 주소여야한다.  
@Future 시간 클래스 Now 보다 미래의 날짜, 시간  
@FutureOrPresent 시간 클래스 Now의 시간이거나 미래의 날짜, 시간  
@Max 실수 제외 숫자 클래스. 지정된 최대 값보다 작거나 같은 숫자이다. long : value (max 값을 지정한다)
@Min 실수 제외 숫자 클래스. 지정된 최소 값보다 크거나 같은 숫자이다. long : value (min 값을 지정한다)
@Negative 숫자 클래스 음수인 값이다.  
@NegativeOrZero 숫자 클래스 0이거나 음수인 값이다  
@NotBlank   null 이 아닌 값이다.공백이 아닌 문자를 하나 이상 포함한다  
@NotEmpty CharSequence,Collection, Map, Array null이거나 empty(빈 문자열)가 아니어야 한다.  
@NotNull 어떤 타입이든 수용한다. null 이 아닌 값이다.  
@Null 어떤 타입이든 수용한다. null 값이다.  
@Past 시간 클래스 Now보다 과거의 날짜, 시간  
@PastOrPresent 시간클래스 Now의 시간이거나 과거의 날짜, 시간  
@Pattern 문자열 지정한 정규식과 대응되는 문자열이어야한다. Java의 Pattern 패키지의 컨벤션을 따른다 String : regexp (정규식 문자열을 지정한다)
@Positive 숫자 클래스 양수인 값이다  
@PositiveOrZero 숫자 클래스 0이거나 양수인 값이다.  
@Size CharSequence,Collection, Map, Array 이 크기가 지정된 경계(포함) 사이에 있어야한다. int : max (element의 크기가 작거나 같다)int : min (element의 크기가 크거나 같다)

@NotNull, @NotEmpty, @NotBlank 차이

null "" " "
@NotNull Invalid Valid
@NotEmpty Invalid Invalid
@NotBlank Invalid Invalid

참조

https://mangkyu.tistory.com/174

 

[Spring] @Valid와 @Validated를 이용한 유효성 검증의 동작 원리 및 사용법 예시 - (1/2)

Spring으로 개발을 하다 보면 DTO 또는 객체를 검증해야 하는 경우가 있습니다. 이를 별도의 검증 클래스로 만들어 사용할 수 있지만 간단한 검증의 경우에는 JSR 표준을 이용해 간결하게 처리할 수

mangkyu.tistory.com

https://jyami.tistory.com/55

 

@Valid 를 이용해 @RequestBody 객체 검증하기

Springboot를 이용해서 어노테이션을 이용한 validation을 하는 방법을 적으려 한다. RestController를 이용하여 @RequestBody 객체를 사용자로부터 가져올 때, 들어오는 값들을 검증할 수 있는 방법을 소개한

jyami.tistory.com

 

728x90
728x90

예외란?

예외란 프로그램 실행 중 발생하는 이벤트로 프로그램 명령의 정상적인 흐름을 방해하는 것이다. 예외가 발생하게 될 경우 예외객체를 만들어 런타임 시스템에 전달한다. 예외객체는 오류발생 당시의 프로그램 상태와 오류정보가 포함되어있다. 이러한 과정을 예외 발생이라고 한다.

예외 요청 흐름

WAS는 스프링 부트가 등록한 에러 설정(/error)에 맞게 요청을 전달하는데, 이러한 흐름을 총 정리하면 다음과 같다.

WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러
-> 컨트롤러(예외발생) -> 인터셉터 -> 서블릿(디스패처 서블릿) -> 필터 -> WAS(톰캣)
-> WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러(BasicErrorController)

컨트롤러 하위에서 예외가 발생하였을 때, 별도의 예외 처리를 하지 않으면 WAS까지 에러가 전달된다. 그러면 WAS는 애플리케이션에서 처리를 못하는 예와라 exception이 올라왔다고 판단을 하고, 대응 작업을 진행한다.

Exception의 종류

자바에서의 예외는 전부 Throwable을 상속받는다.

Error의 경우는 개발자가 직접 사용할 일은 거의없다. 자바가 JVM이나 내부에 있는 라이브러리에서 문제가 될법한 상황에서 나온다. 즉, 애플리케이션이 정상적으로 동작하는데 심각한 문제가 있는 경우이다.

우리가 주로 사용하게 되는 에러는 Exception과 RuntimeException이다.

우리가 사용하는 예외는 크게 Checked Exception, Unchecked Exception 2종류로 분류 할 수 있다.

Exception -> Checked Exception
RuntimeException -> Unchecked Exception

Checked Exception

컴파일 시점에서 Exception을 catch하는지 검사하기 때문에 처리를 하지 않을경우 컴파일 에러가 발생한다. 현 위치에서 처리를 할경우 try/catch를 하여 해결한다. 현 위치에서 처리를 하지 않을 경우 throw 방식을 이용해여 나를 호출한 상위에 위임한다.

Unchecked Exception

Exception이 발생하는 메소드에서 throws 예약어를 이용하여 Exception을 호출 메소드에 전달해야 한다.

Runtime Time Exception 이라고 한다. 컴파일 시점에 Exception을 catch하는지 확인하지 않기 때문에 컴파일 시점에 Exception이 발생할 것인지의 여부를 판단할 수 없다. Exception이 발생하는 메소드에서 throws 예약어를 활용해 Exception을 처리할 필요가 없다. 하지만 처리해도 무방하다.

try,catch와 throw

예외 복구 : 예외가 발생하면 예외 상황에 대해 알맞게 처리하여 복구한다. ex) try, catch

예외 회피 : 예외를 직접 처리하지 않고 예외를 상위 메소드에 위임한다. ex) throw

스프링이 제공하는 다양한 예외처리 방법

Spring은 에러 처리라는 공통 관심사(cross-cutting concerns)를 메인 로직으로부터 분리하는 다양한 예외 처리 방식을 고안하였고, 예외 처리 전략을 추상화한 HandlerExceptionResolver 인터페이스를 만들었다. (전략 패턴이 사용된 것이다.) 대부분의 HandlerExceptionResolver는 발생한 Exception을 catch하고 HTTP 상태나 응답 메세지 등을 설정한다. 그래서 WAS 입장에서는 해당 요청이 정상적인 응답인 것으로 인식되며, 위에서 설명한 복잡한 WAS의 에러 전달이 진행되지 않는다.

예외가 던져지면 디스패처 서블릿까지 전달되는데, 적합한 예외 처리를 위해 HandlerExceptionResolver 구현체들을 빈으로 등록해서 관리한다. 그리고 적용 가능한 구현체를 찾아 예외 처리를 하는데, 우선순위대로 아래의 4가지 구현체들이 빈으로 등록되어 있다.

  • DefaultErrorAttributes: 에러 속성을 저장하며 직접 예외를 처리하지는 않는다.
  • ExceptionHandlerExceptionResolver: 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함
  • ResponseStatusExceptionResolver: Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함
  • DefaultHandlerExceptionResolver:  스프링 내부의 기본 예외들을 처리한다.

Spring은 아래와 같은 도구들로 ExceptionResolver를 동작시켜 에러를 처리할 수 있다.

  1. ResponseStatus
  2. ResponseStatusException
  3. ExceptionHandler
  4. ControllerAdvice, RestControllerAdvice
@RestControllerAdvice
public class ExceptionResponseAdvice {
    /**
     * BaseException 예외처리 핸들러
     * @param e BaseException
     * @return BaseResponse
     */
    @ExceptionHandler(BaseException.class)
    @ResponseStatus(code = HttpStatus.BAD_REQUEST) //응답코드: 400 에러로 일단 통일
    public BaseResponse handlerBaseException(BaseException e){
        return new BaseResponse(e.getStatus());
    }

    /**
     * Exception 예외처리 핸들러
     * @param e Exception
     * @return BaseResponse - 서버 내부에서 에러 발생
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    public BaseResponse handlerException(Exception e){
        e.printStackTrace();
        return new BaseResponse(BaseExceptionStatus.SERVER_INTENER_ERROR);
    }

    /**
     * UnknownHostException 예외처리 핸들러
     * @param e Exception
     * @return BaseResponse - 링크가 잘못되었습니다.
     * @author 박현성
     */
    @ExceptionHandler({UnknownHostException.class, MalformedURLException.class})
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    public BaseResponse unknownHostException(Exception e){
        return new BaseResponse(BaseExceptionStatus.DATA_NAME_INCORRECTION);
    }
			....생략
}

@ResponseStatus는 언제 사용하나?

어노테이션 이름에서 예측가능하듯이 @ResponseStatus는 에러 HTTP 상태를 변경하도록 도와주는 어노테이션이다.

회사 내규에 따라 다르겠지만 Exception이 발생하였을 경우에 Response Body에만 표시해줄지 아니면 Http status code에도 표시할지에 따라서 다를 수 있다. 만약 status code에도 어떤 에러인지 명시적으로 내어주고 싶을 경우에 @ResponseStatus를 사용할 수 있다.

@ExceptionHandler는 언제 사용하나?

@ExceptionHandler는 매우 유연하게 에러를 처리할 수 있는 방법을 제공하는 기능이다. @ExceptionHandler는 다음에 어노테이션을 추가함으로써 에러를 손쉽게 처리할 수 있다.

  • 컨트롤러의 메소드
  • @ControllerAdvice나 @RestControllerAdvice가 있는 클래스의 메소드

@ExceptionHandler는 Exception 클래스들을 속성으로 받아 처리할 예외를 지정할 수 있다. 만약 ExceptionHandler 어노테이션에 예외 클래스를 지정하지 않는다면, 파라미터에 설정된 에러 클래스를 처리하게 된다. 또한 @ResponseStatus와도 결합가능한데,  만약 ResponseEntity에서도 status를 지정하고 @ResponseStatus도 있다면 ResponseEntity가 우선순위를 갖는다.ExceptionHandler는 @ResponseStatus와 달리 에러 응답(payload)을 자유롭게 다룰 수 있다는 점에서 유연하다. 예를 들어 응답을 다음과 같이 정의해서 내려준다면 좋을 것이다.

  • code: 어떠한 종류의 에러가 발생하는지에 대한 에러 코드
  • message: 왜 에러가 발생했는지에 대한 설명
  • erros: 어느 값이 잘못되어 @Valid에 의한 검증이 실패한 것인지를 위한 에러 목록

Spring은 예외가 발생하면 가장 구체적인 예외 핸들러를 먼저 찾고, 없으면 부모 예외의 핸들러를 찾는다. 예를 들어 NullPointerException이 발생했다면, 위에서는 NullPointerException 처리기가 없으므로 Exception에 대한 처리기가 찾아진다.

@ControllerAdvice와 @RestControllerAdvice

Spring은 전역적으로 @ExceptionHandler를 적용할 수 있는 @ControllerAdvice와 @RestControllerAdvice 어노테이션을 각각 Spring3.2, Spring4.3부터 제공하고 있다. 두 개의 차이는 @Controller와 RestController와 같이 @ResponseBody가 붙어 있어 응답을 Json으로 내려준다는 점에서 다르다. ControllerAdvice 어노테이션에는 @Component 어노테이션이 있어서 ControllerAdvice가 선언된 클래스는 스프링 빈으로 등록된다. 그러므로 우리는 다음과 같이 전역적으로 에러를 핸들링하는 클래스를 만들어 어노테이션을 붙여줌으로써 에러 처리를 위임할 수 있다.

예외 복구 범위를 정해보자

메소드 영역 : 메소드 영역은 종속된 복구 기능으로 단순히 try, catch 사용 하면 된다.

클래스 영역 : 클래스 내 공통 예외 복구는 @ExceptionHandler 사용할 수 있다.

전역 영역 : 여러 클래스의 공통 예외 복구는 @ControllerAdvice 사용할 수 있다.

ControllerAdvice는 전역적으로 적용되는데, 만약 특정 클래스에만 제한적으로 적용하고 싶다면 @RestControllerAdvice의 basePackages 등을 설정함으로써 제한할 수 있다.

@ResponseStatus의 코드를 확인해보려면 HttpStatus 안으로 들어가보면 코드를 확인할 수 있다.

Scrap에서는 @ResponseStatus을 400으로 통일을 했습니다.

@Getter
public enum BaseExceptionStatus {

    TEST_ERROR(40401, "실패했어요"),
    SERVER_INTENER_ERROR(2002, "서버 내부적인 에러"),

    // 유저 관련은 3000번 에러
    DUPULICATE_USERNAME(3001, "아이디가 중복됩니다"),
    FAIL_ENCRYPT_PASSWORD(3002, "비밀번호 암호화에 실패했습니다"),
    LOGIN_USER_NOT_EXIST(3003, "해당하는 아이디 또는 비밀번호가 없습니다"),
    NOT_LOGIN_USER(3004,"로그인 하지 않은 유저입니다"),
    USER_NOT_EXIST(3005,"해당하는 유저가 존재하지 않습니다"),

    CATEGORY_NAME_NULL(4444, "카테고리 이름을 입력해주세요"),
    CATEGORY_NAME_LENGTH(4444, "카테고리 이름이 2~60 글자 사이"),
    DATA_NAME_INCORRECTION(44444, "링크가 잘못되었습니다."),
    MYPAGE_USER_NOT_FOUND(44444, "해당 사용자를 찾을 수 없습니다."),

    CATEGORY_NOT_EXIST(4444, "해당 카테고리자 존재하지 않습니다"),
    LINK_NOT_EXIST(4444, "해당 자료가 존재하지 않습니다"),
    LINK_AND_USER_NOT_CORRECT(4444, "해당 유저가 만든 자료가 아닙니다"),
    CATEGORY_AND_USER_NOT_CORRECT(4444, "해당 유저가 만든 카테고리가 아닙니다"),

	... 생략 ...
    
    private final int code;
    private final String message;

    private BaseExceptionStatus(int code, String msg){
        this.code = code;
        this.message = msg;
    }
}

BaseExceptionStatus Enum class

우리가 클라이언트에게 보내줄 에러 코드를 정의해야 한다. 기본적으로 에러 이름과 HTTP 상태 및 메세지를 가지고 있는 에러 코드 클래스를 만들었다. 발생할 수 있는 에러 코드를 위와 같이 정의했다.

Enum class란?

  • 클래스처럼 보이게 하는 상수
  • 서로 관련있는 상수들끼리 모아 상수들을 정의하는것
  • enum 클래스 형을 기반으로 한 클래스형 선언

Enum class 특징

  1. 열거형으로 선언된 순서에 따라 0부터 index 값을 가진다.(순차적으로 증가)
  2. enum 열거형으로 지정된 상수들은 모두 대문자로 선언한다.
  3. 열거형 변수들을 선언한 후 마지막에 세미콜론(;)을 찍지 않는다.
  4. 상수와 특정 값을 연결시킬경우 마지막에 세미콜론(;)을 붙여줘야한다.

상수와 특정값을 연결시켜놓은건데 특정값을 연결시키려면 해당 값들을 리턴할 수 있는 함수가 선언되어있어야한다.

@Getter
public class BaseException extends RuntimeException{

    private final BaseExceptionStatus status;

    public BaseException(BaseExceptionStatus status){
        super(status.getMessage());
        this.status = status;
    }
}

우리가 발생한 예외를 처리해줄 예외 클래스(Exception Class)를 추가해주어야 한다. 우리는 언체크 예외(런타임 예외)를 상속받는 예외 클래스를 다음과 같이 추가해줄 수 있다.

여기서 체크 예외가 아닌 언체크 예외를 상속받도록 한 이유가 있다. 왜냐하면 일반적인 비지니스 로직들은 따로 catch해서 처리할 것이 없므로 만약 체크 예외로 한다면 불필요하게 throws가 전파될 것이기 때문이다.

또한 Spring은 내부적으로 발생한 예외를 확인하여 언체크 예외이거나 에러라면 자동으로 롤백시키도록 처리한다. Spring에서 체크 예외만 롤백을 안하는 이유는 체크 예외는 처리가 강제되기 때문에 개발자가 무언가를 처리할 것이라는 기대 때문이다.

@Getter
@AllArgsConstructor
@JsonPropertyOrder({"code", "message", "result"})
public class BaseResponse<T> {

    private int code;
    private String message;
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private T result;

    // 기본 생성자 막아둠
    private BaseResponse(){}

    public BaseResponse(T result){
        this.code = 1000;
        this.message = "요청에 성공했습니다";
        this.result = result;
    }

    public BaseResponse(String msg, T result){
        this.code = 1000;
        this.message = msg;
        this.result = result;
    }

    public BaseResponse(String msg){
        this.code = 1000;
        this.message = msg;
    }

    public BaseResponse(int code, String msg){
        this.code = code;
        this.message = msg;
    }

    public BaseResponse(BaseExceptionStatus e){
        this.code = e.getCode();
        this.message = e.getMessage();
    }
}

클라이언트로 정해진 포맷의 에러를 던져주도록 하기 위해 위와 같이 에러 응답 클래스를 추가해주었다. 위의 클래스는 다양한 응답에 처리를 해주기 위해 오버로딩을 사용하였다. 또한 만약 errors가 없다면 응답으로 내려가지 않도록 @JsonInclude 어노테이션을 추가하였다.

참조

https://www.nextree.co.kr/p3239/

https://mine-it-record.tistory.com/204

https://mangkyu.tistory.com/204

https://mangkyu.tistory.com/205

728x90
728x90

엔티티 식별자 생성 방식

식별자 생성 방식

  • 직접 할당
    • @Id 설정 대상에 직접 값 설정
      • 사용자가 입력한 값, 규칙에 따라 생성한 값 등
      • 예) 이메일, 주문 번호
    • 저장하기 전에 생성자 할당, 보통 생성 시점에 전달
  • 식별 칼럼 방식
    • DB의 식별 칼럼에 매핑(예, MySQL 자동 증가 칼럼)
      • db가 식별자를 생성하므로 객체 생성 시에 식별 값을 설정하지 않음
    • 설정 방식
      • @GeneratedValue(strategy = Generation.Type.IDENTITY) 설정
    • Insert 쿼리를 실행해야 식별자를 알 수 있음
      • EntityManager#persist() 호출 시점에 Insert 쿼리 실행
      • persist() 실행할 때 객체에 식별자 값 할당됨
  • 시퀀스 사용 방식
    • 시퀀스 사용해서 식별자 생성
      • JPA가 식별자 생성 처리 → 객체 생성 시에 식별 값을 설정하지 않음
    • 설정 방식
      • @SequenceGenerator로 시퀀스 생성기 설정
      • @GeneratedValue로 generator로 시퀀스 생성기 지정
    • EntityManager#persist() 호출 시점에 시퀀스 사용
      • persist() 실행할 때 객체에 식별자 값 할당됨
      • Insert 쿼리는 실행하지 않음
  • 테이블 사용 방식
    • 테이블을 시퀀스처럼 사용
      • 테이블에 엔티티를 위한 키를 보관
      • 해당 테이블을 이용해서 다음 식별자 생성
    • 설정 방식
      • @TableGenerator로 테이블 생성기 설정
      • @GeneratedValue의 generator로 테이블 생성기 지정
    • EntityManager#persist() 호출 시점에 테이블 사용
      • persist() 할 때 테이블을 이용해서 식별자 구하고 이를 엔티티에 할당
      • Insert 쿼리는 실행하지 않음
    • 식별자를 생성할 때 사용할 때 테이블 구조
      • 엔티티 이름 칼럼
      • 식별자 보관 칼럼

https://www.youtube.com/watch?v=Xw9uTs72SVo&list=PLwouWTPuIjUi9Sih9mEci4Rqhz1VqiQXX&index=5 

@Embeddable

  1. 엔티티가 아닌 타입을 한 개 이상의 필드와 매핑할 때 사용
    1. 예 : Adress, Money 등 매핑
  2. 엔티티의 한 속성으로 @Embeddable 적용 타입 사용
@Embeddable
public class Adress {
	@Column(name = "addr1")
	private String address1;
	@Column(name = "addr2")
	private String address2;
	@Column(name = "zipcode")
	private String zipcode;
	
	protected Address() {
	}
	... 생성자, getter 생략

	@Entitiy
	@Table(name = "hotel_info")
	public class Hotel {
		@Id
		@Column(name = "hotel_id")
		private String id;
		....
		@Embedded
		private Adress adress;

같은 @Embeddable 타입 필드가 두 개면?

문제점
@Entity
public class Employee {
	@Id
	private String id;
	@Embedded
	private Address homeAddress;
	@Embedded
	private Address workAddress;
}
서로 같은 컬럼에 매핑이되어서 엔티티메니저팩토리를 초기화할 때 에러가 난다.
해결법
@Entity
public class Employee {
	@Id
	private String id;
	@Embedded
	private Address homeAddress;
	@AttributeOverrides({
		@AttributeOverride(name = "address1", column = @Column(name = "waddr1")),
		@AttributeOverride(name = "address2", column = @Column(name = "waddr2")),
		@AttributeOverride(name = "zipcode", column = @Column(name = "wzipcode"))
	}) 
	@Embedded
	private Address workAddress;
}

정리

  • @Embeddable을 사용하면 모델을 더 잘 표현할 수 있음
  • 개별 속성을 모아서 이해 → 타입으로 더 쉽게 이해
    • (addr1, addr2, zipcode)를 모아서 ‘이게 주소구나’ → ‘주소’네

https://www.youtube.com/watch?v=WtS5IszIueA&list=PLwouWTPuIjUi9Sih9mEci4Rqhz1VqiQXX&index=6 

@Embeddable 다른 테이블에 매핑하기

다른 테이블에 값을 저장할 때

  • @SecondaryTable + 테이블명
@Embeddable
public class Intro {
	@Column(table = "writer_intro", name = "content_type")
	private String contentType;
	
	@Column(table = "writer_intro")
	private String content;
	...
}

@Entity
@SecondaryTable(name = "writer_intro", 
	pkJoinColumns = @PrimaryKeyJoinColumn(
		name = "writer_id", // writer_intro 테이블 칼럼
		referencedColumnName = "id" // writer 테이블 칼럼
	)
)
public class Writer {
	...
	@Embedded
	private Intro intro;
}
  • @SecondaryTable + @AttributeOverride
@Embeddable
public class Address {
	@Column(name = "addr1")
	private String address1;
	@Column(name = "addr2")
	private String address2;
	@Column(name = "zipcode")
	private String zipcode;
}

@Entity
@SecondaryTables({
	@SecondaryTable(name = "writer_address", 
		pkJoinColumns = @PrimaryKeyJoinColumn(
			name = "writer_id",
			referencedColumnName = "id"
	),
	...
})
public class Writer {
	....
	@Embedded
	@AttributeOverrides({
		@AttributeOverride(name = "address1", 
			column = @Column(table= "writer_address", name = "addr1")),
		@AttributeOverride(name = "address2", 
			column = @Column(able= "writer_address", name = "addr2")),
		@AttributeOverride(name = "zipcode", 
			column = @Column(nable= "writer_address"))
	})
	private Address address; 	

정리

  • @SecondaryTable
    • 다른 테이블에 저장된 데이터를 @Embeddable로 매핑 가능
    • 다른 테이블에 저장된 데이터가 개념적으로 밸류(값)일 때 사용
      • 1 - 1 관계인 두 테이블을 매핑할 때 종종 출현

https://www.youtube.com/watch?v=3_sdQGfL2Lg&list=PLwouWTPuIjUi9Sih9mEci4Rqhz1VqiQXX&index=7 

값 컬렉션 Set 매핑

  • 단순 값 Set 매핑
@Entity
@Table(name = "role")
public class Role {
	@Id
	private String id;
	private String name;
	
	@ElementCollection
	@CollectionTable(
		name = "role_perm",
		joinColumns = @JoinColumn(name = "role_id")
	)
	@Column(name = "perm")
	private Set<String> permission = new HashSet<>();

정리

  • 컬렉션 테이블을 이용한 값 set 매핑
    • @ElementCollection과 @CollectionTable이면 끝

https://www.youtube.com/watch?v=lQ4-kVeHVGk&list=PLwouWTPuIjUi9Sih9mEci4Rqhz1VqiQXX&index=8 

값 컬렉션 List 매핑

  • 단순 값 List 매핑
@Entity
@Table(name = "question")
public class Question {
	@Id
	private String id;
	private String text;

	@ElementCollection
	@CollectionTable(
		name = "question_choice",
		joinColumns = JoinColumn(name = "question_id")
	)
	@OrderColumn(name = "idx")
	@Column(name = "text")
	private List<String> choices;

정리

  • 컬렉션 테이블을 이용한 값 List 매핑
    • @ElementCollection과 @CollectionTable, @OrderColumn이면 끝

https://www.youtube.com/watch?v=Wq4B5RpIeAY&list=PLwouWTPuIjUi9Sih9mEci4Rqhz1VqiQXX&index=9 

값 컬렉션 Map 매핑

  • 앞에 Set하고 동일
@Entity
@Table(name = "doc")
public class Role {
	@Id
	private String id;
	private String name;
	private String content;
	@ElementCollection
	@CollectionTable(
		name = "doc_prop",
		joinColumns = @JoinColumn(name = "doc_id")
	)
	@MapKeyColumn(name = "name")
	@Column(name = "value")
	private Map<String, String> props = new HashMap<>();

정리

  • 컬렉션 테이블을 이용한 값 Map 매핑
    • @ElementCollection과 @CollectionTable, @MapKeyColumn이면 끝

https://www.youtube.com/watch?v=CPIgicoqLnM&list=PLwouWTPuIjUi9Sih9mEci4Rqhz1VqiQXX&index=10 

 

728x90
728x90

1 - 3. 스프링 부트 Mapper와 Mapper 오류, Mail 전송

오늘은 회원에 관한 코드들을 스프링에서 스프링 부트로 바꾸는 작업을 진행하였습니다. 코드를 가져와서 실행을 하던 중에 mapper를 찾지 못하는 에러가 발생했습니다. 


1. @Mapper 어노테이션


스프링에서 mybatis를 사용하는 방식은 SqlSession, SqlSessionTemplate을 설정하고 maper네임스페이스. id, parameter 등의 메서드를 통해 쿼리를 사용하였지만 스프링 부트, mybatis 3.0 이상에서는 sqlSessionTemplate을 설정하고, selectone 메서드를 사용하지 않고 @mapper 어노테이션을 이용해 메서드명과 xml 파일의 id를 매핑시켜 편리하게 사용할 수 있다.


mapper 인터페이스에 @Mapper 어노테이션을 붙여주니 해결되었다.

참고 블로그 : https://frozenpond.tistory.com/85

 

[spring] mapper 어노테이션을 통한 springboot, mybatis 세팅하기

spring boot로 프로젝트를 생성, Mybatis 연동하는 예제입니다. 스프링에서 mybatis를 사용하는 방식은 SqlSession, SqlSessionTemplate을 설정하고 selectOne(maper네임스페이스.id, parameter) 등의 메서드를 통..

frozenpond.tistory.com

 


2. 자바 명명규칙

진행하던 중에 프로젝트 패키지 이름이 바뀌고 클래스 이름 등 자바 명명규칙을 어긴 것들을 발견해서 경로들을 수정해주었다. 

참고 블로그 :  https://ozofweird.tistory.com/entry/Java-%EB%AA%85%EB%AA%85-%EA%B7%9C%EC%B9%99


왼쪽은 수정 전 오른쪽은 수정 후 사진이다.

또한 전에 진행했던 데이터 베이스 테이블들 이름이 너무 헷갈려서 명확하게 이름을 다시 지었다.

 


왼쪽은 수정 전 오른쪽은 수정 후 사진이다.


3. Mapper.xml (not found) 오류


유효성 검사를 실시하면 위와 같은 오류가 발생했다. 진짜 찾는데 3일 내내 찾았는데 못 찾았다. 공식문서와 수많은 블로그를 찾아봤는데 다해봤는데도 안됐다. 마지막 4일째 오류를 해결했다... 이유는 Mapper.xml 파일이 하나라도 오류가 나면 전체가 매핑에 실패해 오류가 난다.

mybatis.mapper-locations= mapper/*.xml

application.properties에 위에 코드를 추가시켜준다.


Jsp에서 Mybatis를 이용할 때 스프링 부트에서 만들어주는 resources에 mapper 밑에 Mapper.xml 파일을 추가시켜준다.


보통 Mapper를 찾지 못하는 오류는 mapper namespace의 경로가 잘못되었거나 Mapper.xml 파일의 id와 Mapper.java파일의 메서드 이름이 잘못된 오류이거나 오타이다.


4. 회원가입 인증 메일 전송

회원가입을 할 때 인증번호를 보내는 기능이 있었는데 구글 계정만 있으면 무료로 발송할 수 있는 Gmail SMTP Server를 이용했다. 메일전송은 MailSender 인터페이스를 상속받은 JavaMailSender를 사용해 구현하였습니다. 새로 스프링 부트에는 dependency와 설정이 빠져있어 오류가 발생했다.

implementation 'org.springframework.boot:spring-boot-starter-mail'

build.gradle dependencies에 위에 코드를 작성한다.



Gmail SMTP Server를 이용하려면 위와 사진과 같은 설정이 필요하다.

위의 설정은 application.properties에 추가 해줘야 한다.

spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.username=gmail
spring.mail.password=password

잘되는것을 확인할 수 있다.


오늘의 회고

mapper 오류에 4일을 걸렷다ㅠㅠ 부트로 바꾸는 프로젝트를 진행하기 전에 제가 진행했던 프로젝트여서 코드 분석을 하지 않고 바로 진행했던 점이 문제로 다가왔다. 프로젝트를 끝낸지 7개월이라는 시간이 흘렀고 제가 작성하지 않은 코드들도 있어서 파악이 안되는 문제가 발생했다. 오늘 내일은 코드를 분석하는 작업을 진행하려고 합니다. 분석을 끝내고 위에 진행했던 코드들의 테스트 코드를 블로그에 작성하겠습니다.

728x90
728x90

오늘은 스프링 부트에서 MyBatis 연결을 하려고 합니다. 

1-2 스프링 부트 MyBatis 연결

1. 스프링 부트 MyBatis 설정

implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2'
runtimeOnly 'mysql:mysql-connector-java'

build.gradle dependencies에 위의 코드를 추가시켜줍니다. 

# database
spring.datasource.url: jdbc:mysql://host:3306/DatabaseName?characterEncoding=utf8
spring.datasource.username: DatabaseName
spring.datasource.password: password
spring.datasource.driver-class-name: com.mysql.cj.jdbc.Driver

# mybatis
mybatis.config=mybatis-config.xml
mybatis.type-aliases-package: com.example.demo.model
mybatis.mapper-locations: mapper/*.xml

application.properties에 위에 코드를 추가 시켜줍니다. url에는 자신의 포트번호와 데이터베이스 이름을 입력해주고, 유저네임과 패스워드도 자신과 맞게 입력해줍니다. mapper-locations도 mapper. xml이 있는 파일로 설정해주면 끝입니다.


2. DataBase연결 Test

이제 DataBase연결이 잘 되었는지 Test를 해보려고 합니다. Test에 dbTest페키지를 만들고 MyBatisTest class파일을 만들어 줍니다.

package com.skylife_Transformation.dbTest;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.sql.DriverManager;

public class MyBatisTest {
    // MySQL Connector 의 클래스. DB 연결 드라이버 정의
    private static final String DRIVER = "com.mysql.cj.jdbc.Driver";
    // DB 경로
    private static final String URL = "jdbc:mysql://localhost:port/name?serverTimezone=UTC&allowPublicKeyRetrieval=true&useSSL=false";
    private static final String USER = "name";
    private static final String PASSWORD = "password";

    @Test
    public void testConnection() throws Exception {
        // DBMS에게 DB 연결 드라이버의 위치를 알려주기 위한 메소드
        Class.forName(DRIVER);
        try {
            Connection connection = DriverManager.getConnection(URL, USER, PASSWORD);
            System.out.println(connection);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

MyBatisTest파일에 위의 코드를 추가시켜줍니다. DB 커넥션이 제대로 생성되었는지를 출력해 확인해보는 과정입니다.

DB URL에 allowPublicKeyRetrieval은 클라이언트 옵션을 추가 하여 클라이언트가 서버에서 자동으로 공개 키를 요청할 수 있도록 하는 것입니다. default는 false이고 MySQL 8.0 이상은 이를 true로 지정하지 않을 시 public key retrieval is not allowed 에러가 발생할 수 있습니다. 그 뒤에 userSSL은 default는 true이고 false로 지정 시 SSL 사용을 막을 수 있다. 또한 하지만 개발, 혹은 테스트 중에서만 비활성화시키는 걸 추천합니다. 배포 시엔 useSSL를 false로 지정하기보단 SSL을 설정해주면 됩니다.


위의 Test코드를 실행하면 정상적으로 실행되는 것을 확인 할 수 있다. 


참고 블로그

https://doozi0316.tistory.com/entry/Spring-Boot-MyBatis-MySQL-%EC%97%B0%EB%8F%99-%EB%B0%A9%EB%B2%95


오늘의 회고

오늘은 MyBatis연결부터 연결 테스트 까지 진행하였습니다. URL을 입력할 때 allowPublicKeyRetrieval, userSSL은 기존에 사용했었지만 개념과 사용 이유에 대해 몰랐었는데 새롭게 알게 되었습니다. 리펙토링을 진행하다가 빠진 개념 등 다시 정리하면서 가야 될 것 같습니다.

728x90

+ Recent posts