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 기본키 생성 전략, @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로 설정해서 쓰자!
'Java > Spring Boot JPA' 카테고리의 다른 글
[Trouble Shooting] Hibernate: "Field 'id' doesn't have a default value" (1) | 2024.07.23 |
---|---|
Request Body로 보내지는 JSON의 행방 불명 (3) | 2024.05.16 |
[Spring Batch] 조져보자 meta table (0) | 2024.04.29 |
생성자와 Builder패턴 (1) | 2024.04.26 |
not importing JSONException and JSONObject issue (0) | 2024.04.04 |