Backend/SpringBoot

[기본구조2] JPA 초기 설정 (DB 관리) 및 Entity 예제(validation 포함)

Dean83 2024. 10. 22. 16:25

spring boot 에서 DB 연계를 위해서는 JPA를 통해 진행해야 한다. 

 

  • build.gradle 에 다음을 추가
    • implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  •  appication.yml 설정 추가 (예시)
spring:
  datasource:
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    hibernate:
      ddl-auto: update   # 운영 환경에서는 validate 또는 none 권장
    show-sql: true        # 실행되는 SQL 출력. product 에서는 false
    properties:
      hibernate:
        format_sql: true   # SQL 보기 좋게 정렬
        highlight_sql: true # (Hibernate 5.5+) ANSI 색상 강조
        use_sql_comments: true # JPQL -> SQL 변환 시 주석 추가
        # 사용할 데이터베이스 방언(Dialect) 지정
        dialect: org.hibernate.dialect.H2Dialect
    database-platform: org.hibernate.dialect.MySQL8Dialect
    open-in-view: false   # OSIV 비활성화 (서비스 계층에서만 영속성 사용)

  sql:
    init:
      mode: never   # schema.sql, data.sql 실행 여부 (never, embedded, always)
  #  defer-datasource-initialization: true
  #  ↑ JPA가 스키마 생성 후 data.sql 실행하도록 보장 (Spring Boot 2.5+)

logging:
  level:
    org.hibernate.SQL: DEBUG           # 실행되는 SQL 로그
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE  # 바인딩 파라미터 로그
    org.springframework.orm.jpa: INFO
    org.springframework.transaction: DEBUG
  • 실제 DB 접속 정보는 env 파일에 별도로 두고 해당 값을 읽어온다. env 파일은 git에 올리면 안된다. 
  • open-in-view
    • true 설정시 EntityManager 를 웹 요청이 끝날때까지 사용가능하도록 하여 Persistance를 유지한다. 즉, Controller 에서도 사용가능하다.
    • false 설정시 트랜잭션을 관리하는 Service가 끝나게 되면, 사용할 수 없다.
    • lazy initialization 오류를 방지할 수 있는 옵션으로, 서비스에선 true로, 개발시에는 false로 두어 오류를 잡는것이 좋다.
  •  sql.init.mode
    • 이 옵션을 쓸 일은 잘 없고, 보통 flyway를 써서 관리한다고 한다.  
    • src/main/resources/data/sql 쿼리문이 있을 경우, 해당 쿼리문을 자동실행함 (임시 데이터 삽입 등) 
    • never : 실행안함
    • embedded :내장 DB(H2, HSQL, Derby 등)**일 때만 실행. MySQL, PostgreSQL 같은 외부 DB에서는 실행 안 됨. 개발/테스트 기본값
    • always :DB 종류와 상관없이 항상 실행. 운영/개발 구분 없이 schema.sqldata.sql을 실행함

 

 

이 중, ddl-auto 옵션을 설명하면, 

spring:
    jpa:
        hibernate:
            ddl-auto: update

 

  • update : 엔티티 변경부분을 db에 반영
  • none : 엔티티가 변경되어도 아무것도 안함
  • validate : 실제 DB와 엔티티에 차이점이 있는지만 검증
  • create : 서버가 시작될때 DB를 새로 생성
  • create-drop : 서버가 종료될때 DB를 삭제
  • 개발환경에서는 update를 사용하고, 실제 운용에선 none 이나 validate 만 사용한다고 한다.



 

  • 엔티티 
    • DB 와 소스코드를 매핑한 자바클래스. 클래스 조작을 통해 DB에 반영 가능
    • DB 구조와 동일한 Java Class 를 생성
    • 테이블 매핑을 위해서 @Table(name="테이블명") 어노테이션을 쓰는게 좋다.
    • DB 에서 생성된 key 맴버변수인 경우에는 @Id 와 @GeneratedValue(strategy=GenerationType.IDENTITY) 를 적어준다
      • 만일 개발자가 생성한 키값을 이용한 경우에는 @Id 어노테이션만 붙여준다
    • 예 : id (키값), 이름, 연락처, 주소 DB 가 있다고 한다면, 다음과 같다
    • column 어노테이션은 쓰지 않아도 되긴 하다.
      • name 으로 컬럼명을 지정할 수 있고, length 등으로 옵션 설정을 할 수 있다
    • @Transient 어노테이션을 이용하면, 해당 맴버변수는 매핑에서 제외한다.
    • @Enumerated
      • 맴버변수와 컬럼 매핑시 Enum으로 매핑할때 사용하는 어노테이션
      • @Enumerated(EnumType.STRING) 을 주로 사용하여, DB에는 문자열로 저장한다.
    • DB의 시간값과 매핑하기 위해 LocalDateTime (타임존 없을 경우) 혹은 OffsetDateTime(타임존 있는 경우) 자료형을 이용해 맴버변수를 정의할 수 있다.
    • 접근 할때에는 getter 를 사용중이므로 getter (예 : getname, getphoneNumber ,,,)를 통해 가져올 수 있다.
    • setter 를 통해 직접 값을 설정할 수도 있으나, getter만 설정하고 값 설정은 별도의 함수로 처리하는것이 좋다.
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@Table(name="addresses")
public class AddressTable
{
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    Long idx;

    @Column(length=200)
    String name;

    @Column(length=11)
    String phoneNumber;

    @Column(columnDefinition = "TEXT")
    String address;

}

 

  • 1:N, N:1 관계의 Entity 생성
  • N:1 로 표현을 하고, N이 주도권을 갖는다. 양방향으로 설정하여 1에 해당하는 엔티티에서 데이터를 활용할 수 있다.
  • 만일, 위의 클래스를 맴버변수로 갖는 엔티티 클래스가 있다면(N에 해당), foreign key로 묶이게 되는데, 이때에는 관계를 명시 해주어야 한다. 
    • n:1 관계이고, n이 주도권을 갖고 있다.
      • 이유. 1:N 관계가 주도권을 갖게 되면, N 에 해당하는 엔티티의 외래키가 null 이나 이상한 값으로 설정이 되고, 이후 1 에 해당하는 엔티티가 DB에 작성될때 다시 N 에 해당하는 엔티티의 외래키가 수정된다. 따라서 불필요한 쿼리가 많아지고, 외래키 설정이 not null 일 경우 문제가 된다.
    • @ManyToOne
    • 대부분은 이에 해당한다. n 쪽이 주인이 된다 (DB에서 외래키 필드를 갖고 있으므로)
      • (하위개념이 주인이 된다고 보면 된다)
    • 이 예에서 보자면, 1개의 Address에 다수의 추가 정보가 있는 관계가 된다. 
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@table(name="addtionals")
public class AdditonalClass
{
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    Long idx;

	...

	@ManyToOne
    @JoinColumn(name=외래키 이름)
    AddressTable address;

}

 

  • 1에 해당하는 엔티티의 양방향성 설정
    • @OneToMany 를 이용하고, 변수를 list 로 두어야 한다.
    • 1:N 관계이나, 위의 N이 주인이 되어 매핑을 하였고, 1의 입장에서도 N 을 쓰고 싶을때 쓴다. (ManyToOne의 양방향 관계)
    • 항상 mappedBy를 써야 한다
    • mappedBy 에는 N에 해당하는 클래스(주인역할)의 맴버변수 중  이 클래스 자료형을 갖는 맴버변수명
      • 예에서는 AdditionalClass의 address 맴버변수
    • Cascade
      • foreign key 로 묶인 관계를 나타내는 설정이다. AddressTable 의 항목에 변경이 이뤄졌을때 AdditionalClass 의 항목이 어떻게 동작해야 하는지 설정한다.
      • 데이터 무결성 문제가 있을 수 있으니 잘 고려해야 한다. 
      • 하나의 부모만이 자식 항목들을 소유하고 있을때 사용해야 한다. 만일 자식을 공유한다면, 데이터 무결성 문제가 발생할 수 있다. 
        • ALL : 모든 속성 부여
        • PERSIST : 연관된 엔티티가 영구 보관될때 같이 영구 보관
        • MERGE : 연관된 엔티티가 병합될때 같이 병합
        • REMOVE : 연관된 엔티티가 삭제될때 같이 삭제 
        • REFRESH : 연관된 엔티티가 새로고침 될때 같이 새로고침
        • DETACH : 연관된 엔티티가 영구 보관에서 분리될 경우 같이 분리
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter

public class AddressTable
{
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    Long idx;

	...

	@OneToMany(mappedBy=AdditionalClass의맴버변수명, cascade=CascadeType.REMOVE)
    List<AddtionalClass> aditionals;

}

 

  • 하위개념, 즉 주인이 되는 엔티티를 추가 할때 에는 양방향 모두 값을 추가 해줘야 한다
  • AdditionalClass에 값이 추가될 경우, AddressTable 클래스에 값도 추가해야 한다.
//Address 서비스 코드에서 추가한다.

public void addAdditionalInfo(AdditionalClass item) {
    this.additionals.add(item);
    item.setAddress(this); // 
}

 

  • 1:1 관계에서의 Entity
    • 외래키가 있는 쪽이 주인이 된다. (하위개념이 주인이 된다고 보면 된다)
    • @OneToOne 어노테이션을 사용한다.
      • 만일 양방향으로 구성할 경우에는,  
        • 주인 쪽에서는, @OneToOne 과 @JoinColumn을 사용한다.
        • 주인이 아닌 쪽에서는 @OneToOne 과 mappedBy를 사용한다.
        • 마찬가지로 하위개념 항목이 추가될 경우에는 양쪽 다 추가를 해주어야 한다.

 

 

  • Spring boot Validation
    • Entity에 국한된것은 아니나, 변수값의 validation을 설정 할 수 있다.
    • null 이 되면 안되는 값, 특정 size 가 요구되는 값 등
    • Entity, DTO 에서 주로 사용 (컨트롤러 인자값 확인등)
    • 설치
      • build.gradle 에서 다음을 추가
        • implementation 'org.springframework.boot:spring-boot-starter-validation'
    • import 추가
      • import jakarta.validation.constraints.* 
        • * 부분은 사용되는 validation 어노테이션에 따라 다르다. notnull, size 등등
    • 사용
      • 어노테이션을 이용해 변수 앞에 적어주면 된다. 
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

@Getter
@Setter
@NoArgsCoinstrucor
public class Test {

    @NotNull(message = "value1은 null이면 안됩니다")
    public String value1;

    @Size(min = 2, max = 2, message = "value2는 2글자여야 합니다")
    public String value2;


}

...

import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import jakarta.validation.Valid;

@RestController
@RequestMapping("/api/test")
@Validated
public class TestController {

    @PostMapping
    public ResponseEntity<String> createTest(@Valid @RequestBody Test request) {
        // 검증 통과 시 로직 수행
        return ResponseEntity.ok("Validation Passed: value1=" + request.getValue1() +
                                 ", value2=" + request.getValue2());
    }
}

 

어노테이션대상주요 속성설명은 아래와 같다.

@NotNull 모든 객체 message null이면 안 됨
@NotEmpty 컬렉션, 문자열 message null이 아니고 비어있으면 안 됨
@NotBlank 문자열 message null이 아니고, 공백 문자만으로 구성되면 안 됨
@Size 문자열, 컬렉션, 배열, Map min, max, message 길이/크기 제한
@Min 숫자(Long, Integer 등) value, message 최소값 지정
@Max 숫자 value, message 최대값 지정
@DecimalMin BigDecimal, String value, inclusive, message 최소값 지정 (소수 가능)
@DecimalMax BigDecimal, String value, inclusive, message 최대값 지정 (소수 가능)
@Positive 숫자 message 0보다 큰 값
@PositiveOrZero 숫자 message 0 이상
@Negative 숫자 message 0보다 작은 값
@NegativeOrZero 숫자 message 0 이하
@Email 문자열 message, regexp 이메일 형식 검사
@Pattern 문자열 regexp, flags, message 정규식 패턴 검증
@Past Date, LocalDate 등 message 과거 날짜여야 함
@PastOrPresent Date, LocalDate 등 message 과거 또는 현재 날짜
@Future Date, LocalDate 등 message 미래 날짜여야 함
@FutureOrPresent Date, LocalDate 등 message 미래 또는 현재 날짜
@AssertTrue boolean message 값이 true여야 함
@AssertFalse boolean message 값이 false여야 함