728x90

https://denodo1.tistory.com/322

 

Unrecognized token 'id': was expecting ('true', 'false' or 'null')

Request Body로 보내지는 JSON의 행방 불명문제api 테스트 시 Postman을 자주 사용하는데, 다음과 같이 JSON을 서버에 보내면,서버의 Request에서 JSON 정보를 찾을 수 없다.해결하지만 동일한 json 데이터를 p

denodo1.tistory.com

 

728x90

Entity 객체를 Builder하여 save시키는 단순한 로직인데 500에러가 발생을 한다고 Fe개발자에게 연락을 받았습니다.

( 회사 실무가 아닌 주말 스터디 팀이에요... ㅎㅎㅎㅎ 500에러 후,,,, 다행 )

바로 aws서버 로그를 까서 보았습니다.

해당 필드에 null값이 들어가면서 sql문에 오류가 발생한 것 인데요.

기존에 잘 쓰던 로직이라 처음에는 뭔가 이해가 되지 않더라구요.

 

바로 최근 변경내역에 대한 히스토리를 보다가 문득 아차싶은 부분을 볼 수 있었습니다.

변경 전 : @GeneratedValue

변경 후 : @GeneratedValue(strategy = GenerationType.IDENTITY)

 

최근 Entity에 대하여 IDENTITY 옵션을 주며 수정을 했습니다.

해당 옵션을 주면서 장애가 발생하게 된 것인데요.

결론부터 말해서 수정방법은 아래와 같습니다.

자동증가(AI)옵션을 추가해주면 됩니다.

 

우리는 개발자이기에 왜 멀쩡하던 기능이 option하나 주었다고 왜 이렇게 장애로 바뀐것 인지 탐구해 볼 필요가 있는데요.

 

yml파일에서 ddl-auto: create로 보통 쓰시진 않을 것 으로 생각합니다.

create 였다면, 해당 테이블을 삭제 후 새로 생성하면서 AI옵션을 넣어주었을 것이고, 물론 장애도 안생겼겠죠.

하지만 update, none으로 설정하였기에 테이블을 새로 생성하진 않습니다.

 

이러면서 문제가 발생 합니다.

기존 @GeneratedValue의 경우 

save() 처리 시 hibernate_sequence테이블에서 id값을 할당받아 자동으로 채우고 insert를 시키게 되는데

변경 된 @GeneratedValue(strategy = GenerationType.IDENTITY)의 경우

save() 처리 시 자동증가값을 사용하도록 설정이 되어 있기에 id값이 sql에 없는걸 확인 할 수 있습니다.

 

눈치 채셨나요? 네 DB와 JPA가 Save()의 sql문이 상이한 것 입니다.

현재 DB는 AI설정이 off이므로 sql에 필수 값 이였던 것 이죠.

 

IDENTITY 외 테이블 방식도 있는데 어떤 DB와 전략을 사용하느냐에 따라 설정하고 DB까지 맞춰줘야겠군요.

 

GeneratedValue설정을 변경 할 때 어떻게 동작하게 될지. DB에 대한 영향도를 생각안하고 변경만 해버린 제 실수가 500장애를 발생시켰습니다.

JPA를 사용하기에 앞서 많은 이해와 영향도를 생각하는 개발자가 되도록 준비해야겠내요.

 

https://kounjeong.tistory.com/20

 

JPA - Field 'id' doesn't have a default value

안녕하세요. Ruk입니다. 요즘 한창 JPA를 탐구하고 사용해보는 중인데요. 이번에 발생한 에러에 대해 적어보려고 해요. Entity 객체를 Builder하여 save시키는 단순한 로직인데 500에러가 발생을 한다고

kounjeong.tistory.com

 

 


JPA 기본키 생성 전략, @GeneratedValue 사용시 주의점

 

JPA로 테이블과 엔티티를 매핑할 때, 식별자로 사용할 필드 위에 @Id 어노테이션을 붙여 테이블의 Primary Key와 연결 시켜줘야한다.

이 때, 컬럼 명을 따로 지정하지 않으면, 관례에 따라 매핑되는 테이블 컬럼명은 camelCase로 작성된 필드명을 snake_case로 바뀐 테이블 컬럼을 찾아서 매핑시켜준다. ex) memberId -> member_id , orderItemId -> order_item_id 

 

@Column 어노테이션을 활용하여 테이블의 pk 컬럼을 따로 지정할 수도 있다.

public class  Member {
    @Id  @Column(name = "member_id") // 컬럼명 따로 지정
    private Long id;
    
    }

 

 

이렇게 @Id로 식별자필드와 테이블의 PK를 매핑만 시켜놓으면, 식별자로 사용될 값을 일일히 수동으로 넣어줘야 하는 불편함이 있는데, @GeneratedValue 를 사용하면 이를 해결할 수 있다.

 

@GeneratedValue 어노테이션을 사용하면 식별자 값을 자동 생성 시켜줄 수 있다.

@GeneratedValue에는 3가지 전략이 있고, JPA에게 전략 선택을 위임하는 옵션인 AUTO 옵션을 포함해, 총 4가지 옵션이 존재한다.

 

1. GenerationType.AUTO 옵션.(자동으로 IDENTITY, SEQUENCE, TABLE 中 택 1)

 

 hibernate.dialect에 설정된 DB 방언 종류에 따라, 하이버네이트가 자동으로 전략을 선택하게끔 위임한다.

@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>

*주의 :하이버네이트를 무조건 믿어선 안된다! Mysql의 경우 Auto로 설정하면 당연히 Identity전략을 취할 것이라 생각하고 생략하거나, 추후 DBMS 종류 변경을 고려해 그냥 Auto로 사용하는 경우가 있는데, 버전에 따라 선택되는 전략이 달라질 수 있으므로, 직접 DBMS에 맞는 전략을 지정해주도록 한다.

Mysql일 때 GenerationType.AUTO 에서의 전략 선택 알고리즘(빨간 화살표 흐름)

 

Hibernate 5부터 MySQL에서의 GenerationType.AUTO는 IDENTITY가 아닌 TABLE을 기본 시퀀스 전략으로 가져간다.

참고 : https://jojoldu.tistory.com/295

 

2. IDENTITY 전략

@Id @GeneratedValue(strategy = GenerationType.IDENTITY) 
	private Long id;

- 기본 키 생성을 데이터베이스에 위임한다. 

- 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용한다. (예: MySQL의 AUTO_ INCREMENT)

- IDENTITY 전략은, em.persist()로 객체를 영속화 시키는 시점에 곧바로 insert 쿼리가 DB로 전송되고, 거기서 반환받은 식별자 값을 가지고 1차 캐시에 엔티티를 등록시켜 관리한다.

 

JPA는 보통의 경우에, 트랜잭션이 commit 되는 시점에 쓰기 지연 저장소에 모아놓은 SQL을 한 번에 DB로 전송하며 실행한다. 이렇게 해야 어플리케이션과 DB 사이에 네트워크를 오가는 횟수가 줄어들고 성능면에서 이득을 볼 수 있기 때문이다.

 

하지만 IDENTITY전략은 DB에 기본키 생성을 위임하므로, Mysql의 경우 AUTO_INCREMENT를 활용하여 생성하는데,

이 때, JPA 입장에선 DB에 INSERT SQL를 실행하기 전엔 도저히 AUTO_INCREMENT되는 값을 알 수 없으므로, persist() 시점에 insert 쿼리가 실행되는 것이다. (영속성 컨텍스트로 엔티티를 관리하려면 1차 캐시에 Id값을 key 값으로 들고 있어야 하기 때문에)

 

아래 그림에서 1차 캐시의 Key, Value 값 구조와 쓰기 지연 SQL 저장소와 flush()가 트랜잭션 commit 직전에 이루어지는 순서를 보면 이해가 쉬울 것이다.

 

 

 

3. SEQUENCE 전략

@Entity 
@SequenceGenerator( 
     name = “MEMBER_SEQ_GENERATOR", 
     sequenceName = “MEMBER_SEQ", //매핑할 데이터베이스 시퀀스 이름
     initialValue = 1, allocationSize = 50) 
public class Member { 
     @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, 
     generator = "MEMBER_SEQ_GENERATOR") 
     private Long id; 
 
 }

- DB의 시퀀스를 활용하여 Id값을 증가시킨다.

- 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트(예: 오라클 시퀀스)

- 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용한다.

 

sequenceName 으로 시퀀스를 분리하여 지정할 수 있고, allocationSize로 한 번에 사용할 시퀀스 덩어리 사이즈를 정해서 최적화 할 수 있다.

 

4. TABLE 전략

 

@Entity 
@TableGenerator(
     name = "MEMBER_SEQ_GENERATOR", 
     table = "MY_SEQUENCES", 
     pkColumnValue = “MEMBER_SEQ", allocationSize = 1) 
public class Member { 
     @Id 
     @GeneratedValue(strategy = GenerationType.TABLE, 
     generator = "MEMBER_SEQ_GENERATOR") 
     private Long id; 
 }

- 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략이다.

- 모든 데이터베이스에 적용 가능하나, 성능적인 손해가 있어서 잘 쓰지 않는다.

 

 

결론 : @GeneratedValue 사용시, 반드시  @GeneratedValue(strategy = GenerationType.전략명) 와 같은 형태로 GenerationType 명시해줘야 한다! 기본값인 AUTO는 못미덥다. 사용하지 말자!

 

JPA와 Mysql 사용시, @GeneratedValue의 GenerationType은 IDENTITY로 설정해서 쓰자!

728x90
Spring Batch 에서는 meta table이라고 해서 Batch 의 작업을 도와주는 테이블이 있는데...

 

 

 

 

여하튼 서론은 막론하고 MySQL 8.0 버전을 올리면서 이후부터는 System Variable 로 sql_require_primary_key 가 true 가 걸려있게 되는 경우 application.yml 에서 설정을 해도 작동을 하지 않는다 이말이다

 

전통적으로는 spring.batch.jdbc.

initialize-schema: always

 

이렇게 때려박아 주고 있는데, 여하튼 이거랑 별도로 환경변수에서 저렇게 잡아놓으면 데이터베이스가 생성하고 싶어도 생성할 수가 없다.

 

 

그렇다면 Spring Batch 공식문서로 가보자


The BATCH_JOB_EXECUTION_PARAMS Table

The BATCH_JOB_EXECUTION_PARAMS table holds all information relevant to the JobParameters object. It contains 0 or more key/value pairs passed to a Job and serves as a record of the parameters with which a job was run. For each parameter that contributes to the generation of a job’s identity, the IDENTIFYING flag is set to true. Note that the table has been denormalized. Rather than creating a separate table for each type, there is one table with a column indicating the type, as the following listing shows:

CREATE TABLE BATCH_JOB_EXECUTION_PARAMS  (
	JOB_EXECUTION_ID BIGINT NOT NULL ,
	PARAMETER_NAME VARCHAR(100) NOT NULL ,
	PARAMETER_TYPE VARCHAR(100) NOT NULL ,
	PARAMETER_VALUE VARCHAR(2500) ,
	IDENTIFYING CHAR(1) NOT NULL ,
	constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID)
	references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
);
 

The following list describes each column:

  • JOB_EXECUTION_ID: Foreign key from the BATCH_JOB_EXECUTION table that indicates the job execution to which the parameter entry belongs. Note that multiple rows (that is, key/value pairs) may exist for each execution.
  • PARAMETER_NAME: The parameter name.
  • PARAMETER_TYPE: The fully qualified name of the type of the parameter.
  • PARAMETER_VALUE: Parameter value
  • IDENTIFYING: Flag indicating whether the parameter contributed to the identity of the related JobInstance.

Note that there is no primary key for this table. This is because the framework has no use for one and, thus, does not require it. If need be, you can add a primary key with a database generated key without causing any issues to the framework itself.


 

간단하게 요약하자면 원래 이 테이블에는 Primary Key 따위는 없는 것인데, 네놈이 꼴린다면 generated Key 를 생성해서 그 키를 Primary Key로 쓸 수 있다 이말씀이다.

 

참고로 MySQL에서 SEQUENCE 를 생성해서 사용하려고 해도 PRIMARY KEY 때문에 작동하지 않을 것이므로 

 

여하튼 데이터베이스를 알게된 이래로 generated key 라는 놈은 처음 들어봣는데

 

 

내용을 잘 살펴보아하자니 프로세스가 진행될 때마다 증가하면서 자동으로 생성되는 놈이라하니 꼭 AI 를 박아놓은 친구처럼 생겼다.

 

그래서 만들어보았다 generated key

CREATE TABLE BATCH_JOB_EXECUTION_PARAMS (
    PARAM_ID BIGINT AUTO_INCREMENT PRIMARY KEY,
    JOB_EXECUTION_ID BIGINT NOT NULL,
    TYPE_CD VARCHAR(6) NOT NULL,
    KEY_NAME VARCHAR(100) NOT NULL,
    STRING_VAL VARCHAR(250),
    DATE_VAL DATETIME(6) DEFAULT NULL,
    LONG_VAL BIGINT,
    DOUBLE_VAL DOUBLE PRECISION,
    IDENTIFYING CHAR(1) NOT NULL,
    CONSTRAINT JOB_EXEC_PARAMS_FK FOREIGN KEY (JOB_EXECUTION_ID)
        REFERENCES BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
) ENGINE=InnoDB;

INSERT INTO BATCH_STEP_EXECUTION_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_STEP_EXECUTION_SEQ);
CREATE TABLE BATCH_STEP_EXECUTION_SEQ (
                                        SEQ_ID  BIGINT  AUTO_INCREMENT  PRIMARY KEY ,
                                          ID BIGINT NOT NULL,
                                          UNIQUE_KEY CHAR(1) NOT NULL,
                                          constraint UNIQUE_KEY_UN unique (UNIQUE_KEY)
) ENGINE=InnoDB;


CREATE TABLE BATCH_JOB_EXECUTION_SEQ (
                                         SEQ_ID  BIGINT  AUTO_INCREMENT  PRIMARY KEY ,
                                         ID BIGINT NOT NULL,
                                         UNIQUE_KEY CHAR(1) NOT NULL,
                                         constraint UNIQUE_KEY_UN unique (UNIQUE_KEY)
) ENGINE=InnoDB;

INSERT INTO BATCH_JOB_EXECUTION_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_JOB_EXECUTION_SEQ);

CREATE TABLE BATCH_JOB_SEQ (
                               SEQ_ID  BIGINT  AUTO_INCREMENT  PRIMARY KEY ,
                               ID BIGINT NOT NULL,
                               UNIQUE_KEY CHAR(1) NOT NULL,
                               constraint UNIQUE_KEY_UN unique (UNIQUE_KEY)
) ENGINE=InnoDB;

INSERT INTO BATCH_JOB_SEQ (ID, UNIQUE_KEY) select * from (select 0 as ID, '0' as UNIQUE_KEY) as tmp where not exists(select * from BATCH_JOB_SEQ);

 

이러면 쌈뽕하게 작동한다

728x90

첫번째 미션으로 유저 객체를 생성할 때 Builder 패턴을 이용해야 했다.

프로그래머스 스터디 뿐만 아니라 이전에 참고했던 여러 서적들(책 ‘처음 배우는 스프링 부트2’와 ‘스프링 부트와 AWS로 혼자 구현하는 웹서비스’)에서도 빌더 패턴을 사용해서 구현한다. 무엇보다도 찾아봤던 블로그, 책마다 빌더 패턴을 구현하는 모습이 서로 조금씩 달라서 빌더패턴을 코드로 이렇게 작성하는게 맞는건지 너무 헷갈렸다.

왜 빌더 패턴을 사용해야 하는 걸까? 빌더 패턴과 생성자의 차이는 무엇이고 어떤 상황에서 사용해야 하는 걸까? 코드로는 어떻게 구현할까? 이에 대해 한번 정리해 봐야겠다.


먼저 생성자란,

인스턴스가 생성될 때 호출되는 인스턴스 초기화 메소드이다. 연산자 new가 인스턴스를 생성하는 것이지 생성자가 인스턴스를 생성하는 것은 아니다.

User Sara = new User();

 

빌더패턴이란

생성자처럼 인스턴스를 초기화하고 new 대신 build() 메소드를 이용해 객체를 생성하는 방법이다. 기존 생성자의 문제점을 보완해서 객체 생성을 더 깔끔하고 유연하게 할 수 있다.

 

빌더 패턴은 왜 / 언제 사용해야할까?

일반 생성자 이용시, 인자 수가 늘어나면 코드 작성이 어려워지고 읽기 어려운 코드가 되고 만다

NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 3, 35, 27);

호출 코드만 보고서는 저 많은 인자가 도대체 무슨 값인지 알 수가 없고 특히 자료형이 같은 인자들이 많을 경우 컴파일 단계에서 걸러지지 않기 때문에 런타임시 문제 발생하기가 딱 좋다 - 실수하기 좋은 구조.

대안책으로 setter메소드를 이용한 자바빈 패턴이 있다.

// cocaCola을 영양 성분 정보를 setter메소드를 통해 초기화하는 자바빈 패턴
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohdydrate(27);

가독성은 좋아졌지만, 1회의 함수 호출로 객체 생성을 끝낼 수 없고 setter메서드로 인해 Immutable한 클래스를 만들 수가 없다. Setter 메소드 사용은 지양해야하며 변경 가능성은 최소화해야한다.

따라서 가독성도 좋고 immutable 객체로도 만들 수 있는 Builder 패턴을 사용하게 된다.

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                                            .calories(100)
                                            .sodium(35)
                                            .carbohydrate(27)
                                            .build();

각 인자가 어떤 값을 의미하는지 알기가 쉽고 setter메소드가 없으므로 변경 불가능한 객체도 만들 수 있다. 한번에 객체를 생성하므로 객체 일관성이 깨지지 않으며, build() 함수로 잘못된 값이 입력되었는지 검증하게 할 수도 있다.

Builder패턴은

  • 생성자의 인자수가 많을 때 사용하며
  • 불변성과 네임드 파라미터(Named Parameter, 매개변수 이름을 통해서 인자 값을 전달)를 가져갈 수 있어
  • 안전하고 가독성이 좋다.

 

빌더패턴을 코드로 작성해보자

public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder {
        // Required parameters(필수 인자)
        private final int servingSize;
        private final int servings;

        // Optional parameters - initialized to default values(선택적 인자는 기본값으로 초기화)
        private int calories      = 0;
        private int fat           = 0;
        private int carbohydrate  = 0;
        private int sodium        = 0;

        public Builder() {
        }

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings    = servings;
        }

        public Builder calories(int val) {
            calories = val;
            return this;    // 이렇게 하면 . 으로 체인을 이어갈 수 있다.
        }
        public Builder fat(int val) {
            fat = val;
            return this;
        }
        public Builder carbohydrate(int val) {
            carbohydrate = val;
            return this;
        }
        public Builder sodium(int val) {
            sodium = val;
            return this;
        }
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder) {
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
        sodium       = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

위와 같이 하면 다음처럼 객체를 생성해 사용하면 된다.

NutritionFacts cocaCola = new NutritionFacts
    .Builder(240, 8)    // 필수값 입력
    .calories(100)
    .sodium(35)
    .carbohydrate(27)
    .build();           // build() 가 객체를 생성해 돌려준다.

이렇게 정리해보니 궁금함이 생겼다. 생성자의 단점을 보완해서 나온게 빌더패턴이라면 생성자말고 빌더패턴만 사용하는게 좋지 않나? 그런데 왜 생성자와 빌더패턴을 섞어서 사용할까? => 스터디 리더분께 물어보니 명쾌하게 의견을 주셨다. 정답은 없다고. 생성자의 매개변수가 많아지면 네임드 파라미터의 빌더 패턴 사용을 고려해보는 것처럼 그 상황과 용도에 맞게 더 fit한 생성자를 이용하는게 더 좋다고. 결국 프로그래밍에 있어서 각 방법의 장단점을 확실하게 인지하고 상황에 맞는 방법을 유연하게 적용할 수 있는 능력이 정말 중요한 것 같다.

references

Effective Java 2/E
자바의 정석
https://johngrib.github.io/wiki/builder-pattern/
https://softwareengineering.stackexchange.com/questions/380397/why-do-we-need-a-builder-class-when-implementing-a-builder-pattern

 

Why do we need a Builder class when implementing a Builder pattern?

I have seen many implementations of the Builder pattern (mainly in Java). All of them have an entity class (let's say a Person class), and a builder class PersonBuilder. The builder "stacks" a vari...

softwareengineering.stackexchange.com

 

 

https://yjna2316.github.io/study/2020/11/06/%EC%83%9D%EC%84%B1%EC%9E%90%EC%99%80-%EB%B9%8C%EB%8D%94%ED%8C%A8%ED%84%B4/

 

 

+ Recent posts