[자바 ORM 표준 JPA] JPA 기본키 매핑
[자바 ORM 표준 JPA] JPA 기본키 매핑
기본키 매핑
기본 키 매핑 어노테이션#
- @Id
- @GeneratedValue
@Id @GeneratedValue(strategy = GenerationTpye.AUTO)
private Long id;
기본 키 매핑 방법#
- 직접 할당 : @Id 만 사용
- 자동 생성(@GeneratedValue)
- IDENTITY : 데이터베이스에 위임, MySLQ)
- SEQUENCE : 데이터베이스 시퀀스 오브젝트 사용, ORACLE
- @SequenceGenerator 필요
- TABLE: 키 생성용 테이블 사용, 모든 DB에서 사용
- @TableGenerator 필요
- AUTO: 방언에 따라 자동 지정, 기본값
Id 직접할당 - @Id 사용#
Member.java 기본 키 매핑에 좀더 집중하기 위해 Memeber의 Id를 String 으로 변경하고, Id, name을 제외한 나머지를 일단 제거합니다.
package hellojpa;
import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
@Entity
public class Member {
@Id
private String id;
@Column(name="name", length = 10)
private String userName;
public Member() {
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
JpaMain.java
Member member1 = new Member();
member1.setId("USER_A"); // 아이디 채번을 직접하여 할당
member1.setUserName("유저A");
em.persist(member1);
Member member2 = new Member();
member2.setId("USER_B");
member2.setUserName("유저B");
em.persist(member2);
tx.commit();
Id 자동할당 - @GeneratedValue#
GeneratedValue의 전략은 AUTO, IDENTITY, SEQUENCE, TABLE가 있으며,
AUTO는 방언에 따라 다르게 생성
@GeneratedValue(strategy = GenerationType.AUTO)
@GeneratedValue(strategy = GenerationType.IDENTITY) 위임함
@eneratedValue(strategy = GenerationType.SEQUENCE)
@GeneratedValue(strategy = GenerationType.TABLE)
GenerationType.IDENTITY#
GenerationType.IDENTITY - 특징#
- 기본 키 생성을 데이터베이스에 위임
- 주로 MySQL, PostgreSQL, SQL Server. DB2에서 사용
예) MySQL의 AUTO_INCREMENT- JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL 실행
- AUTO_INCREMENMT는 데이터베이스에 INSERT SQL을 실헹한 이후에 ID 값을 알 수 있음
- IDENTITY 전략은 em.persist() 시점에 즉시 INSERT_SQL 실행하고 DB에서 식별자를 조회
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private String id;
Hibernate:
drop table Member if exists
Hibernate:
create table Member (
id varchar(255) generated by default as identity,
name varchar(10),
primary key (id)
)
12월 26, 2021 10:48:37 오후 org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl getIsolatedConnection
INFO: HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@2a2da905] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode.
12월 26, 2021 10:48:37 오후 org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl getIsolatedConnection
INFO: HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@1b11ef33] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode.
12월 26, 2021 10:48:37 오후 org.hibernate.tool.schema.internal.ExceptionHandlerLoggedImpl handleException
WARN: GenerationTarget encountered exception accepting command : Error executing DDL "
create table Member (
id varchar(255) generated by default as identity,
name varchar(10),
primary key (id)
)" via JDBC Statement
org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL "
create table Member (
id varchar(255) generated by default as identity,
name varchar(10),
primary key (id)
)" via JDBC Statement
at org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase.accept(GenerationTargetToDatabase.java:67)
at org.hibernate.tool.schema.internal.SchemaCreatorImpl.applySqlString(SchemaCreatorImpl.java:440)
at org.hibernate.tool.schema.internal.SchemaCreatorImpl.applySqlStrings(SchemaCreatorImpl.java:424)
at org.hibernate.tool.schema.internal.SchemaCreatorImpl.createFromMetadata(SchemaCreatorImpl.java:315)
at org.hibernate.tool.schema.internal.SchemaCreatorImpl.performCreation(SchemaCreatorImpl.java:166)
at org.hibernate.tool.schema.internal.SchemaCreatorImpl.doCreation(SchemaCreatorImpl.java:135)
at org.hibernate.tool.schema.internal.SchemaCreatorImpl.doCreation(SchemaCreatorImpl.java:121)
at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.performDatabaseAction(SchemaManagementToolCoordinator.java:155)
at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.process(SchemaManagementToolCoordinator.java:72)
at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:310)
at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:467)
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:939)
at org.hibernate.jpa.HibernatePersistenceProvider.createEntityManagerFactory(HibernatePersistenceProvider.java:56)
at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:79)
at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:54)
at hellojpa.JpaMain.main(JpaMain.java:12)
Caused by: org.h2.jdbc.JdbcSQLFeatureNotSupportedException: Feature not supported: "CHARACTER VARYING(255)"; SQL statement:
create table Member (
id varchar(255) generated by default as identity,
name varchar(10),
primary key (id)
) [50100-202]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:556)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:477)
at org.h2.message.DbException.get(DbException.java:223)
at org.h2.message.DbException.get(DbException.java:199)
at org.h2.message.DbException.getUnsupportedException(DbException.java:287)
at org.h2.command.ddl.SequenceOptions.getBounds(SequenceOptions.java:313)
at org.h2.command.ddl.SequenceOptions.getBounds(SequenceOptions.java:244)
at org.h2.schema.Sequence.<init>(Sequence.java:101)
at org.h2.table.Column.initializeSequence(Column.java:459)
at org.h2.command.ddl.CommandWithColumns.generateSequences(CommandWithColumns.java:103)
at org.h2.command.ddl.CreateTable.update(CreateTable.java:110)
at org.h2.command.CommandContainer.update(CommandContainer.java:173)
at org.h2.command.Command.executeUpdate(Command.java:247)
at org.h2.server.TcpServerThread.process(TcpServerThread.java:413)
at org.h2.server.TcpServerThread.run(TcpServerThread.java:190)
at java.base/java.lang.Thread.run(Thread.java:834)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:507)
at org.h2.engine.SessionRemote.done(SessionRemote.java:611)
at org.h2.command.CommandRemote.executeUpdate(CommandRemote.java:237)
at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:228)
at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:201)
at org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase.accept(GenerationTargetToDatabase.java:54)
... 15 more
12월 26, 2021 10:48:37 오후 org.hibernate.tool.schema.internal.SchemaCreatorImpl applyImportSources
INFO: HHH000476: Executing import script 'org.hibernate.tool.schema.internal.exec.ScriptSourceInputNonExistentImpl@1eba372c'
Member.java
persistence.xml에서 MySQL 방언으로 다시 실행
AUTO_INCREMENT로 키를 생성하겠다 설정이 된것을 볼 수 있습니다.
h2 데이터베이스는 꼭 다음 링크에 들어가서 1.4.200 버전을 설치해주세요. 최근에 나온 2.0.202 버전을 설치하면 일부 기능이 정상 동작하지 않습니다H2 1.4.202 버전을 사용하고 있는데, @GeneratedValue(strategy = GenerationType.IDENTITY) 가 적용이 안되고,
ERROR: NULL not allowed for column “ID”; SQL statement: /* insert hellojpa.Member */ insert into Member (id, name) values (null, ?) [23502-200]
오류가 발생하여 검색을 해보니 최신 H2 DB에 버그라고 하네요.
2.0.200 버전으로 다시 설치 하였습니다.
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(id, name)
values
(null, ?)
12월 26, 2021 11:09:52 오후 org.hibernate.engine.jdbc.spi.SqlExceptionHelper logExceptions
WARN: SQL Error: 23502, SQLState: 23502
12월 26, 2021 11:09:52 오후 org.hibernate.engine.jdbc.spi.SqlExceptionHelper logExceptions
ERROR: NULL not allowed for column "ID"; SQL statement:
/* insert hellojpa.Member */ insert into Member (id, name) values (null, ?) [23502-200]
12월 26, 2021 11:09:52 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/test]
Process finished with exit code 0
IDENTITY 전략 애매한점#
strategy = GenerationType.IDENTITY를 사용할 경우 Key를 Null로 하여 DB에 인서트할 당시에 키가 생성하게됩니다.
영속성 컨텍스트에서 관리를 하기 위해서는 PK값이 있어야 합니다. 하지만 이 전략은 DB에 들어가봐야 PK를 알 수 있습니다. 그래서 제약이 생기게 됩니다.
GeneratedValue 전략을 다시 GenerationType.IDENTITY로 바꿔서 테스트 해보겠습니다.
Memeber.java
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
JpaMain.java
System.out.println("----------- 0");
Member member1 = new Member();
member1.setUserName("유저A");
em.persist(member1);
System.out.println("member1.id : " +member1.getId());
System.out.println("----------- 1");
Member member2 = new Member();
member2.setUserName("유저A");
em.persist(member2);
System.out.println("member2.id : " +member2.getId());
System.out.println("----------- 2");
tx.commit();
System.out.println("----------- commit");
persist() 시점에 Insert SQL 이 날라가게 됩니다. 그리고 바로 영속성 컨텍스트의 1차 캐시에 Id 값을 가져올 수 있는 것 을 확인 할 수 있습니다.
GenerationType.SEQUENCE#
GenerationType.SEQUENCE - 특징#
- 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트( 예) 오라클 시퀀스)
- 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용
Member.java
GeneratedValue 전략을 GenerationType.SEQUENCE 로 변경
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private String id;
GeneratedValue(strategy = GenerationType.SEQUENCE) 적용시 오류 발생이 되었고 오류는 아래와 같습니다.
javax.persistence.PersistenceException: org.hibernate.id.IdentifierGenerationException: Unknown integral data type for ids : java.lang.String
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:154)
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:181)
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:188)
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:807)
at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:785)
at hellojpa.JpaMain.main(JpaMain.java:22)
Caused by: org.hibernate.id.IdentifierGenerationException: Unknown integral data type for ids : java.lang.String
at org.hibernate.id.IdentifierGeneratorHelper.getIntegralDataTypeHolder(IdentifierGeneratorHelper.java:224)
at org.hibernate.id.enhanced.SequenceStructure$1.getNextValue(SequenceStructure.java:98)
at org.hibernate.id.enhanced.NoopOptimizer.generate(NoopOptimizer.java:40)
at org.hibernate.id.enhanced.SequenceStyleGenerator.generate(SequenceStyleGenerator.java:482)
at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:119)
at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:192)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:135)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:62)
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:800)
... 2 more
Caused by: org.hibernate.id.IdentifierGenerationException: Unknown integral data type for ids : java.lang.String
알 수 없는 정수 데이터 유형 : String
Member.java의 Id 가 현재 String이라 나오던 문제였습니다.
Member.java - String Id -> Long Id로 변경
package hellojpa;
import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(name="name", length = 10) // 엔티티 명은 userName으로, DB 컬럼명을 name으로 매핑하여 사용 지정
private String userName;
// JPA 기본적으로 동적으로 객체를 생성하는 기능이 있어, 기본 생성자도 추가해줘야 된다.
public Member() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
call next value for hibernate_sequence 기본 시퀀스를 사용해서 새로운 키값을 생성하게 되는데, 테이블 마다 시퀀스를 따로 관리하고 싶다면 @SequnceGenerator를 사용하면 됩니다.
Sequence 전략 - 매핑#
Member.java
@Entity
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR"
, sequenceName = "MEMBER_SEQ" // 매핑할 데이터베이스 시퀀스 이름
, initialValue = 1, allocationSize = 1
)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE
, generator = "MEMBER_SEQ_GENERATOR") //사용할 Generator 매핑
private Long id;
SEQUENCE - @SequenceGenerator#
- 주의 allocationSize 기본 값 = 50
속성 | 설명 | 기본값 |
---|---|---|
name | 식별자 생성기 이름 | 필수 |
sequenceName | 데이터베이스에 등록되어있는 시퀀스 이름 | hibernate_sequence |
initialValue | DDL 생성 시에만 사용됨, 시퀀스 DDL을 생성할 때 처음 시작하는 수를 1로 지정한다. | 1 |
allocationSize | 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용됨) 데이터베이스 시퀀스 값이 하나씩 증가하도록 설정되어 있으면 이 값을 반드시 1로 설정해야 한다 |
50 |
catalog, schema | 데이터베이스 catalog, schema 이름 |
SEQUENCE - 추가 설명#
GeneratedValue의 전략을 GenerationType.SEQUENCE로 사용하게 되면 sequence를 생성하게 되는데
Hibernate:
drop table Member if exists
Hibernate:
drop sequence if exists MEMBER_SEQ
Hibernate: create sequence MEMBER_SEQ start with 1 increment by 1 // 1부터 시작하여, 1씩 증가한다.
Hibernate:
JpaMain.java
System.out.println("----------- 0");
Member member1 = new Member();
member1.setUserName("유저A");
em.persist(member1);
System.out.println("member1.id : " +member1.getId());
System.out.println("----------- 1");
Member member2 = new Member();
member2.setUserName("유저A");
em.persist(member2);
System.out.println("member2.id : " +member2.getId());
System.out.println("----------- 2");
tx.commit();
System.out.println("----------- commit");
SEQUECE 전략도 영속성 컨텍스트에 저장할 당시 PK 값이 필요하기 때문에,
----------- 0
Hibernate:
call next value for MEMBER_SEQ
member1.id : 1
----------- 1
———– 0 과 ———– 1 사이에서 call next value for MEMBER_SEQ 시퀀스 nextVal으로 키값을 받아, 영속성 컨텍스트의 1차 캐시에 이미 id와 userName이 들어간 상태이고 2번째 유저 또한 ———– 1 과 ———– 2 영속성 컨텍스트에 저장됩니다.
이후 commit()을 하게되면 Insert를 하게됩니다.
IDENTITY에서는 안되지만, SEQUENCE에서는 JDBC BATCH를 이용한 버퍼를 이용할 수 있습니다.
이렇게 보다보니 성능에 한번에 인서트 하는게 아니라 seq 얻어올때, Insert 할때 자꾸 DB에 네트워킹을 통해 성능적으로 떨어지는 것 아닌가 싶기도합니다.
그래서 성능 최적화를 위하여 JPA에 allocationSize로 성능을 최적화 하는 방법이 있습니다. 자세한 설명은 뒤에서 하겠습니다.
GenerationType.TABLE#
GenerationType.TABLE - 특징#
- 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략
장점 : 모든 데이터베이스에 적용 가능
단점 : 성능
Table 전략 - 매핑#
Member.java
@Entity
@TableGenerator(
name = "MEMBER_SEQ_GENERATOR"
, table = "MY_SEQUENCE" // 매핑할 테이블명
, pkColumnValue = "MEMBER_SEQ" , allocationSize = 1)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.TABLE
, generator = "MEMBER_SEQ_GENERATOR") //사용할 Generator 매핑
private Long id;
ctrate table MY_SEQUENCE (
sequence_name varchar(255) not null,
next_val bigint,
primary ket (sequence_name)
)
수정하고 애플리케이션 재시작 하면, JPA가 자동으로 MY_SEQUENCE 테이블을 생성해준다.
@TableGenerator - 속성#
속성 | 설명 | 기본값 |
---|---|---|
name | 식별자 생성기 이름 | 필수 |
table | 키 생성 테이블 이름 | hibernate_sequences |
pkColumnName | 시퀀스 컬럼명 | sequence_name |
valueColumnName | 시퀀스 값 컬럼명 | next_val |
pkColumnValue | 키로 사용할 값 이름 | 엔티티 이름 |
initalValue | 초기 값, 마지막으로 생성된 값이 기준이다. | 0 |
allocationSize | 시퀀스 한 번 호출에 증가하는 수 | 50 |
catalog, schema | 데이터베이스 catalog, schema 이름 | |
uniqueConstains(DDL) | 유니크 제약 조건을 지정할 수 있다. |
운영에서는 table 전략을 사용하기는 부담스럽고 각 테이블의 sequence를 사용한 sequence 전략을 적극 채용하여 사용한다고 합니다.
권장하는 식별자 생성 전략#
- 기본 키 제약 조건 : null 아님, 유일, 변하면 안된다.
- 미래까지 이 조건을 만족하는 자연키를 찾기 어렵다. 대리키(대체키)를 사용하자. ( 비즈니스와 상관없는 랜덤키)
- 예를 들어 주민등록번호도 기본 키로 적절하지 않다.
- 권장 : Long형 + 대체키 + 카 생성전략 사용
10억이 넘어도 동작해야 하니까 Long형, 시퀀스를 쓴다던가, uuid를 쓴다던가 대체키를 쓰시고, 카생성 전략들을 조합해서 사용하는 것을 권장
AUTO-INCREMENT나 SEQUENCE Object 둘중 하나를 사용하시고 아니면 때에 따라서 uuid, 랜덤 값을 조합한 회사내의 룰을 따르길 권장.
절대 비즈니스 로직을 키로 끌고 오는것을 권장하지는 않는다고 합니다.
allocationSize를 이용한 성능향상#
allocationSize를 1로 설정하여 1씩 증가하게 세팅을 해두었는데, 기본 값은 50 입니다. 기본 값이 50인 이유는 JPA는 새로운 키 50개를 한번에 만들어 놓고,
DB에 50으로 세팅하고 메모리 상에서 1부터 50 까지 순차적으로 사용합니다. 이후 50개를 모두 사용하면, call next를 하여 51 부터 100까지 미리 만들어 사용합니다.
또 대단한 것이 어떤 DB를 사용해도 이슈 없이 동작한다고 합니다.
@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;
현재 값은 -1이고 증가는 50 인것을 볼 수 있습니다.
JpaMain.java는 객체 생성 및 persist() 모두 삭제
call next value for MEMBER_SEQ;
위 SQL을 통항 next value를 가져오면 1이 되길 원하는 것입니다.
JpaMain.java
System.out.println("----------- 0");
Member member1 = new Member();
member1.setUserName("유저A");
em.persist(member1);
System.out.println("member1.id : " +member1.getId());
System.out.println("----------- 1");
Member member2 = new Member();
member2.setUserName("유저B");
//em.persist(member2);
System.out.println("member2.id : " +member2.getId());
System.out.println("----------- 2");
Member member3 = new Member();
member3.setUserName("유저C");
//em.persist(member3);
System.out.println("member2.id : " +member2.getId());
System.out.println("----------- 3");
tx.commit();
System.out.println("----------- commit");
처음
call next value for MEMBER_SEQ;
를 호출하고 1이면 50개의 키를 메모리로 가져온 것이 아니기 때문에 1번더 호출합니다.
다시 JpaMain에 persist를 추가하여 테스트 해보면
System.out.println("----------- 0");
Member member1 = new Member();
member1.setUserName("유저A");
System.out.println("----------- 1");
Member member2 = new Member();
member2.setUserName("유저B");
System.out.println("----------- 2");
Member member3 = new Member();
member3.setUserName("유저C");
System.out.println("----------- 3");
em.persist(member1); // call next value for MEMBER_SEQ; DB SEQ = 1 , Key = 1
em.persist(member2); // call next value for MEMBER_SEQ; DB SEQ = 51 , Key = 2
em.persist(member3); // memory; DB SEQ = 51 , Key = 3
System.out.println("----------- 4");
tx.commit();
System.out.println("----------- commit");
allocationSize를 크게 잡아 메모리에 두고 사용할 수 있지만, 애플리케이션이 내려가게 되면 사용하지 않았던 Id 값들의 구멍이 생겨 낭비가 될 수 있기 때문에 50 ~ 100 정도가 적당합니다.
Table 전략 또한 동일하며, 테이블의 데이터를 미리 설정한 값 만큼 미리 세팅하고 키를 메모리에서 생성하여 사용합니다. 물론 서버가 여러대인 경우에도 서버 각각 중복되지 않고 호출한 순서대로 1~50
, 50 ~100 씩 할당받아 메모리에 키를 두고 사용합니다. 때문에 동시성 문제가 발생하지 않고 문제없이 동작합니다.